Activity: the TUI dashboard (streamd <key> -i) #281

Closed
opened 2026-10-17 08:23:51 +02:00 by agentic-developer · 2 comments
Collaborator

Part of #277. Needs #279 (and #280 for -i and its flags).

Goal

The view of an activity command in the front-end (streamd <key> -i, a tab of streamd -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):

 Sport  streamd sport · calendar               ‹ Oct 2026 ›
┌───────────────┐┌─────────────────┐┌──────────────┐
│ Oct 2026      ││ Active days     ││ Per week     │
│ 15 shards     ││ 11 of 17 days   ││ 6.2 shards   │
└───────────────┘└─────────────────┘└──────────────┘
┌ OCTOBER 2026 ─────────────────────────────────────┐
│   Mo  Tu  We  Th  Fr  Sa  Su                      │
│               ░ 1 · 2 ▓ 3 ◩ 4                     │
│   · 5 ░ 6 · 7 ░ 8 ▒ 9 ░10 ▒11                     │
│   ·12 ░13 ·14 ·15[◩16]░17 ┄18                     │
│   ┄19 ┄20 ┄21 ┄22 ┄23 ┄24 ┄25                     │
│   ┄26 ┄27 ┄28 ┄29 ┄30 ┄31                         │
│   ░▒▓█ Run  ░▒▓█ Climbing  ◩ Mixed  · None        │
└───────────────────────────────────────────────────┘
┌ DAYS  of 17 tracked ──────────────────────────────┐
│ Mixed days count for each series · x to change    │
│ Run       incl. 2 mixed           7    41%        │
│ Climbing  incl. 2 mixed           6    35%        │
│ Mixed     also counted above      2    12%        │
│ None      no series matched       6    35%        │
│ Shards    Run 7 · Climbing 8                      │
└───────────────────────────────────────────────────┘
┌ LATEST SHARDS  15 total ──────────────────────────┐
│ Sat 17 Oct 06:45  Long run · 9 km           @Run  │
│ Fri 16 Oct 18:30  Fingerboard session  @Climbing  │
│ Fri 16 Oct 06:45  Long run · 17 km          @Run  │
│ Tue 13 Oct 18:30  Lead climbing · 105 min @Climb… │
│ Sun 11 Oct 06:45  Tempo run · 16 km         @Run  │
│ a  all 15 shards                                  │
└───────────────────────────────────────────────────┘
 Fri 16 Oct · Run 1 · Climbing 1 · mixed
 hjkl day  ⏎ drill  ⌫ up  [/] step  t range  x mixed  a all

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:

 Sport  streamd sport · grid                   ‹ All time ›
┌──────────────┐┌─────────────────┐┌──────────────┐
│ All time     ││ Active days     ││ Per week     │
│ 606 shards   ││ 388 of 650 days ││ 6.5 shards   │
└──────────────┘└─────────────────┘└──────────────┘
┌ 2026 ──────────────────────────────────────────────────────────
│     Jan    Feb    Mar    Apr    May   Jun    Jul   Aug  Sep  Oct
│ Mo  ·░·▒░·▓·░·▒░·░·▒·░·▒░·░·▒·░··▒░·░·▒·░·▒·░·▒░┄┄┄┄┄┄┄┄┄
│     ░·▒·░◩·░·▒░·░·▒·░·▒░·░·▒·░·░·▒░·░·▒·░·░·▒░·┄┄┄┄┄┄┄┄┄
│ We  …
│ Su  ░·▒░·░·▒·░·▒░·░·▒·░·▒░·░·▒·░·▒░·░·▒·░·▒░·░[░]┄┄┄┄┄┄┄┄
├ 2025 ──────────────────────────────────────────────────────────
│     Jan    Feb   …                                         Dec
│ Mo    ·░·▒░·▓·░·▒░·░·▒·░·▒░·░·▒·░··▒░·░·▒·░·▒·░·▒░·░·▒
│ …
└────────────────────────────────────────────────────────────────
 (Days and Latest shards as above)

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 and e edits it.

 Sport  streamd sport · day                 ‹ 16 Oct 2026 ›
┌───────────────┐┌──────────────┐┌──────────────┐
│ 16 Oct 2026   ││ Run          ││ Climbing     │
│ 2 shards      ││ 1 · at 06:45 ││ 1 · at 18:30 │
└───────────────┘└──────────────┘└──────────────┘
┌ FRIDAY, 16 OCTOBER 2026 ──────────────────────────┐
│▶18:30  ● Fingerboard session · 120 min @Climbing  │
│ 06:45  ● Long run · 17 km                   @Run  │
└───────────────────────────────────────────────────┘
 journal/20261016-183000.md:1
 j/k shard  e edit  ⌫ up  [/] day  t range  x mixed

All shards (a): the same list over the whole range, grouped under day headers, newest first, paged from the core as it scrolls. 1…9 filter it to one series and 0 shows all again; the header shows the counts (All 15 · Run 7 · Climbing 8). e edits, ⌫ 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

  • Range: starts at the range of the command line (this month by default). [ / ] step it (#279 step, does nothing when can_step is false; the status line says why). t opens the range overlay (#282; until then, t says not available yet). The header shows ‹ label ›, and an arrow is dim when that way is closed.
  • Cursor (a day, drawn reversed): calendar h/l by day, j/k by week; grid h/l by week, j/k by weekday. It stays inside the range and on tracked days. The context line describes the selected day (Fri 16 Oct · Run 1 · Climbing 1 · mixed, none, future, not tracked).
  • Drill-down: ⏎ on a day in the grid opens its month, and ⏎ in the calendar opens the day. ⌫ goes one level up (day → month → the range before). A new range from t or [/] starts a new root and clears this back stack.
  • x toggles exclusive/inclusive for the session (the Days table switches to Run only / Climbing only / Mixed / None with the wording of #280). The header shows incl. mixed / excl. mixed.
  • e (day view, all shards) opens the selected shard in the editor (R20a) and reloads afterwards. r reloads. J/K or PgUp/PgDn scroll the page. ?, q, Esc, Tab, Shift-Tab and : are R30's. The tab summary is N shards · A of T days.
  • Colours, glyphs, today, future and untracked as in #280, through the palette of commands/tui/style.rs plus 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 an Effect), view.rs (drawn with TestBackend in tests, built on tui::widgets and tui::panes), and terminal.rs (ActivityView, which reloads through the stream). One arm in frontend/views.rs.

Tests

The model on the sample of #279 (cursor bounds, drill and back, a new root clearing the stack, step limits, the x toggle), TestBackend snapshots 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.

Part of #277. Needs #279 (and #280 for `-i` and its flags). ## Goal The view of an activity command in the front-end (`streamd <key> -i`, a tab of `streamd -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): ``` Sport streamd sport · calendar ‹ Oct 2026 › ┌───────────────┐┌─────────────────┐┌──────────────┐ │ Oct 2026 ││ Active days ││ Per week │ │ 15 shards ││ 11 of 17 days ││ 6.2 shards │ └───────────────┘└─────────────────┘└──────────────┘ ┌ OCTOBER 2026 ─────────────────────────────────────┐ │ Mo Tu We Th Fr Sa Su │ │ ░ 1 · 2 ▓ 3 ◩ 4 │ │ · 5 ░ 6 · 7 ░ 8 ▒ 9 ░10 ▒11 │ │ ·12 ░13 ·14 ·15[◩16]░17 ┄18 │ │ ┄19 ┄20 ┄21 ┄22 ┄23 ┄24 ┄25 │ │ ┄26 ┄27 ┄28 ┄29 ┄30 ┄31 │ │ ░▒▓█ Run ░▒▓█ Climbing ◩ Mixed · None │ └───────────────────────────────────────────────────┘ ┌ DAYS of 17 tracked ──────────────────────────────┐ │ Mixed days count for each series · x to change │ │ Run incl. 2 mixed 7 41% │ │ Climbing incl. 2 mixed 6 35% │ │ Mixed also counted above 2 12% │ │ None no series matched 6 35% │ │ Shards Run 7 · Climbing 8 │ └───────────────────────────────────────────────────┘ ┌ LATEST SHARDS 15 total ──────────────────────────┐ │ Sat 17 Oct 06:45 Long run · 9 km @Run │ │ Fri 16 Oct 18:30 Fingerboard session @Climbing │ │ Fri 16 Oct 06:45 Long run · 17 km @Run │ │ Tue 13 Oct 18:30 Lead climbing · 105 min @Climb… │ │ Sun 11 Oct 06:45 Tempo run · 16 km @Run │ │ a all 15 shards │ └───────────────────────────────────────────────────┘ Fri 16 Oct · Run 1 · Climbing 1 · mixed hjkl day ⏎ drill ⌫ up [/] step t range x mixed a all ``` **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: ``` Sport streamd sport · grid ‹ All time › ┌──────────────┐┌─────────────────┐┌──────────────┐ │ All time ││ Active days ││ Per week │ │ 606 shards ││ 388 of 650 days ││ 6.5 shards │ └──────────────┘└─────────────────┘└──────────────┘ ┌ 2026 ────────────────────────────────────────────────────────── │ Jan Feb Mar Apr May Jun Jul Aug Sep Oct │ Mo ·░·▒░·▓·░·▒░·░·▒·░·▒░·░·▒·░··▒░·░·▒·░·▒·░·▒░┄┄┄┄┄┄┄┄┄ │ ░·▒·░◩·░·▒░·░·▒·░·▒░·░·▒·░·░·▒░·░·▒·░·░·▒░·┄┄┄┄┄┄┄┄┄ │ We … │ Su ░·▒░·░·▒·░·▒░·░·▒·░·▒░·░·▒·░·▒░·░·▒·░·▒░·░[░]┄┄┄┄┄┄┄┄ ├ 2025 ────────────────────────────────────────────────────────── │ Jan Feb … Dec │ Mo ·░·▒░·▓·░·▒░·░·▒·░·▒░·░·▒·░··▒░·░·▒·░·▒·░·▒░·░·▒ │ … └──────────────────────────────────────────────────────────────── (Days and Latest shards as above) ``` **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 and `e` edits it. ``` Sport streamd sport · day ‹ 16 Oct 2026 › ┌───────────────┐┌──────────────┐┌──────────────┐ │ 16 Oct 2026 ││ Run ││ Climbing │ │ 2 shards ││ 1 · at 06:45 ││ 1 · at 18:30 │ └───────────────┘└──────────────┘└──────────────┘ ┌ FRIDAY, 16 OCTOBER 2026 ──────────────────────────┐ │▶18:30 ● Fingerboard session · 120 min @Climbing │ │ 06:45 ● Long run · 17 km @Run │ └───────────────────────────────────────────────────┘ journal/20261016-183000.md:1 j/k shard e edit ⌫ up [/] day t range x mixed ``` **All shards** (`a`): the same list over the whole range, grouped under day headers, newest first, paged from the core as it scrolls. `1`…`9` filter it to one series and `0` shows all again; the header shows the counts (`All 15 · Run 7 · Climbing 8`). `e` edits, ⌫ 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 - **Range**: starts at the range of the command line (`this month` by default). `[` / `]` step it (#279 `step`, does nothing when `can_step` is false; the status line says why). `t` opens the range overlay (#282; until then, `t` says `not available yet`). The header shows `‹ label ›`, and an arrow is dim when that way is closed. - **Cursor** (a day, drawn reversed): calendar h/l by day, j/k by week; grid h/l by week, j/k by weekday. It stays inside the range and on tracked days. The context line describes the selected day (`Fri 16 Oct · Run 1 · Climbing 1 · mixed`, `none`, `future`, `not tracked`). - **Drill-down**: ⏎ on a day in the grid opens its month, and ⏎ in the calendar opens the day. ⌫ goes one level up (day → month → the range before). A new range from `t` or `[`/`]` starts a new root and clears this back stack. - `x` toggles exclusive/inclusive for the session (the Days table switches to `Run only` / `Climbing only` / `Mixed` / `None` with the wording of #280). The header shows `incl. mixed` / `excl. mixed`. - `e` (day view, all shards) opens the selected shard in the editor (R20a) and reloads afterwards. `r` reloads. J/K or PgUp/PgDn scroll the page. `?`, `q`, `Esc`, `Tab`, `Shift-Tab` and `:` are R30's. The tab summary is `N shards · A of T days`. - Colours, glyphs, today, future and untracked as in #280, through the palette of `commands/tui/style.rs` plus 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 an `Effect`), `view.rs` (drawn with `TestBackend` in tests, built on `tui::widgets` and `tui::panes`), and `terminal.rs` (`ActivityView`, which reloads through the stream). One arm in `frontend/views.rs`. ## Tests The model on the sample of #279 (cursor bounds, drill and back, a new root clearing the stack, step limits, the `x` toggle), `TestBackend` snapshots 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.
Author
Collaborator

Plan

Replaces the placeholder ActivityView (crates/streamd-cli/src/commands/activity/interactive.rs) by a real view in commands/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)

  • Narrow grid: below the width a strip needs (~62 columns), each strip drops its oldest weeks from the left, so the newest weeks and the cursor stay in sight. Nothing scrolls sideways.
  • Latest shards under calendar/grid fill the height left on the page after the tiles, the chart and the Days table, at least 3; a all N shards below them.
  • e works only in the day view and in the all-shards list (not on a day of the calendar/grid; ⏎ drills to the day first).
  • No vim counts in this view: digits 1…9/0 only filter series in the all-shards list. gg / G (first / last day, shard) still work through tui::Pending without counts.
  • t says not available yet in the status line until #282.

1. Sharing the drawing with print.rs

print.rs already 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 needs pub(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.
  • Split calendar and grid so the view can mark the cursor: a variant (or an Option<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).
  • grid gets a max_weeks: Option<usize> (or width) to cut the oldest weeks of a strip; the printed command keeps all.
  • A small to_ratatui(&print::Line) -> ratatui::text::Line<'static> helper in view.rs.

2. model.rs (pure, no I/O)

Model holds:

  • key, the ActivityCommand (cloned) and today / start (first tracked day) for stepping.
  • root: RangeSpec and stack: Vec<Level> — a Level is { 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 (from ViewOptions.exclude_mixed, else the command's), toggled by x.
  • mode: Mode — Dashboard or AllShards { filter: Option<usize>, selected, offset }.
  • cursor: NaiveDate on calendar/grid; selected_shard: usize in the day view.
  • scroll: u16 (page offset, J/K, PgUp/PgDn), help: bool, pending: Pending, status: Option<Status>.
  • A snapshot of what was computed, owned (the core's Activity borrows the shards): Snapshot { range: DayRange, shape: Shape, days: Vec<DayCell>, stats: Stats, matches: Vec<ShownMatch> }, where ShownMatch { 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:

  • Calendar: h/l ±1 day, j/k ±7 days. Grid: h/l ±7 days (a week), j/k ±1 day (weekday). gg/G first/last selectable day.
  • The cursor stays inside the range and on days whose status is not Untracked; a move that would leave them does nothing. Future days are selectable (they are shown, context says future).
  • Initial cursor: today if in the range, else the last tracked day of the range, else its first day. After a recompute the cursor is kept if still in the range, else reset the same way; ⌫ restores the cursor stored in the popped level.
  • ⏎: grid → RangeSpec::Month of the cursor's day; calendar → RangeSpec::Day; day view → nothing.
  • ⌫: pop one level; on the root nothing (status already at the top).
  • [/]: range.step(±1) if can_step(±1, today, start), else status saying why (nothing tracked before … / nothing after today); the result's spec() 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: AllShards over the current range; 1…9 filter to that series if it exists, 0 clears; j/k/gg/G/PgUp/PgDn move; e edits; ⌫ back to the dashboard.
  • x, r, J/K, ? as in the issue; q/Esc/Tab/: are left to the front-end (is_modal true 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:line of the selected shard through PathDisplay. 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's scroll (clamped; the strip or row with the cursor is scrolled into sight):

  • Header line: ‹ label ›, an arrow style::dim() when that way is closed, incl. mixed / excl. mixed.
  • Tiles: three bordered boxes (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, from SeriesDay.first). Under 60 columns: the one line 15 shards · 11 of 17 days active · 6.2 per week.
  • Chart box by shape: calendar titled with the month(s) (OCTOBER 2026), grid one strip per year newest first; legend at its bottom.
  • Days box: days_table of print.rs with the hint line Mixed days count for each series · x to change (wording per #280 for both modes).
  • Latest shards box (calendar/grid): as many rows as fit the remaining height, min 3, then a all N shards.
  • Day shape: the FRIDAY, 16 OCTOBER 2026 box with the shards (time, bullet, title, tags right-aligned), ▶ on the selected one.
  • All shards mode: a list grouped under day headers, newest first, header All 15 · Run 7 · Climbing 8 (active filter highlighted), rows as in the day view.
  • key_bar per mode as in the mockups; status overrides it (tui::widgets::status_line); help overlay with tui::widgets::help.
  • Colours only via tui::style and the series colours of the configuration.

4. terminal.rs (ActivityView<'a>)

  • Holds &ActivityCommand, &ProjectConfig, base_folder, &PathDisplay, the model and the loaded Vec<LocalizedShard> (load_markdown_shards_cached, as run.rs does).
  • recompute(): activity(&shards, command, &range, Utc::now(), tz) for the current level → Snapshot into the model.
  • Titles: run.rs::shown is moved/generalised so it can take a slice of matches (one fs::read_to_string per 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 } via PageRequest::of), cached by (file, line) until the next reload. A file that cannot be read gives its file:line as 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: the Activity arm builds ActivityView::new(key, command, options.range, options.exclude_mixed, project, base_folder, paths) (returning Result like the timesheet view). Remove NOT_IMPLEMENTED and its uses/tests (README/R30 text mentioning the placeholder too).

5. Tests

  • model.rs on the sample of #279/#280 (print.rs tests' sport() / sample() — move them to a shared #[cfg(test)] mod fixtures in commands/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, x toggle, digit filters in the list, e giving Effect::Edit with file and line, no effect of digits outside the list.
  • view.rs TestBackend snapshots (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: an ActivityView on a temporary stream folder with a FakeEditor: e opens the right file:line and reloads.
  • print.rs golden tests stay unchanged (the shared helpers must not alter the printed output).

6. Docs

  • REQUIREMENTS.md R36: 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 the interactive.rs placeholder description in commands/activity/ with interactive/ (model.rs, view.rs, terminal.rs) and the helpers shared with print.rs.

Finish with cargo test, cargo clippy -D warnings, nix flake check.

## Plan Replaces the placeholder `ActivityView` (`crates/streamd-cli/src/commands/activity/interactive.rs`) by a real view in `commands/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) - **Narrow grid**: below the width a strip needs (~62 columns), each strip drops its **oldest weeks from the left**, so the newest weeks and the cursor stay in sight. Nothing scrolls sideways. - **Latest shards** under calendar/grid **fill the height** left on the page after the tiles, the chart and the Days table, **at least 3**; `a all N shards` below them. - **`e`** works only in the day view and in the all-shards list (not on a day of the calendar/grid; ⏎ drills to the day first). - **No vim counts** in this view: digits `1`…`9`/`0` only filter series in the all-shards list. `gg` / `G` (first / last day, shard) still work through `tui::Pending` without counts. - `t` says `not available yet` in the status line until #282. ### 1. Sharing the drawing with `print.rs` `print.rs` already 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 needs `pub(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`. - Split `calendar` and `grid` so the view can mark the cursor: a variant (or an `Option<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). - `grid` gets a `max_weeks: Option<usize>` (or width) to cut the oldest weeks of a strip; the printed command keeps all. - A small `to_ratatui(&print::Line) -> ratatui::text::Line<'static>` helper in `view.rs`. ### 2. `model.rs` (pure, no I/O) `Model` holds: - `key`, the `ActivityCommand` (cloned) and `today` / `start` (first tracked day) for stepping. - `root: RangeSpec` and `stack: Vec<Level>` — a `Level` is `{ 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` (from `ViewOptions.exclude_mixed`, else the command's), toggled by `x`. - `mode: Mode` — `Dashboard` or `AllShards { filter: Option<usize>, selected, offset }`. - `cursor: NaiveDate` on calendar/grid; `selected_shard: usize` in the day view. - `scroll: u16` (page offset, J/K, PgUp/PgDn), `help: bool`, `pending: Pending`, `status: Option<Status>`. - A snapshot of what was computed, **owned** (the core's `Activity` borrows the shards): `Snapshot { range: DayRange, shape: Shape, days: Vec<DayCell>, stats: Stats, matches: Vec<ShownMatch> }`, where `ShownMatch { 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: - Calendar: `h`/`l` ±1 day, `j`/`k` ±7 days. Grid: `h`/`l` ±7 days (a week), `j`/`k` ±1 day (weekday). `gg`/`G` first/last selectable day. - The cursor stays inside the range and on days whose status is not `Untracked`; a move that would leave them does nothing. Future days are selectable (they are shown, context says `future`). - Initial cursor: today if in the range, else the last tracked day of the range, else its first day. After a recompute the cursor is kept if still in the range, else reset the same way; ⌫ restores the cursor stored in the popped level. - ⏎: grid → `RangeSpec::Month` of the cursor's day; calendar → `RangeSpec::Day`; day view → nothing. - ⌫: pop one level; on the root nothing (status `already at the top`). - `[`/`]`: `range.step(±1)` if `can_step(±1, today, start)`, else status saying why (`nothing tracked before …` / `nothing after today`); the result's `spec()` 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`: `AllShards` over the current range; `1`…`9` filter to that series if it exists, `0` clears; `j`/`k`/`gg`/`G`/PgUp/PgDn move; `e` edits; ⌫ back to the dashboard. - `x`, `r`, `J`/`K`, `?` as in the issue; `q`/`Esc`/`Tab`/`:` are left to the front-end (`is_modal` true 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:line` of the selected shard through `PathDisplay`. `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's `scroll` (clamped; the strip or row with the cursor is scrolled into sight): - Header line: `‹ label ›`, an arrow `style::dim()` when that way is closed, `incl. mixed` / `excl. mixed`. - Tiles: three bordered boxes (`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`, from `SeriesDay.first`). Under 60 columns: the one line `15 shards · 11 of 17 days active · 6.2 per week`. - Chart box by shape: calendar titled with the month(s) (`OCTOBER 2026`), grid one strip per year newest first; legend at its bottom. - Days box: `days_table` of `print.rs` with the hint line `Mixed days count for each series · x to change` (wording per #280 for both modes). - Latest shards box (calendar/grid): as many rows as fit the remaining height, min 3, then `a all N shards`. - Day shape: the `FRIDAY, 16 OCTOBER 2026` box with the shards (time, bullet, title, tags right-aligned), `▶` on the selected one. - All shards mode: a list grouped under day headers, newest first, header `All 15 · Run 7 · Climbing 8` (active filter highlighted), rows as in the day view. - `key_bar` per mode as in the mockups; `status` overrides it (`tui::widgets::status_line`); help overlay with `tui::widgets::help`. - Colours only via `tui::style` and the series colours of the configuration. ### 4. `terminal.rs` (`ActivityView<'a>`) - Holds `&ActivityCommand`, `&ProjectConfig`, `base_folder`, `&PathDisplay`, the model and the loaded `Vec<LocalizedShard>` (`load_markdown_shards_cached`, as `run.rs` does). - `recompute()`: `activity(&shards, command, &range, Utc::now(), tz)` for the current level → `Snapshot` into the model. - Titles: `run.rs::shown` is moved/generalised so it can take a slice of matches (one `fs::read_to_string` per 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 }` via `PageRequest::of`), cached by `(file, line)` until the next reload. A file that cannot be read gives its `file:line` as 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`: the `Activity` arm builds `ActivityView::new(key, command, options.range, options.exclude_mixed, project, base_folder, paths)` (returning `Result` like the timesheet view). Remove `NOT_IMPLEMENTED` and its uses/tests (README/R30 text mentioning the placeholder too). ### 5. Tests - `model.rs` on the sample of #279/#280 (`print.rs` tests' `sport()` / `sample()` — move them to a shared `#[cfg(test)] mod fixtures` in `commands/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, `x` toggle, digit filters in the list, `e` giving `Effect::Edit` with file and line, no effect of digits outside the list. - `view.rs` `TestBackend` snapshots (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`: an `ActivityView` on a temporary stream folder with a `FakeEditor`: `e` opens the right `file:line` and reloads. - `print.rs` golden tests stay unchanged (the shared helpers must not alter the printed output). ### 6. Docs - `REQUIREMENTS.md` R36: 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 the `interactive.rs` placeholder description in `commands/activity/` with `interactive/` (`model.rs`, `view.rs`, `terminal.rs`) and the helpers shared with `print.rs`. Finish with `cargo test`, `cargo clippy -D warnings`, `nix flake check`.
Author
Collaborator

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 yet message on t.

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 warnings passes, as do all seven x86_64-linux checks of nix flake check. Working tree is clean.

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 yet` message on `t`. 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 warnings` passes, as do all seven x86_64-linux checks of `nix flake check`. Working tree is clean.
Sign in to join this conversation.
No labels
android
cli
epic
planned
tui
No project
No assignees
1 participant
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#281
No description provided.