Activity: the TUI dashboard (streamd <key> -i) #281
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #277. Needs #279 (and #280 for
-iand its flags).Goal
The view of an activity command in the front-end (
streamd <key> -i, a tab ofstreamd -i, R30): a dashboard with drill-down that follows the app. Tiles sit on top, then the chart the range calls for, then the Days table, then the latest shards. The whole body is one page that scrolls.Layout ("boxed tiles", chosen in #277)
Calendar (2–42 days):
Grid (more than 42 days, All): one strip per year, newest on top, 53 columns, 7 rows, with the weekday labels on the left. Nothing scrolls sideways. If the terminal is too short, the page scrolls vertically (J/K) and the strip with the cursor is kept in sight. Shortened:
Day (1 day): the shard list only, with no axis. The tiles show the date and one tile per series (
Run 1 · at 06:45). j/k select a shard andeedits it.All shards (
a): the same list over the whole range, grouped under day headers, newest first, paged from the core as it scrolls.1…9filter it to one series and0shows all again; the header shows the counts (All 15 · Run 7 · Climbing 8).eedits, ⌫ goes back.On a narrow terminal (< 60 columns) the three tiles collapse into the one line
15 shards · 11 of 17 days active · 6.2 per week.Behaviour
this monthby default).[/]step it (#279step, does nothing whencan_stepis false; the status line says why).topens the range overlay (#282; until then,tsaysnot available yet). The header shows‹ label ›, and an arrow is dim when that way is closed.Fri 16 Oct · Run 1 · Climbing 1 · mixed,none,future,not tracked).tor[/]starts a new root and clears this back stack.xtoggles exclusive/inclusive for the session (the Days table switches toRun only/Climbing only/Mixed/Nonewith the wording of #280). The header showsincl. mixed/excl. mixed.e(day view, all shards) opens the selected shard in the editor (R20a) and reloads afterwards.rreloads. J/K or PgUp/PgDn scroll the page.?,q,Esc,Tab,Shift-Taband:are R30's. The tab summary isN shards · A of T days.commands/tui/style.rsplus the series colours (the one place literal colours enter is the configuration).Structure
As every view:
commands/activity/interactive/model.rs(pure: range, back stack, shape, cursor, mode, scroll; a key gives anEffect),view.rs(drawn withTestBackendin tests, built ontui::widgetsandtui::panes), andterminal.rs(ActivityView, which reloads through the stream). One arm infrontend/views.rs.Tests
The model on the sample of #279 (cursor bounds, drill and back, a new root clearing the stack, step limits, the
xtoggle),TestBackendsnapshots of the calendar, grid, day and all-shards views at 80×40 and at a narrow width.Docs
R36 (the view, like R18d), README, AGENTS.md.
activitycommand type in .streamd.toml #278Plan
Replaces the placeholder
ActivityView(crates/streamd-cli/src/commands/activity/interactive.rs) by a real view incommands/activity/interactive/(mod.rs,model.rs,view.rs,terminal.rs), as the timesheet view is built. Nothing in the core changes: everything needed is there (RangeSpec,DayRange::{step, can_step, label, spec},Shape::of,activity,Stats,PageRequest::of).Decisions (clarified)
a all N shardsbelow them.eworks only in the day view and in the all-shards list (not on a day of the calendar/grid; ⏎ drills to the day first).1…9/0only filter series in the all-shards list.gg/G(first / last day, shard) still work throughtui::Pendingwithout counts.tsaysnot available yetin the status line until #282.1. Sharing the drawing with
print.rsprint.rsalready builds lines of(String, Style)parts for the calendar, the grid, the legend, the totals, the Days table and the shard rows. Make what the view needspub(super)and reuse it instead of copying:glyph/glyph_part,series_style,legend,days_table(command, stats, exclude_mixed),bullet,tag_parts,plural,monday_of,cell,LEVELS/MIXED/NONE/FUTURE,WEEKDAYS,GRID_LABEL.calendarandgridso the view can mark the cursor: a variant (or anOption<NaiveDate>parameter) that draws the selected day reversed ([◩16]in the mockup =Modifier::REVERSED). The printed output must stay byte-identical (existing golden tests guard it).gridgets amax_weeks: Option<usize>(or width) to cut the oldest weeks of a strip; the printed command keeps all.to_ratatui(&print::Line) -> ratatui::text::Line<'static>helper inview.rs.2.
model.rs(pure, no I/O)Modelholds:key, theActivityCommand(cloned) andtoday/start(first tracked day) for stepping.root: RangeSpecandstack: Vec<Level>— aLevelis{ range: RangeSpec, cursor: NaiveDate }; the current level is the last of root+stack. ⏎ pushes, ⌫ pops;[/]and a range from #282 replace the root and clear the stack.exclude_mixed: bool(fromViewOptions.exclude_mixed, else the command's), toggled byx.mode: Mode—DashboardorAllShards { filter: Option<usize>, selected, offset }.cursor: NaiveDateon calendar/grid;selected_shard: usizein the day view.scroll: u16(page offset, J/K, PgUp/PgDn),help: bool,pending: Pending,status: Option<Status>.Activityborrows the shards):Snapshot { range: DayRange, shape: Shape, days: Vec<DayCell>, stats: Stats, matches: Vec<ShownMatch> }, whereShownMatch { day, moment, series, file: PathBuf, line, }and the titles/tags are filled lazily (see terminal). The model never reads files.Keys give an
Effect:None,Recompute(range or root changed),Edit { file, line },Reload,LoadTitles(PageRequest),Quit.Cursor rules:
h/l±1 day,j/k±7 days. Grid:h/l±7 days (a week),j/k±1 day (weekday).gg/Gfirst/last selectable day.Untracked; a move that would leave them does nothing. Future days are selectable (they are shown, context saysfuture).RangeSpec::Monthof the cursor's day; calendar →RangeSpec::Day; day view → nothing.already at the top).[/]:range.step(±1)ifcan_step(±1, today, start), else status saying why (nothing tracked before …/nothing after today); the result'sspec()becomes the new root, stack cleared. In the day view this steps the day (it is the current level, not the root — stepping a drilled level replaces that level only, keeping the stack below it).a:AllShardsover the current range;1…9filter to that series if it exists,0clears;j/k/gg/G/PgUp/PgDn move;eedits; ⌫ back to the dashboard.x,r,J/K,?as in the issue;q/Esc/Tab/:are left to the front-end (is_modaltrue only while help is open).context(): dashboard → the selected day:Fri 16 Oct · Run 1 · Climbing 1 · mixed/none/future/not tracked; day view and list →file:lineof the selected shard throughPathDisplay.summary():N shards · A of T days.3.
view.rs(drawing only,TestBackend-tested)One scrolling page built as a
Vec<Line>plus box borders, drawn into the body with the model'sscroll(clamped; the strip or row with the cursor is scrolled into sight):‹ label ›, an arrowstyle::dim()when that way is closed,incl. mixed/excl. mixed.Block::bordered) side by side — range label +N shards,Active days A of T days,Per week X shards; in the day shape the date plus one tile per series (Run 1 · at 06:45, fromSeriesDay.first). Under 60 columns: the one line15 shards · 11 of 17 days active · 6.2 per week.OCTOBER 2026), grid one strip per year newest first; legend at its bottom.days_tableofprint.rswith the hint lineMixed days count for each series · x to change(wording per #280 for both modes).a all N shards.FRIDAY, 16 OCTOBER 2026box with the shards (time, bullet, title, tags right-aligned),▶on the selected one.All 15 · Run 7 · Climbing 8(active filter highlighted), rows as in the day view.key_barper mode as in the mockups;statusoverrides it (tui::widgets::status_line); help overlay withtui::widgets::help.tui::styleand the series colours of the configuration.4.
terminal.rs(ActivityView<'a>)&ActivityCommand,&ProjectConfig,base_folder,&PathDisplay, the model and the loadedVec<LocalizedShard>(load_markdown_shards_cached, asrun.rsdoes).recompute():activity(&shards, command, &range, Utc::now(), tz)for the current level →Snapshotinto the model.run.rs::shownis moved/generalised so it can take a slice of matches (onefs::read_to_stringper file,shard_summary) and is called only for what is visible: the latest-shards rows, the shards of the day, and the page of the all-shards list (PageRequest { offset, limit: visible rows + margin }viaPageRequest::of), cached by(file, line)until the next reload. A file that cannot be read gives itsfile:lineas title and a status message, not a crash.Effect::Edit→OpenEditor::edit(file, line)then reload; errors go to the status line.reload()reads the stream again and recomputes, keeping range, stack, cursor and mode.frontend/views.rs: theActivityarm buildsActivityView::new(key, command, options.range, options.exclude_mixed, project, base_folder, paths)(returningResultlike the timesheet view). RemoveNOT_IMPLEMENTEDand its uses/tests (README/R30 text mentioning the placeholder too).5. Tests
model.rson the sample of #279/#280 (print.rstests'sport()/sample()— move them to a shared#[cfg(test)] mod fixturesincommands/activity/): cursor bounds in calendar and grid (edges of the range, untracked days skipped),gg/G, drill grid→month→day and ⌫ back with the cursor restored,[/]clearing the stack, step limits with the status message, stepping a drilled day keeping the stack,xtoggle, digit filters in the list,egivingEffect::Editwith file and line, no effect of digits outside the list.view.rsTestBackendsnapshots (asserted as strings, like the existing views) of calendar, grid, day and all-shards at 80×40 and at a narrow width (50 columns: tiles collapsed into one line, grid strips cut to the newest weeks with the cursor visible); the latest-shards box filling the height (and the minimum of 3 on a short terminal); dim arrows when a step is closed.terminal.rs: anActivityViewon a temporary stream folder with aFakeEditor:eopens the rightfile:lineand reloads.print.rsgolden tests stay unchanged (the shared helpers must not alter the printed output).6. Docs
REQUIREMENTS.mdR36: a sub-requirement for the view (like R18d): layout per shape, keys, cursor rules, drill-down/back stack,x,e, narrow behaviour; R30: drop "for now a placeholder".README.md: the interactive section of activity commands with the keys.AGENTS.md: replace theinteractive.rsplaceholder description incommands/activity/withinteractive/(model.rs,view.rs,terminal.rs) and the helpers shared withprint.rs.Finish with
cargo test,cargo clippy -D warnings,nix flake check.Implemented in #308 (three GPG-signed commits); the PR has since been merged by kfickel.
Elapsed time: approximately 16 minutes, including verification and publishing.
Token usage: approximately 20,000 generated tokens (estimate only). Exact input/output token telemetry is not exposed by this runtime, so an exact total is unavailable.
The dashboard now supports calendar/grid/day layouts, drill-down and restored parent cursors, bounded range stepping, session mixed-day toggles, a filtered all-shards list, lazy visible title loading, editor/reload effects and narrow terminals. Shared chart/title helpers preserve the existing printed golden output. README, AGENTS.md and R36a describe the new behavior.
Findings: stepping a drilled day preserves its parent stack, while other range changes start a new root; narrow grids need a cursor-aware window rather than unconditional left cropping. Missing source files now produce source labels and status errors. The inherited NO_COLOR environment suppressed escapes in an existing colour golden test; local tests and commit hooks passed with that variable unset. The range picker remains #282, with the agreed
not available yetmessage ont.Validation: all 1,700 workspace tests pass; 32 activity tests include eight TestBackend references at 80/50 columns and editor/reload/error handling.
cargo clippy --all-targets -- -D warningspasses, as do all seven x86_64-linux checks ofnix flake check. Working tree is clean.