You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Map for v0.4.0: Actions in the Picker, non-git Projects, a generated config, and removing the query argument. Breaking. Settled in a grilling session.
Notes
cdd is the TUI. Typed jumps from the command line are zoxide's job, so cdd takes no query and always opens the Picker.
Action is a new domain term: a named command bound to a key and run on the selected Project. Custom commands, remapping Enter, presets, "open git remote" and "run in the project dir" are all Actions.
Working rules: micro commits in Conventional Commits format, one PR per ticket, no file over 300 lines, tests per package. Before merging: golangci-lint run ./... is clean locally, and gh pr checks <N> --watch is green (CI runs build, test, lint, the 300-line limit and goreleaser check).
Tickets
See sub-issues. Blocking edges are native issue dependencies.
feat: Actions — run a command on the selected Project with a key #62: Actions live in internal/action. Register a built-in by adding an action.Action{Name, Key, Run, Jump, Detach} to the builtins slice in action/action.go; action.Merge applies a user's [actions.<name>] field by field, and config.Config.ResolvedActions is the result (user-only Actions follow the built-ins, sorted by name, not in config order). The action.Runner interface (Start detached, Run on /dev/tty returning the exit status; ExecRunner is the real one) is injected as picker.Options.Runner and into jump.Resolve; picker.Run now returns a picker.Choice{Row, Action} (nil Action is the plain Enter Jump, which feat(picker): Enter is the remappable jump Action #63 should move onto the map as a built-in jump with no Run). Action keys dispatch before the key map in picker/update.go (boundAction), and Model.message is the footer message hook, shown in footerView in place of the key hints until the next key press (PR feat: Actions, named commands bound to keys in the Picker (#62) #76)
feat: built-in copy Action copies the Project path (ctrl+y) #66: built-in copy (ctrl+y) is handled Go-side, not by the Runner: action.Action has a new Copy bool (built-in only, not a TOML key; a user run or jump clears it, key rebinds it) and the Picker copies the path with action.Copy (first of wl-copy when WAYLAND_DISPLAY is set, xclip, xsel, pbcopy; injectable as picker.Options.Copy), else returns tea.SetClipboard for OSC 52; success sets the footer Model.message to copied <path> with Model.messageOK true, which footerView draws green instead of red, for feat(picker): key-hint footer and vim ? overlay #67 to restyle; the footer key hints do not list ctrl+y yet (PR feat: built-in copy Action copies the Project path (ctrl+y) (#66) #81)
feat(picker): key-hint footer and vim ? overlay #67: the footer keys line is built from picker.Options.Actions (keyHints in picker/view_footer.go): the Action on enter first, the rest in the order given, unbound ones skipped, whole hints dropped behind … when too wide, plain-key Actions hidden in the vim filter focus; action.Merge(user, order, vim) now takes the [actions.*] table names in config order (config.actionOrder) and returns the user's Actions first in that order, then the built-ins, so ResolvedActions is already the hint order; Model.message/messageOK still replace the hints until the next key (now drawn by theme.messageLine), with the count kept at the right edge; vim list focus ? opens a full-frame overlay (Model.help, view_help.go) that closes on ?/esc/q, swallows other keys except ctrl+c, and yields ? to an Action bound to it; [picker] hints (default true) maps to picker.Options.HideHints, which hides only the hints (count and messages stay); config.example.toml still needs [picker] hints from feat(cli): example config in releases and cdd config init/path/edit #70 (PR feat(picker): key-hint footer and vim ? overlay #82)
feat(cli): cdd add and cdd forget for non-git Projects #68: cdd add [dir] / cdd forget [dir] (default: cwd; add needs an existing directory, forget does not) write add <abs path> / forget <abs path> lines to projects beside History ($XDG_DATA_HOME/cdd/projects), last line wins, appended with one O_APPEND write; the API is project.OpenStore(path), (*Store).Add/Forget(absPath) and (*Store).Marks() returning Marks{IsAdded, IsForgotten, Added} (project.StorePath() is the default path); jump.Resolve and scan.Run now take a *project.Store, so feat(picker): forget the selected Project with ctrl+d #69 calls store.Forget(path) and refreshes its rows; Added directories with no Visit are listed after the visited ones with a zero LastVisit; ADR 0002 partly supersedes 0001 (PR feat(cli): cdd add and cdd forget for non-git Projects (#68) #83)
feat(picker): forget the selected Project with ctrl+d #69: built-in forget (ctrl+d) asks forget <name>? y/n on the footer (Model.confirm, checked before the help overlay and boundAction; y forgets, any other key cancels and is consumed), calls picker.Options.Forget (wired to store.Forget in jump.Resolve), drops the row and keeps the cursor index (a Forget error shows red and keeps the row); Action.Copy bool became Action.Internal string (action.InternalCopy, action.InternalForget: a Picker-side job with no run, cleared by a user run or jump), so a further Go-side Action is one constant and one case in runAction (PR feat(picker): forget the selected Project with ctrl+d (#69) #84)
feat(cli): example config in releases and cdd config init/path/edit #70: config.example.toml at the repo root is the single source: embedded as cdd.ConfigExample (root package, config_example.go), shipped in the release archives (archives.files now lists README.md, LICENSE and it), and config_example_test.go loads it through config.LoadFrom, as shipped and with the Action examples uncommented; a config key added later goes in that file, commented if it is not a default; cdd config init [--force] (refuses over an existing file), path and edit ($VISUAL, $EDITOR, vi via sh -c; inits first if missing) exist, config.Path() is the exported config location, and config is in the Wrapper passthrough (PR feat(cli): example config in releases and cdd config init/path/edit #85)
Map for v0.4.0: Actions in the Picker, non-git Projects, a generated config, and removing the query argument. Breaking. Settled in a grilling session.
Notes
cddtakes no query and always opens the Picker.config.example.toml(feat(cli): example config in releases and cdd config init/path/edit #70 adds that file, and a test keeps it valid). Tickets do not wait on feat(cli): example config in releases and cdd config init/path/edit #70 for this: whichever lands second adds the keys.root,picker.layout = "grouped") and the removed query argument fail as plain invalid input, with no "removed in vX" messages.Layoutsizing struct toMetricsandLayoutStyletoLayout) is folded in as an independent refactor. The Grouped Layout it mentions is gone; only the renames still apply.golangci-lint run ./...is clean locally, andgh pr checks <N> --watchis green (CI runs build, test, lint, the 300-line limit and goreleaser check).Tickets
See sub-issues. Blocking edges are native issue dependencies.
Decisions so far
feat!: remove the query argument; cdd always opens the Picker #71: jump.Resolve no longer takes a query and picker.Options.Query is now unused by cdd (still used by picker tests);
cdd pickand the root take no args (cobra.NoArgs), socdd fooexits 2 with "unknown command"; config removed-key/value messages are gone (PRs feat!: remove the query argument; cdd always opens the Picker (#71) #72, docs: cdd takes no query (#71) #73)refactor(picker): the Layout type names frame sizing, not the Layout #54: picker.LayoutStyle is now picker.Layout and the sizing struct Layout is now picker.Metrics (ComputeLayout -> ComputeMetrics, layout.go -> metrics.go) (PR refactor(picker): rename Layout sizing struct to Metrics, LayoutStyle to Layout (#54) #75)
feat: Actions — run a command on the selected Project with a key #62: Actions live in
internal/action. Register a built-in by adding anaction.Action{Name, Key, Run, Jump, Detach}to thebuiltinsslice inaction/action.go;action.Mergeapplies a user's[actions.<name>]field by field, andconfig.Config.ResolvedActionsis the result (user-only Actions follow the built-ins, sorted by name, not in config order). Theaction.Runnerinterface (Startdetached,Runon /dev/tty returning the exit status;ExecRunneris the real one) is injected aspicker.Options.Runnerand intojump.Resolve;picker.Runnow returns apicker.Choice{Row, Action}(nil Action is the plain Enter Jump, which feat(picker): Enter is the remappable jump Action #63 should move onto the map as a built-injumpwith noRun). Action keys dispatch before the key map inpicker/update.go(boundAction), andModel.messageis the footer message hook, shown infooterViewin place of the key hints until the next key press (PR feat: Actions, named commands bound to keys in the Picker (#62) #76)feat(picker): Enter is the remappable jump Action #63: Enter is the built-in
jumpAction (action.Builtins()[0], keyenter, no Run), dispatched like any Action; the hard-codedentercases andchoose()are gone, so apicker.Optionswith no Actions has no Enter, and picker tests passActions: action.Builtins(). Givingenterto another Action needsjumpmoved orkey = ""first (the feat: Actions — run a command on the selected Project with a key #62 duplicate-key error still applies, no automatic unbinding). Enter on an empty list is now a no-op. The footer key hints still hard-code "enter jump" for feat(picker): key-hint footer and vim ? overlay #67.Choice.Actionis always set by the Picker (nil is still tolerated as a Jump) (PR feat(picker): Enter is the remappable jump Action (#63) #77)feat(picker): Enter is the remappable jump Action #63 follow-up: a user binding displaces the built-in on that key (PR fix(action): a user binding displaces the built-in on its key (#63) #78)
feat: built-in files and editor Actions #64: built-ins
files(ctrl+o,action.Opener(runtime.GOOS) + " {path}", detached) andeditor(ctrl+e,${VISUAL:-${EDITOR:-vi}} {path}, attached; the shell resolves it at run time) are inbuiltins;action.Opener(goos)returnsopenon darwin andxdg-openotherwise, for feat: built-in remote Action opens the git remote (ctrl+g) #65 and feat: built-in copy Action copies the Project path (ctrl+y) #66 to reuse; the footer key hints still hard-code "enter jump" so the new keys show there only after feat(picker): key-hint footer and vim ? overlay #67; also fixed the three golangci-lint findings from feat: Actions — run a command on the selected Project with a key #62 (PR feat(action): built-in files and editor Actions (#64) #79)feat: built-in remote Action opens the git remote (ctrl+g) #65: built-in
remote(ctrl+g,Opener(GOOS) + " {remote}", detached) opens the git remote:{remote}in any Action'sRunexpands to the shell-quoted https home page oforigin, else the first remote (git.RemoteURL/git.WebURL);action.Commandnow returns(*exec.Cmd, error)andStart/Runfail withgit.ErrNoRemote/git.ErrNotRepo, which the Picker shows in the footer and stays open;[actions.remote] runreplaces it; feat: built-in copy Action copies the Project path (ctrl+y) #66 can reuse the same placeholder idea for a{remote}-style Go-resolved value (PR feat(action): built-in remote Action opens the git remote on ctrl+g (#65) #80)feat: built-in copy Action copies the Project path (ctrl+y) #66: built-in
copy(ctrl+y) is handled Go-side, not by the Runner:action.Actionhas a newCopy bool(built-in only, not a TOML key; a userrunorjumpclears it,keyrebinds it) and the Picker copies the path withaction.Copy(first of wl-copy when WAYLAND_DISPLAY is set, xclip, xsel, pbcopy; injectable aspicker.Options.Copy), else returnstea.SetClipboardfor OSC 52; success sets the footerModel.messagetocopied <path>withModel.messageOKtrue, whichfooterViewdraws green instead of red, for feat(picker): key-hint footer and vim ? overlay #67 to restyle; the footer key hints do not listctrl+yyet (PR feat: built-in copy Action copies the Project path (ctrl+y) (#66) #81)feat(picker): key-hint footer and vim ? overlay #67: the footer keys line is built from
picker.Options.Actions(keyHintsinpicker/view_footer.go): the Action onenterfirst, the rest in the order given, unbound ones skipped, whole hints dropped behind…when too wide, plain-key Actions hidden in the vim filter focus;action.Merge(user, order, vim)now takes the[actions.*]table names in config order (config.actionOrder) and returns the user's Actions first in that order, then the built-ins, soResolvedActionsis already the hint order;Model.message/messageOKstill replace the hints until the next key (now drawn bytheme.messageLine), with the count kept at the right edge; vim list focus?opens a full-frame overlay (Model.help,view_help.go) that closes on?/esc/q, swallows other keys except ctrl+c, and yields?to an Action bound to it;[picker] hints(default true) maps topicker.Options.HideHints, which hides only the hints (count and messages stay);config.example.tomlstill needs[picker] hintsfrom feat(cli): example config in releases and cdd config init/path/edit #70 (PR feat(picker): key-hint footer and vim ? overlay #82)feat(cli): cdd add and cdd forget for non-git Projects #68:
cdd add [dir]/cdd forget [dir](default: cwd; add needs an existing directory, forget does not) writeadd <abs path>/forget <abs path>lines toprojectsbeside History ($XDG_DATA_HOME/cdd/projects), last line wins, appended with one O_APPEND write; the API isproject.OpenStore(path),(*Store).Add/Forget(absPath)and(*Store).Marks()returningMarks{IsAdded, IsForgotten, Added}(project.StorePath()is the default path);jump.Resolveandscan.Runnow take a*project.Store, so feat(picker): forget the selected Project with ctrl+d #69 callsstore.Forget(path)and refreshes its rows; Added directories with no Visit are listed after the visited ones with a zeroLastVisit; ADR 0002 partly supersedes 0001 (PR feat(cli): cdd add and cdd forget for non-git Projects (#68) #83)feat(picker): forget the selected Project with ctrl+d #69: built-in
forget(ctrl+d) asksforget <name>? y/non the footer (Model.confirm, checked before the help overlay andboundAction;yforgets, any other key cancels and is consumed), callspicker.Options.Forget(wired tostore.Forgetinjump.Resolve), drops the row and keeps the cursor index (a Forget error shows red and keeps the row);Action.Copy boolbecameAction.Internal string(action.InternalCopy,action.InternalForget: a Picker-side job with norun, cleared by a userrunorjump), so a further Go-side Action is one constant and one case inrunAction(PR feat(picker): forget the selected Project with ctrl+d (#69) #84)feat(cli): example config in releases and cdd config init/path/edit #70:
config.example.tomlat the repo root is the single source: embedded ascdd.ConfigExample(root package,config_example.go), shipped in the release archives (archives.filesnow lists README.md, LICENSE and it), andconfig_example_test.goloads it throughconfig.LoadFrom, as shipped and with the Action examples uncommented; a config key added later goes in that file, commented if it is not a default;cdd config init [--force](refuses over an existing file),pathandedit($VISUAL,$EDITOR,viviash -c; inits first if missing) exist,config.Path()is the exported config location, andconfigis in the Wrapper passthrough (PR feat(cli): example config in releases and cdd config init/path/edit #85)docs(demo): re-record the demo with v0.4.0 features and stand-in project names #74: demo re-recorded with stand-in projects in ~/Code and ~/Work (setup.sh refuses if they exist, teardown.sh removes them); record it in the main session with VHS v0.11.0, since a subagent stalled (PR docs(demo): re-record the demo with v0.4.0 features and stand-in projects #86)
fix(picker): an Added non-git directory shows a red 'git could not read it' status #87: a Project with no
.git(git.NotRepo) now shows no row marker and a mutednot a git repoin the preview (no branch or sync); the red!is only for git.Unknown, whose preview readsgit could not read it; the legend is unchanged (PR fix(picker): a non-git Project shows no marker and a muted not a git repo (#87) #88)