Add / filter for the tree

A text input above the footer filters submodules by path (case-insensitive).
Matches keep their ancestors. A filtered-out selection moves to the next
visible submodule; esc closes the input and removes the filter.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:15:24 +02:00
co-authored by Claude Sonnet 5.5
parent 8aa12774eb
commit f5fabfd093
11 changed files with 351 additions and 21 deletions
+2 -2
View File
@@ -32,9 +32,9 @@ through a git provider (`internal/submodule/git`): `submarine [dir]`. It
fetches in the background on startup (`--no-fetch` to skip; `f` to fetch
again) and follows changes made in other shells. `submarine --mock` shows
the built-in mock data (`internal/submodule/mock`). The actions `u`, `b`,
`/`, `!` only show a "not implemented" message.
`!` only show a "not implemented" message.
Next steps, roughly in order:
1. Actions: checkout pin (`submodule update`), bump pin to tip, init.
2. Filter (`/`) and "problems only" (`!`) views.
2. "Problems only" (`!`) view; it can reuse the tree's filter.
+15
View File
@@ -231,3 +231,18 @@ those preferred refs, by commits on either side: scanning every branch's
merge base would be too slow. Cost: a pin on main far behind its tip shows a
younger branch that contains it if that branch's tip is closer, so "n behind
main" is no longer visible there; `via <rule>` still tells which ref it is.
## Filter: matches with ancestors, lives as long as the input
`/` filters by case-insensitive substring of the submodule path. Matches
keep their ancestors, shown expanded, because a nested submodule alone
would lose its context; the user's fold state is not touched, so closing the
filter restores it (plus the selection's ancestors, so the selection stays
visible). The filter exists exactly while the input line is open, and `esc`
closes both, so there is no hidden filter that makes the list look
incomplete. While typing, `j`/`k`/`q` are text; `enter` returns the keys to
the tree and keeps the filter, so a filtered list can be navigated with the
usual keys. A filtered-out selection moves to the next visible submodule
(previous if none follows) instead of the first, so the cursor stays near
where it was. This adds the `bubbles/textinput` dependency (and its
`atotto/clipboard` import).
+6 -1
View File
@@ -103,13 +103,18 @@ commit hashes. It doesn't answer the questions users actually have:
- **F53** `f` fetches again; `r` reloads everything (e.g. after editing
files, which is not watched).
- **F54** `/` filters the tree by name/path (case-insensitive substring).
A text input above the footer takes the term; `esc` closes it and removes
the filter. A selection that is filtered out moves to the next visible
submodule (none if nothing matches); closing the filter keeps the
selection. See [Filter](ux-guide.md#filter).
### Actions (planned, not implemented)
| Key | Action |
|---|---|
| `u` | Check out the pinned commit (`git submodule update`) |
| `b` | Bump the pin to the tip of its ref |
| `/` | Filter by name/path |
| `!` | Show only submodules with issues |
## Usage
+24 -2
View File
@@ -34,6 +34,7 @@
| Header | 1 line |
| Tree | Remaining width |
| Detail pane | 40% of the width, at least 36 columns; hidden below 90 columns |
| Filter line | 1 line above the footer, only while filtering |
| Footer | 1 line (more with full help) |
### Key bar and palette
@@ -207,7 +208,8 @@ colors in components.
| `f` | Fetch all repositories (does nothing while a fetch runs) |
| `r` | Reload everything (e.g. after editing files) |
| `c` | Copy the absolute directory of the selected submodule to the clipboard (OSC 52, so it also works over SSH; needs terminal support, in tmux `set -g set-clipboard on`). The footer confirms with `copied <path>`. Without a local provider (mock) the path relative to the superproject is copied |
| `u`, `b`, `/`, `!` | Planned actions (see requirements) |
| `/` | Open the filter, see [Filter](#filter) |
| `u`, `b`, `!` | Planned actions (see requirements) |
"First child" always means the first child **as displayed** (children are
grouped by directory).
@@ -216,6 +218,26 @@ Collapsing an ancestor of the selection moves the selection to that ancestor.
The view scrolls so the selection is always visible; when the first submodule
is selected, headers above it are shown too.
### Filter
`/` shows a text input above the footer (`/ term`). While it is open the
tree shows only the submodules whose path (which includes the name)
contains the term, ignoring case; their ancestors stay as context and are
shown expanded, whatever the user folded. Directory headers without a
match vanish. If nothing matches the tree says `no submodule matches`.
- **Selection.** If the selected submodule is filtered out, the next
visible one (in display order) is selected, else the previous one; if none
is left, nothing is selected (the detail pane is empty).
- **Typing.** The input has the focus: letters edit the term, so `j`, `q`
etc. are text. `↑` `↓` `pgup` `pgdn` still move the selection. `enter`
hands the keys back to the tree (the filter stays, all keys work again);
`/` edits the term again.
- **`esc`** closes the input and removes the filter, in both modes. The
selection stays; its ancestors are expanded so it is still visible.
- The filter line shows the keys that apply on its right.
- The header counts are not filtered.
### Key reference
Every key in the UI, where it is shown, and whether it works. Update this
@@ -237,7 +259,7 @@ where the palette can't be shown, see [Key bar and palette](#key-bar-and-palette
| `b` | Bump pin to tip | Submodule | Help | no (flashes "not implemented") |
| `f` | Fetch all repositories | Global | Help | yes |
| `r` | Reload everything | Global | Help | yes |
| `/` | Filter | Global | Help | no (flashes "not implemented") |
| `/` | Filter | Global | Help | yes |
| `!` | Problems only | Global | Help | no (flashes "not implemented") |
| `?` | Toggle full help | Global | Bar, Help | yes |
| `q`, `ctrl+c` | Quit | Global | Bar, Help | yes |