Better Editor for Streamd Tag Maintenance #327

Closed
opened 2026-10-25 10:01:57 +01:00 by kfickel · 2 comments
Owner

Adding new tags in the streamd tag maintenance TUI is annoying.

  • First, if the text becomes to long, the editor runs out. Add proper line breaks.
  • There is no auto-completion for tags in the description, which would be really useful.
  • Add a search to the add @X to ... (extensible dimension), so that I don't have to switch to the arrow keys here.
Adding new tags in the streamd tag maintenance TUI is annoying. * First, if the text becomes to long, the editor runs out. Add proper line breaks. * There is no auto-completion for tags in the description, which would be really useful. * Add a search to the add @X to ... (extensible dimension), so that I don't have to switch to the arrow keys here.
Collaborator

Plan

Scope: the tag maintenance view (streamd config tags maintenance -i, tab config, R33), its dialogs Add to dimension and description. All changes are in the CLI (crates/streamd-cli/src/maintenance/interactive/ and a shared input in commands/tui/); the core only provides what already exists (tag_summaries, mention_at).

Decisions (clarified)

  • Description field: soft wrap and a real cursor (not just append/backspace at the end).
  • Completion offers the configured tags only (every tag and alias of .streamd.toml, as tag_summaries lists them, with their one line) — not the other unplaced tags.
  • While a mention is typed, a list of suggestions shows under the field: ↑/↓ choose, Tab inserts; Enter always adds the tag.
  • The dimension picker is type-to-narrow like Merge: letters filter at once (smartcase), ↑/↓/Home/End move, Enter picks; j/k/q become text there, Esc goes back.

1. A shared text input: commands/tui/input.rs

  • TextInput { text: String, cursor: usize } (cursor a byte offset, always on a char boundary). key(&mut self, KeyEvent) -> bool (changed?): printable chars insert at the cursor (not with Ctrl/Alt), Backspace/Delete, ←/→, Home/End, Ctrl-←/Ctrl-→ by word, Ctrl-U/Ctrl-W (clear to start / delete word back). insert, replace(range, with) (moves the cursor to the end of what was put in).
  • wrapped(width) -> (Vec<String>, (row, col)): the text broken at spaces into rows of at most width columns (by unicode-width, as columns of tui::widgets), a word longer than a row broken inside it; plus where the cursor is. Pure, unit-tested (empty, exact fit, long word, wide chars, cursor at the end of a full row → next row).
  • If it fits without contortion, let the range overlay of the activity view (commands/activity/interactive/range.rs, which has its own char-boundary editing and wrapped) use it too; otherwise leave it and note it in AGENTS.md as a follow-up. Not a requirement of this issue.
  • type_into of maintenance/interactive/model.rs stays for the Merge filter (or uses TextInput as well — either is fine, but one way of typing per view is preferred).

2. Description dialog (model)

  • Dialog::Description { dimension: usize, input: TextInput, suggestion: usize }.
  • Loaded / Model get tags: Vec<TagSummary> → name it mentionable (the tag_summaries(&config) of the view's own configuration, filled in terminal.rs next to dimensions and targets; re-export through the CLI's config/project.rs / document as needed).
  • Model::suggestions(&self) -> Vec<usize>: if mention_at(input.text, input.cursor) (core, config/project/index.rs) gives a mention, the typed part after the @ up to the cursor is the prefix; candidates are the mentionable whose name starts with it (ignoring case unless the prefix has an uppercase char), then those that only contain it, without duplicates, without the name exactly as typed if that is the only one; none if there is no mention at the cursor.
  • Keys: ↑/↓ move suggestion while suggestions show (else they do nothing); Tab replaces the mention range with @<Name> and, if the next char is not a space, adds nothing else (the user types on); Enter → Effect::Add with the trimmed text as now; Esc → back to the dimension dialog (keeping its filter); everything else goes to TextInput::key, and resets suggestion to 0 when the text changed.
  • Note: Tab is a key of R30 (switch tab), but the dialog is modal so it never reaches the front-end — check that in a test.

3. Description dialog (view)

  • The popup takes a fixed width: min(area.width - 4, 72) (at least 30), and the height its wrapped rows need (+ suggestion rows), capped by the area; if the text is taller than that, scroll so the cursor row is visible.
  • The rows of the text: first row prefixed by the dim description: label, continuation rows indented to the same column; tags in the text coloured as everywhere (tui::panes::styled / style::tag_parts on the ranges of description_mentions). The cursor is a reversed cell at its position (as typed does at the end now); additionally frame.set_cursor_position is not needed.
  • Under a dim rule: at most 6 suggestions, each @Name in its colour plus its what in dark gray, cut to the width (styled_fit), the selected one with ▶ and the bar (choice), scrolled with panes::scrolled.
  • Key bar: type what the tag stands for ↑/↓ choose tab complete enter add esc back (the ↑/↓ … tab part only while suggestions show).

4. Dimension dialog with search (model + view)

  • Dialog::Dimension { filter: String, selected: usize }; Model::dimension_candidates(filter) like merge_candidates, matching name and key with search::matches (smartcase).
  • Keys as in Merge: printable chars / Backspace edit the filter and select the first match, ↑/↓/Home/End move, Enter on a candidate opens the description (with the position in dimensions), on none does nothing, Esc returns to the menu. The description dialog keeps the filter so Esc there returns to the narrowed list.
  • View: the typed("/", filter) row on top as in Merge, no dimension has this in its name or key when empty, at most 10 rows with scrolling. Key bar: type to narrow ↑/↓ move enter choose esc back.

5. Tests

  • tui/input.rs: editing at a cursor in the middle, unicode (ä, wide CJK, emoji) boundaries, word moves, wrapping and cursor position.
  • model.rs: dimension filter narrows by name and key, smartcase, Enter/Esc paths; j/q are text there; description cursor editing; suggestions appear at @Ta|, not after a space or in an e-mail address; Tab inserts @Task in the middle of text; Enter while suggestions show still adds the tag; Esc keeps the dimension filter; Tab is not forwarded (modal).
  • view.rs (TestBackend): a long description wraps over several rows inside the popup at 80 and at 40 columns, the cursor cell in the middle of the text, the suggestion list with colours, the filtered dimension list and its empty message.
  • terminal.rs: the loaded view has the mentionable tags of the configuration; adding a tag with a description that was completed writes description = "… @Task …".

6. Documentation

  • REQUIREMENTS.md R33, Add to dimension: the typed filter of the dimensions (by name or key, smartcase, ↑/↓/Home/End, at most 10 rows, Esc to the menu), the description prompt (wrapping, cursor keys, the completion of configured tags with ↑/↓ and Tab, Enter always adds, Esc back to the narrowed dimensions).
  • README.md: the maintenance section's keys.
  • AGENTS.md: commands/tui/input.rs in the list of commands/tui/ (and in the description of maintenance/interactive/model.rs: the Dialogs now carry a filter and a TextInput).
  • Finish with nix flake check.
## Plan Scope: the tag maintenance view (`streamd config tags maintenance -i`, tab `config`, R33), its dialogs **Add to dimension** and **description**. All changes are in the CLI (`crates/streamd-cli/src/maintenance/interactive/` and a shared input in `commands/tui/`); the core only provides what already exists (`tag_summaries`, `mention_at`). ### Decisions (clarified) - Description field: **soft wrap and a real cursor** (not just append/backspace at the end). - Completion offers the **configured tags only** (every tag and alias of `.streamd.toml`, as `tag_summaries` lists them, with their one line) — not the other unplaced tags. - While a mention is typed, a list of suggestions shows under the field: **↑/↓ choose, `Tab` inserts**; `Enter` always adds the tag. - The dimension picker is **type-to-narrow like Merge**: letters filter at once (smartcase), ↑/↓/Home/End move, `Enter` picks; `j`/`k`/`q` become text there, `Esc` goes back. ### 1. A shared text input: `commands/tui/input.rs` - `TextInput { text: String, cursor: usize }` (cursor a byte offset, always on a char boundary). `key(&mut self, KeyEvent) -> bool` (changed?): printable chars insert at the cursor (not with Ctrl/Alt), `Backspace`/`Delete`, `←`/`→`, `Home`/`End`, `Ctrl-←`/`Ctrl-→` by word, `Ctrl-U`/`Ctrl-W` (clear to start / delete word back). `insert`, `replace(range, with)` (moves the cursor to the end of what was put in). - `wrapped(width) -> (Vec<String>, (row, col))`: the text broken at spaces into rows of at most `width` columns (by `unicode-width`, as `columns` of `tui::widgets`), a word longer than a row broken inside it; plus where the cursor is. Pure, unit-tested (empty, exact fit, long word, wide chars, cursor at the end of a full row → next row). - If it fits without contortion, let the range overlay of the activity view (`commands/activity/interactive/range.rs`, which has its own char-boundary editing and `wrapped`) use it too; otherwise leave it and note it in AGENTS.md as a follow-up. Not a requirement of this issue. - `type_into` of `maintenance/interactive/model.rs` stays for the Merge filter (or uses `TextInput` as well — either is fine, but one way of typing per view is preferred). ### 2. Description dialog (model) - `Dialog::Description { dimension: usize, input: TextInput, suggestion: usize }`. - `Loaded` / `Model` get `tags: Vec<TagSummary>` → name it `mentionable` (the `tag_summaries(&config)` of the view's own configuration, filled in `terminal.rs` next to `dimensions` and `targets`; re-export through the CLI's `config/project.rs` / `document` as needed). - `Model::suggestions(&self) -> Vec<usize>`: if `mention_at(input.text, input.cursor)` (core, `config/project/index.rs`) gives a mention, the typed part after the `@` up to the cursor is the prefix; candidates are the `mentionable` whose name starts with it (ignoring case unless the prefix has an uppercase char), then those that only contain it, without duplicates, without the name exactly as typed if that is the only one; none if there is no mention at the cursor. - Keys: `↑`/`↓` move `suggestion` while suggestions show (else they do nothing); `Tab` replaces the mention range with `@<Name>` and, if the next char is not a space, adds nothing else (the user types on); `Enter` → `Effect::Add` with the trimmed text as now; `Esc` → back to the dimension dialog (keeping its filter); everything else goes to `TextInput::key`, and resets `suggestion` to 0 when the text changed. - Note: `Tab` is a key of R30 (switch tab), but the dialog is modal so it never reaches the front-end — check that in a test. ### 3. Description dialog (view) - The popup takes a fixed width: `min(area.width - 4, 72)` (at least 30), and the height its wrapped rows need (+ suggestion rows), capped by the area; if the text is taller than that, scroll so the cursor row is visible. - The rows of the text: first row prefixed by the dim `description: ` label, continuation rows indented to the same column; tags in the text coloured as everywhere (`tui::panes::styled` / `style::tag_parts` on the ranges of `description_mentions`). The cursor is a reversed cell at its position (as `typed` does at the end now); additionally `frame.set_cursor_position` is not needed. - Under a dim rule: at most 6 suggestions, each `@Name` in its colour plus its `what` in dark gray, cut to the width (`styled_fit`), the selected one with `▶` and the bar (`choice`), scrolled with `panes::scrolled`. - Key bar: ` type what the tag stands for ↑/↓ choose tab complete enter add esc back` (the `↑/↓ … tab` part only while suggestions show). ### 4. Dimension dialog with search (model + view) - `Dialog::Dimension { filter: String, selected: usize }`; `Model::dimension_candidates(filter)` like `merge_candidates`, matching name **and key** with `search::matches` (smartcase). - Keys as in Merge: printable chars / Backspace edit the filter and select the first match, ↑/↓/Home/End move, `Enter` on a candidate opens the description (with the position in `dimensions`), on none does nothing, `Esc` returns to the menu. The description dialog keeps the filter so `Esc` there returns to the narrowed list. - View: the `typed("/", filter)` row on top as in Merge, `no dimension has this in its name or key` when empty, at most 10 rows with scrolling. Key bar: ` type to narrow ↑/↓ move enter choose esc back`. ### 5. Tests - `tui/input.rs`: editing at a cursor in the middle, unicode (`ä`, wide CJK, emoji) boundaries, word moves, wrapping and cursor position. - `model.rs`: dimension filter narrows by name and key, smartcase, Enter/Esc paths; `j`/`q` are text there; description cursor editing; suggestions appear at `@Ta|`, not after a space or in an e-mail address; `Tab` inserts `@Task` in the middle of text; `Enter` while suggestions show still adds the tag; `Esc` keeps the dimension filter; `Tab` is not forwarded (modal). - `view.rs` (TestBackend): a long description wraps over several rows inside the popup at 80 and at 40 columns, the cursor cell in the middle of the text, the suggestion list with colours, the filtered dimension list and its empty message. - `terminal.rs`: the loaded view has the `mentionable` tags of the configuration; adding a tag with a description that was completed writes `description = "… @Task …"`. ### 6. Documentation - `REQUIREMENTS.md` R33, *Add to dimension*: the typed filter of the dimensions (by name or key, smartcase, ↑/↓/Home/End, at most 10 rows, `Esc` to the menu), the description prompt (wrapping, cursor keys, the completion of configured tags with ↑/↓ and `Tab`, `Enter` always adds, `Esc` back to the narrowed dimensions). - `README.md`: the maintenance section's keys. - `AGENTS.md`: `commands/tui/input.rs` in the list of `commands/tui/` (and in the description of `maintenance/interactive/model.rs`: the `Dialog`s now carry a filter and a `TextInput`). - Finish with `nix flake check`.
Collaborator

Implemented in #330: #330

Elapsed time: approximately 17 minutes (2026-10-25, 09:51–10:08 UTC), including tests, signed commits, push and PR creation.

Token usage: unavailable. This runtime exposes no per-task token counter; the usage query returned no token report, so I cannot provide a measured count.

The description now soft-wraps, scrolls to its editable Unicode-safe cursor and completes configured tags/aliases at the cursor with ↑/↓ and Tab. Enter always adds the tag. Dimensions can be narrowed by name or key with smartcase, and Esc from the description preserves the narrowed list.

Findings:

  • Existing core functions (mention_at, description_mentions, tag_summaries) supply the necessary tag information; all implementation changes stay in the CLI, without new dependencies.
  • A name can appear both as a configured alias and in a rule. Completion explicitly deduplicates names; a regression test first reproduced the duplicate and now passes.
  • The optional activity range input migration remains a follow-up in AGENTS.md: its current field scrolls horizontally and its preview normalizes whitespace.
  • The pre-existing colour test requires clearing this environment's NO_COLOR; commit hooks passed with it unset and a colour-capable TERM.

Validation: all 1,748 Rust tests, strict Clippy, formatting and commit hooks passed. The final nix flake check passed, including 102 Kotlin checks. Tests cover cursor editing, Unicode wrapping, smartcase and suggestion order, modal Tab routing, rendering at 80/40 columns and tiny sizes, scrolling, and writing a completed description to .streamd.toml.

Three GPG-signed conventional commits were pushed; README.md, REQUIREMENTS.md R33 and AGENTS.md are updated.

Implemented in #330: https://git.konstantinfickel.de/kfickel/streamd/pulls/330 Elapsed time: approximately 17 minutes (2026-10-25, 09:51–10:08 UTC), including tests, signed commits, push and PR creation. Token usage: unavailable. This runtime exposes no per-task token counter; the usage query returned no token report, so I cannot provide a measured count. The description now soft-wraps, scrolls to its editable Unicode-safe cursor and completes configured tags/aliases at the cursor with ↑/↓ and Tab. Enter always adds the tag. Dimensions can be narrowed by name or key with smartcase, and Esc from the description preserves the narrowed list. Findings: - Existing core functions (`mention_at`, `description_mentions`, `tag_summaries`) supply the necessary tag information; all implementation changes stay in the CLI, without new dependencies. - A name can appear both as a configured alias and in a rule. Completion explicitly deduplicates names; a regression test first reproduced the duplicate and now passes. - The optional activity range input migration remains a follow-up in AGENTS.md: its current field scrolls horizontally and its preview normalizes whitespace. - The pre-existing colour test requires clearing this environment's `NO_COLOR`; commit hooks passed with it unset and a colour-capable TERM. Validation: all 1,748 Rust tests, strict Clippy, formatting and commit hooks passed. The final `nix flake check` passed, including 102 Kotlin checks. Tests cover cursor editing, Unicode wrapping, smartcase and suggestion order, modal Tab routing, rendering at 80/40 columns and tiny sizes, scrolling, and writing a completed description to `.streamd.toml`. Three GPG-signed conventional commits were pushed; README.md, REQUIREMENTS.md R33 and AGENTS.md are updated.
Sign in to join this conversation.
No labels
android
cli
epic
planned
tui
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd#327
No description provided.