diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d7b482e..5d90df9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: id: what-happened attributes: label: What happened? - description: A clear description of the bug, including which Project or Root was involved. + description: A clear description of the bug, including which Project was involved. validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index cba161d..941d690 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -15,7 +15,7 @@ body: id: solution attributes: label: Proposed solution - description: What you would like cdd to do, using the project's vocabulary (Root, Kind, Project, Jump, Visit, History, Scan, Picker, Wrapper) where relevant. + description: What you would like cdd to do, using the project's vocabulary (Project, Jump, Visit, History, Scan, Picker, Wrapper) where relevant. validations: required: true - type: textarea diff --git a/CONTEXT.md b/CONTEXT.md index 6893ba0..6c95e8d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -4,16 +4,8 @@ A terminal UI for jumping to recently used projects. It remembers where you have ## Language -**Root**: -The top-level directory whose Kinds are searched for Projects. -_Avoid_: Base, home, workspace, projects dir - -**Kind**: -A first-level directory under Root that groups Projects by purpose. -_Avoid_: Category, group, type, namespace - **Project**: -A directory exactly one level below a Kind, identified by its path relative to Root. It need not be a repository or contain anything in particular. +A directory holding a git repository (a `.git` directory or file), identified by its absolute path. It may sit at any depth, and a directory that is not a repository is never a Project. _Avoid_: Repo, workspace, folder, directory **Jump**: @@ -25,7 +17,7 @@ A single recorded Jump to a Project, or an entry seeded for a Project by a Scan. _Avoid_: Access, hit, entry, usage **Stale Visit**: -A Visit whose Project is no longer found under Root, whatever the cause. It contributes nothing to the Picker and is never pruned; it ages out of History. +A Visit whose Project no longer has a `.git` at its path, whatever the cause. It contributes nothing to the Picker and is never pruned; it ages out of History. _Avoid_: Orphan, dead entry, dangling, missing project **History**: @@ -33,7 +25,7 @@ The ordered record of Visits from which recent Projects are derived. _Avoid_: Cache, log, database, store **Scan**: -A one-off sweep of Root that seeds History with a Visit per Project, dated from evidence found inside the Project itself. +A one-off sweep of the directories given to it (the home directory by default) that seeds History with a Visit per Project found at any depth below them, dated from evidence found inside the Project itself. Nothing about which directories were swept is remembered. _Avoid_: Import, index, crawl, rebuild **Picker**: @@ -41,7 +33,7 @@ The interactive screen that lists Projects and lets the user choose one to Jump _Avoid_: Menu, list, finder, selector **Layout**: -One of the arrangements in which the Picker draws Projects: the Grouped Layout puts them under Kind headers, the List Layout in one flat run. +One of the arrangements in which the Picker draws Projects. The List Layout, the only one today, draws them in one flat run. _Avoid_: View, mode, style, theme, skin **Wrapper**: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac091c2..b6ce69b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ recently used Projects. ## Vocabulary Before writing code or docs, read [`CONTEXT.md`](CONTEXT.md). It is the -glossary for this project: terms like Root, Kind, Project, Jump, Visit, Stale +glossary for this project: terms like Project, Jump, Visit, Stale Visit, History, Scan, Picker, and Wrapper have precise, agreed meanings. Use those terms verbatim in code, comments, commit messages, and pull requests instead of synonyms, so the codebase and its discussions share one diff --git a/README.md b/README.md index 7378627..d3b4724 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # cdd -A TUI that lets you jump to recent Projects. +A TUI that lets you jump to the git repositories you work in, most recent +first. There is nothing to lay out or configure: `cdd scan` finds every +repository below the directories you give it, at any depth. ## Demo - + ![cdd demo](demo/demo.gif) ## Install @@ -54,21 +56,35 @@ eval "$(cdd init zsh)" ## First run -Create a `config.toml` (see [Config reference](#config-reference)) with at -least a `root`, then seed History with a Scan of everything already under -Root: +Seed History with a Scan. With no arguments it walks your home directory; +give it one or more directories to walk those instead: ```sh -cdd scan +cdd scan # every git repository below ~ +cdd scan ~/Developer ~/work # only below these ``` +A Project is any directory holding a `.git` directory or file (so +worktrees and submodules count). The walk goes to any depth but stops at a +repository, so repositories nested inside one are not listed, unless you +name that repository to `cdd scan` itself. It does not follow symlinks, +skips hidden directories and anything matched by `exclude`, and passes over +directories it cannot read. Run it again whenever you clone something new: +it never overrides the date of a real Jump. + ## Usage ```sh cdd # open the Picker over History, ordered by recency -cdd # open the Picker pre-filtered by query +cdd # Jump straight there when exactly one Project matches, else + # open the Picker pre-filtered by query ``` +A query matches a Project straight away when it is the Project's name +(`cdd cdd`), a trailing part of its path (`cdd tools/cdd`), or its whole +path. The Picker lists only Projects in History, and drops any whose `.git` +has since gone. + ### Keys Default key map: @@ -96,63 +112,63 @@ Vim key map (`keys.vim = true`): the list is focused on open. ### Layout -The Picker ships two Layouts, selected with `picker.layout`. - -The **Grouped Layout** (`grouped`, the default) groups Projects under Kind -headers, marks the selected row with a caret, and puts the filter line -above the list. - -The **List Layout** (`list`) is a flat fzf-style run: no Kind headers, -Projects in History order with never-visited ones last, `kind/` muted -before each Project name, the filter prompt below the list, and a `▌` bar -plus a background highlight on the selected row. - -Both draw the same preview pane, use the same keys, status glyphs and -colours, and degrade the same way on a narrow terminal. +The Picker draws the **List Layout**: a flat fzf-style run of Projects in +History order, each Project's parent directory muted before its name (`~` +standing in for your home directory, and trimmed from the start on a narrow +terminal), the filter prompt below the list, and a `▌` bar plus a +background highlight on the selected row. The filter matches the parent +directory as well as the name. ## Config reference `cdd` reads `config.toml` from `$XDG_CONFIG_HOME/cdd/config.toml`, falling -back to `~/.config/cdd/config.toml` when `XDG_CONFIG_HOME` is unset. +back to `~/.config/cdd/config.toml` when `XDG_CONFIG_HOME` is unset. The +file is optional, and so is every key in it; these are the defaults: ```toml -root = "~/Developer" # required, no default -exclude = [] # paths relative to Root, glob-matched +exclude = [] include_hidden = false [history] -max_visits = 1000 # must be >= 1 +max_visits = 1000 [keys] vim = false [picker] -layout = "grouped" # or "list" for the flat fzf-style layout +layout = "list" ``` -- `root` (required): the top-level directory whose Kinds are searched for - Projects. A leading `~` is expanded to the user's home directory; - `$VAR` is left as-is. -- `exclude`: paths relative to Root, matched with `path.Match` semantics, - that are skipped when discovering Kinds and Projects. -- `include_hidden`: when `false` (the default), hidden directories are - excluded at both Kind and Project level. +- `exclude`: glob patterns (`filepath.Match` semantics) for directories + `cdd scan` skips, along with everything below them. A pattern containing + `/` is matched against the directory's absolute path, any other against + its name alone, as in `.gitignore`: `["node_modules", "~/go/pkg/*"]`. A + leading `~` is expanded to your home directory; `$VAR` is left as-is. +- `include_hidden`: when `false` (the default), `cdd scan` skips hidden + directories. - `[history].max_visits`: the maximum number of Visits kept in History. Must be at least 1; defaults to `1000`. - `[keys].vim`: when `true`, the Picker opens with the list focused and uses the vim key map described above. Defaults to `false`, the default key map. -- `[picker].layout`: which Layout the Picker draws, `"grouped"` (the - default) or `"list"`, as described above. Any other value is a config - error. +- `[picker].layout`: which Layout the Picker draws. `"list"`, described + above, is the default and, for now, the only one. Any other value is a + config error. + +Upgrading from v0.2: `root` and the `"grouped"` layout are gone. Delete +them from `config.toml` (cdd names the offending line), then run `cdd scan` +to rebuild History, since the old entries were stored relative to Root and +are ignored. History is stored at `$XDG_DATA_HOME/cdd/history`, falling back to `~/.local/share/cdd/history` when `XDG_DATA_HOME` is unset. ## How it works -A Scan sweeps Root once and seeds History with a Visit per discovered -Project, dated from evidence found inside the Project itself. The Picker -lists Projects drawn from History, most recently visited first, and lets -you choose one; choosing a Project records a new Visit and hands its path -to the Wrapper, which turns it into a Jump in your shell. +A Scan walks the directories it is given once and seeds History with a +Visit per git repository it finds, dated from the repository's last commit +(or the directory's mtime, before the first commit). The Picker lists +Projects drawn from History alone, most recently visited first, so opening +it never walks the disk; choosing a Project records a new Visit and hands +its path to the Wrapper, which turns it into a Jump in your shell. See +[ADR 0001](docs/adr/0001-projects-are-git-repos-found-by-scan.md) for why. ## Contributing diff --git a/demo/demo.gif b/demo/demo.gif index b282d4a..db6f8e1 100644 Binary files a/demo/demo.gif and b/demo/demo.gif differ diff --git a/demo/demo.tape b/demo/demo.tape index 9ae5df0..4822cc8 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -1,5 +1,5 @@ # Records demo/demo.gif against the environment built by demo/setup.sh. -# Run `demo/setup.sh` first; it never touches your real Root or History. +# Run `demo/setup.sh` first; it never touches your real home or History. # # demo/setup.sh && vhs demo/demo.tape # @@ -19,12 +19,13 @@ Set TypingSpeed 60ms Env XDG_CONFIG_HOME "/tmp/cdd-demo/xdg/config" Env XDG_DATA_HOME "/tmp/cdd-demo/xdg/data" +Env HOME "/tmp/cdd-demo/home" Hide Type "set -gx PATH /tmp/cdd-demo/bin $PATH" Enter Type "set -g fish_autosuggestion_enabled 0" Enter Type "cdd init fish | source" Enter -Type "cd /tmp/cdd-demo/root/lab/notes" Enter +Type "cd ~/notes" Enter Type "clear" Enter Show diff --git a/demo/setup.sh b/demo/setup.sh index 1057b8f..3c8f2aa 100755 --- a/demo/setup.sh +++ b/demo/setup.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash # Builds a self-contained demo environment for recording cdd, so the -# recording never touches your real Root, config, or History. +# recording never touches your real home, config, or History. # # demo/setup.sh # builds /tmp/cdd-demo # DEMO_DIR=~/x demo/setup.sh # # Layout it creates: -# $DEMO_DIR/root a fake Root with five Kinds and a few Projects +# $DEMO_DIR/home a fake home with git repositories at several depths # $DEMO_DIR/xdg/config XDG_CONFIG_HOME holding cdd/config.toml # $DEMO_DIR/xdg/data XDG_DATA_HOME holding cdd/history # $DEMO_DIR/bin/cdd the binary built from this checkout set -euo pipefail DEMO_DIR="${DEMO_DIR:-/tmp/cdd-demo}" -ROOT="$DEMO_DIR/root" +ROOT="$DEMO_DIR/home" CONFIG="$DEMO_DIR/xdg/config" DATA="$DEMO_DIR/xdg/data" BIN="$DEMO_DIR/bin" @@ -27,7 +27,7 @@ mkdir -p "$ROOT" "$CONFIG/cdd" "$DATA/cdd" "$BIN" # out so `cdd scan` seeds a varied History. git_demo() { git -c user.name=demo -c user.email=demo@example.com -c commit.gpgsign=false "$@"; } -repo() { # repo +repo() { # repo local dir="$ROOT/$1"; mkdir -p "$dir" git_demo -C "$dir" init -q -b main echo "# ${1##*/}" > "$dir/$3" @@ -41,54 +41,44 @@ ago() { # ago -> ISO timestamp, GNU or BSD date || date -u -v-"$1"d +%Y-%m-%dT%H:%M:%SZ } -repo domain/kryft.dev 1 README.md -repo domain/wowaitech.com 3 README.md -repo domain/candidex.dev 12 README.md -repo tools/cdd 0 main.go -repo tools/grg 6 main.go -repo tools/dotfiles 40 README.md -repo ops/internal 2 README.md -repo ops/backups 90 README.md -repo lab/bubbletea-play 5 main.go -repo uni/compilers 20 notes.md +repo Developer/domain/kryft.dev 1 README.md +repo Developer/domain/wowaitech.com 3 README.md +repo Developer/tools/cdd 0 main.go +repo Developer/tools/grg 6 main.go +repo Developer/tools/lab/bubbletea-play 5 main.go +repo work/internal 2 README.md +repo work/clients/acme/api 9 README.md +repo dotfiles 40 README.md +repo uni/2025/compilers 20 notes.md # clean vs. dirty vs. untracked vs. ahead -echo "wip" >> "$ROOT/domain/wowaitech.com/README.md" # modified -touch "$ROOT/tools/grg/scratch.go" # untracked -echo "wip" >> "$ROOT/ops/internal/README.md"; touch "$ROOT/ops/internal/new.txt" # both +echo "wip" >> "$ROOT/Developer/domain/wowaitech.com/README.md" # modified +touch "$ROOT/Developer/tools/grg/scratch.go" # untracked +echo "wip" >> "$ROOT/work/internal/README.md"; touch "$ROOT/work/internal/new.txt" # both upstream="$DEMO_DIR/upstream.git" git_demo init -q --bare "$upstream" -git_demo -C "$ROOT/domain/kryft.dev" remote add origin "$upstream" -git_demo -C "$ROOT/domain/kryft.dev" push -q -u origin main -echo "more" >> "$ROOT/domain/kryft.dev/README.md" -git_demo -C "$ROOT/domain/kryft.dev" commit -q -am "second commit" # ahead by 1 +git_demo -C "$ROOT/Developer/domain/kryft.dev" remote add origin "$upstream" +git_demo -C "$ROOT/Developer/domain/kryft.dev" push -q -u origin main +echo "more" >> "$ROOT/Developer/domain/kryft.dev/README.md" +git_demo -C "$ROOT/Developer/domain/kryft.dev" commit -q -am "second commit" # ahead by 1 -mkdir -p "$ROOT/lab/notes" # not a repository -mkdir -p "$ROOT/.archive/old-thing" "$ROOT/uni/.hidden" # hidden, never listed +mkdir -p "$ROOT/notes/2026" # not a repository, never listed +repo Developer/tools/cdd/vendor/inner 1 README.md # inside a repository, never listed +repo .cache/some-tool 1 README.md # hidden, never listed # --- Config and History --------------------------------------------------- -cat > "$CONFIG/cdd/config.toml" <> "$hist" -printf '%s\tjump\t%s\n' "$(ago 1)" domain/kryft.dev >> "$hist" -printf '%s\tjump\t%s\n' "$(ago 2)" ops/internal >> "$hist" +printf '%s\tjump\t%s\n' "$(ago 0)" "$ROOT/Developer/tools/cdd" >> "$hist" +printf '%s\tjump\t%s\n' "$(ago 1)" "$ROOT/Developer/domain/kryft.dev" >> "$hist" +printf '%s\tjump\t%s\n' "$(ago 2)" "$ROOT/work/internal" >> "$hist" cat <