Repository navigation
v0.4.0 map: Actions, added Projects, generated config #61
Description
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
cddtakes 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.
- Every feature ticket that adds a config key also adds it, commented, to
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. - Removed config values (
root,picker.layout = "grouped") and the removed query argument fail as plain invalid input, with no "removed in vX" messages. - refactor(picker): the Layout type names frame sizing, not the Layout #54 (rename the picker's
Layoutsizing struct toMetricsandLayoutStyletoLayout) is folded in as an independent refactor. The Grouped Layout it mentions is gone; only the renames still apply. - 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, 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)
Activity
hammadmajid commentedon Oct 2, 2026
All tickets done. Released as v0.4.0: https://github.com/kryft-dev/cdd/releases/tag/v0.4.0