Commit Graph

23 Commits

Author SHA1 Message Date
Claude (agent-faces) f7d2730736 style(faces): rustfmt the face migration (improvise-nn4s.2)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 22:05:24 -07:00
Claude (agent-faces) 12d976b432 feat(faces): the TUI draws axis, wizard-field, mode and minibuffer colours through face_style (improvise-nn4s.2)
axis_fg, minibuf_fg, field_kind_fg and mode_style are gone, and AxisColor
leaves improvise-core. The category-add prompt turns green, ADR 004
clause 8's one declared exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 21:58:01 -07:00
Edward Langley 241cc5f384 fix(tui): the grid and the tile bar scroll by the width they are drawn in (improvise-gtn7.1)
Grid: App::cmd_context counted visible_cols from term_width while
draw_content gave the grid term_width minus the 32-column side panel, so
with any panel open the cursor was scrolled past the last drawn column.
viewport::grid_width, reached through App::grid_width, is now the one
answer: draw_content splits on it and the keypress path counts on it.

Tile bar: the scroll measured from the end of the prefix while the draw
began one column later, two once scrolled (the left arrow), so the
selected tile could be dropped. tiles_left(start) is now where both the
scroll (first_tile) and the draw begin.

The grid defect dates from b5418f2e; it became the launch state when
50e4a372 opened the category panel by default, which is why it read as
a regression. Mixed-width columns still lose the cursor with no panel
open: improvise-gtn7.2. Reasoning: context/code/tui.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 18:32:47 -07:00
Edward Langley b945a7f881 test: the TUI cursor stays drawn beside a side panel and on the tile bar (improvise-gtn7.1)
Both fail on gh/main: the grid at column 4 with any side panel open, the
tile bar at width 42.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 18:25:40 -07:00
Edward Langley 915d0fefeb fix(tui): one key table on screen, and it is the derived one (improvise-rr8n)
Two hand-written key tables were drawn BESIDE derived ones, in a different
notation, and one of them was false.

The tile bar's non-TileSelect hint read " Ctrl+^v<-> to move tiles" while
Ctrl-Left/Right/Up/Down are bound in Normal to enter-tile-select -- pressing one
ENTERS tile mode and moves nothing. It was the only persistent on-screen
documentation of those keys, and it spelt Ctrl+ where key_label, and therefore
every derived hint and help row, spells Ctrl-.

The fix DELETES both of the tile bar's hints rather than rewriting one, which
departs from the ticket's acceptance criteria and does so on the ticket's own
evidence. rr8n proposed rewriting the string and suppressing the STATUS BAR's
hint in TileSelect. But the derived status line is a strict superset of the tile
bar's literal -- it carries Space, h/l and Tab, which the tile bar's omits, as
the ticket's own finding 2 records -- and it cannot go stale, being read off the
keymap. Suppressing it would have deleted information and kept the table that had
just been shown to rot. In Normal the derived line already says T:tiles, so only
the Ctrl-arrow spelling is lost, and it was never documented correctly.

The wizard goes the other way and there the ticket is right. Its overlay draws
correct per-step hints; the status bar drew one static line under it, wrong on
four of eight steps, including telling the user Esc cancels on Done where Esc
goes BACK. That line is derived too, but from the wrong input -- the wizard's
real keys are resolved per step inside HandleWizardKey. So draw_status suppresses
its hint fallback for that one mode.

The rule, stated in context/code/tui.md: keep whichever table is derived from the
thing it describes.

Five tests. Four faults injected, each caught by a distinct test; the sharpest is
suppressing the hint fallback unconditionally, which passes both tests aimed at
the suppression and is caught only by the counterweight asserting TileSelect
still advertises its keys. Two things that let the false hint survive are
recorded: every_hint_key_resolves_in_its_mode cannot see a literal in a widget,
and the widget's own characterization test renders at 80 columns, where the hint
does not fit and is never drawn.

Evidence: context/test-evidence/tui.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:51:12 -07:00
Edward Langley e8e7a7f1fe refactor(panels, examples): cap the remaining runs
The panel migration tests (improvise-35e) keep their claim and lose their
history; tile_bar's scroll loop keeps its algorithm note. gen-grammar's
usage block is trimmed to the one line a reader runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:51:38 -07:00
Edward Langley 92fd3a8701 feat(ui): widgets read per-widget caches, not the model (improvise-35e)
The ticket asked for `RenderCache::from_app(app, subs)` populating a fat
aggregate cache. That aggregate does not exist and will not: improvise-rkeq
decided caches are per-widget and state syncs by events (gotcha 32). Checking
the premise first (doctrine(check_the_ticket_premise)) found the builders
already present and public in `ui/project.rs` — `grid_of`, `category_rows`,
`formula_texts`, `view_summaries`, `tile_entries` — with `Dirty` already
answering what a `SubscriptionSet` was for. Two things were actually missing.

**The root crate could not name a cache.** `improvise` has no
`improvise-protocol` dependency and `improvise-command` did not re-export it,
so no widget in `src/` could write `fn new(cache: &FormulaCache)`. Re-exported
as `improvise_command::protocol`, so a host names the caches through the crate
that builds them.

**The builders returned payloads, not caches.** `grid_of` handed back a
`GridCache` while its four siblings handed back the `Vec`s inside one.
`category_tree_of`/`formulas_of`/`view_list_of`/`tile_bar_of` complete the set,
and `document_projections` and the tests' `server_truth` now derive from them,
so the cache a widget holds and the projection a client folds are one
derivation. There is deliberately no function returning all five and no struct
holding them — that aggregate is the rejected shape.

Three widgets migrate off the model: the formula panel, the view panel and the
tile bar now render from `FormulaCache`, `ViewListCache` and `TileBarCache`.
The view panel takes its active view from `ViewState`, which is where per-client
state belongs. No widget names the seam: `draw.rs` builds the caches per frame,
and that per-frame rebuild is the temporary half, deleted when the TUI becomes
a client.

Not migrated, and why: the category panel's `build_cat_tree` walks
`Model::category_names()` and so shows `_Index`/`_Dim`/`_Measure`, while
`CategoryRow` carries only `regular_category_names()`. Migrating it would drop
three rows from the panel — a behaviour change, not a relocation, so it is left
for improvise-edp to decide. The grid is `edp`/`cr3` work: `GridCache` is
viewport-indexed and carries no group headers, cursor or edit overlay.

Latent bug noted, not fixed: `TileBar` called `View::axis_of`, which panics for
a category the view has never seen, on a list `tileable_cats` admits exactly
such categories to. `tile_entries` uses `try_axis_of`, so the migrated widget
cannot panic there.

Tests: an equivalence test per migrated widget asserting the cache carries what
the widget used to read off `&Model`/`&View`, plus pinned render output. All
five fire under injection (doctrine(check_the_checker)): reversing
`formula_texts`, filtering virtual categories out of `tile_entries`, and
dropping Column axes from `view_summaries` each fail their test and pass
without.

Gates: 930 passing (924 on base, +6 new), clippy clean, fmt clean,
wasm32 check clean for improvise-command and improvise-protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 16:39:17 -07:00
Edward Langley f448c651ce feat(axis): add Axis::Filter for drill-fixed coordinates
Drilling into a cell fixed its coordinates on Axis::Page, which put them
in the tile bar and under [ and ] as if the user could move them. They
are constraints of the drill, not dimensions to page through. They now
land on Axis::Filter.

The variant splits one question into two, and picking the wrong one is
the trap this introduces. "What data is visible" is View::filtering_cats
-- Page and Filter both constrain -- and GridLayout and add-record-row
ask it. "What can the user act on" is categories_on(Axis::Page) for [/]
and View::tileable_cats for the three tile-select sites. Both predicates
are named on Axis (filters_data, is_user_assignable) rather than left as
tests against the variant, so a third variant does not silently join the
wrong side.

Axis::next maps Filter to itself and no navigable axis rotates into it,
so neither tile select nor cycle-axis can hand out a Filter assignment.

The .improv grammar gains a filter keyword and round-trips a selection
with it. Filter only ever occurs in the _Drill view, which is never
written -- but the alternative to supporting it was silently writing a
Filter axis out as some other axis.

Worth recording: adding the variant produced exactly one compile error
outside axis.rs. improvise-rml had already collapsed the four scattered
display matches into one file, so the consolidation paid for itself on
the very next ticket.

Closes improvise-dz8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V69PGQfMJuGRmtSWpfVAqn
2026-08-31 22:45:00 -07:00
Edward Langley c9b256889d refactor(axis): consolidate axis presentation onto the Axis enum
Axis label and color tables were spread across four sites: the tile bar,
the category panel, the view panel, and the tile command. Adding an axis
variant meant finding all four. They now hang off Axis in view/axis.rs.

The ticket's signature was a layering violation. It asked for
display_short() -> (&'static str, Color), but Color is a ratatui type and
Axis lives in improvise-core, which may not name a UI type. Split into
color() -> AxisColor, a semantic palette, plus the label methods, with
draw.rs::axis_fg resolving the palette to a terminal color -- the same
arrangement minibuf_fg already has for MinibufColor.

The ticket also called the category panel an "exact copy" of the tile
bar. It was not. The colors were identical; the labels were not ("Row ↕"
vs "Row", "None ∅" vs "·"). So only the color table was true
duplication, and that is the part with nothing keeping the two copies in
agreement. The four label sets are four deliberate vocabularies and stay
four methods -- plain_label, glyph (Display appends it), short_label
(three columns; None is "·", not a truncation of "Non"), and initial
(one letter; None is empty). A characterization test pins all four
verbatim so the move could not quietly restyle anything.

next() replaces the rotation match in both cmd/tile.rs and
View::cycle_axis. is_data_axis() from the ticket is omitted: no caller
wanted it.

Not touched: persistence/mod.rs maps Axis to the on-disk keywords. That
is a format concern and belongs in the io layer.

Closes improvise-rml

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V69PGQfMJuGRmtSWpfVAqn
2026-08-31 22:30:16 -07:00
Edward Langley 4e37e12f9a style: reformat code and cleanup whitespace
Reformat code for improved readability and remove unnecessary whitespace.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-26B-A4B-it-UD-Q5_K_XL.gguf)
2026-04-15 23:42:44 -07:00
Edward Langley 3fbf56ec8b refactor: break Model↔View cycle, introduce Workbook wrapper
Model is now pure data (categories, cells, formulas, measure_agg) with
no references to view/. The Workbook struct owns the Model together
with views and the active view name, and is responsible for cross-slice
operations (add/remove category → notify views, view management).

- New: src/workbook.rs with Workbook wrapper and cross-slice helpers
  (add_category, add_label_category, remove_category, create_view,
  switch_view, delete_view, normalize_view_state).
- Model: strip view state and view-touching methods. recompute_formulas
  remains on Model as a primitive; the view-derived none_cats list is
  gathered at each call site (App::rebuild_layout, persistence::load)
  so the view dependency is explicit, not hidden behind a wrapper.
- View: add View::none_cats() helper.
- CmdContext: add workbook and view fields so commands can reach both
  slices without threading Model + View through every call.
- App: rename `model` field to `workbook`.
- Persistence (save/load/format_md/parse_md/export_csv): take/return
  Workbook so the on-disk format carries model + views together.
- Widgets (GridWidget, TileBar, CategoryContent, ViewContent): take
  explicit &Model + &View instead of routing through Model.

Tests updated throughout to reflect the new shape. View-management
tests that previously lived on Model continue to cover the same
behaviour via a build_workbook() helper in model/types.rs. All 573
tests pass; clippy is clean.

This is Phase A of improvise-36h. Phase B will mechanically extract
crates/improvise-core/ containing model/, view/, format.rs, workbook.rs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:08:11 -07:00
Edward Langley 8b7b45587b refactor(ui): use iterator in TileBar loop
Update the loop in TileBar to use an iterator-based approach with
enumerate, take, and skip instead of indexing.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-31B-it-UD-Q4_K_XL.gguf)
2026-04-14 01:03:25 -07:00
Edward Langley 4686f47026 fix: scroll tile bar to keep selected tile visible
When there are more tiles than fit in the available width, the tile
bar now auto-scrolls to ensure the selected tile is always visible.
Overflow indicators (◀ ▶) show when tiles exist beyond the visible
area. Scroll offset is computed fresh each frame from tile_cat_idx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:07:58 -07:00
Edward Langley 7dd9d906c1 refactor(ui): improve rendering, feedback, and help system
Improve UI rendering and feedback:

- `src/draw.rs` :
    - Automatically enter Help mode if the model is empty.
    - Render the `HelpWidget` with the current help page.
    - Render the `WhichKeyWidget` when a transient keymap is active.
- `src/ui/tile_bar.rs` : Use more descriptive labels for axes (e.g., "Row",
  "Col", "Pag").
- `src/ui/view_panel.rs` :
    - Include an axis summary (e.g., "R:Region C:Product") next to view
      names.
    - Refactor `ViewContent` to hold a reference to the `Model` .
- `src/ui/effect.rs` : Add error feedback to `AddItem` , `AddItemInGroup` ,
  and `AddFormula` when operations fail.
- `src/ui/help.rs` : Refactor `HelpWidget` into a multi-page system.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
2026-04-11 00:06:50 -07:00
Edward Langley 9b8f939245 misc 2026-04-11 00:06:48 -07:00
Edward Langley def4f0b7df refactor(ui): improve TileBar rendering and width calculation
Update TileBar to use UnicodeWidthStr for accurate text width calculation and
improve axis display.

- Use `unicode_width::UnicodeWidthStr` for calculating label and hint widths
- Move `axis_display` to a static method on `TileBar`
- Update axis symbols for better visual clarity (e.g., '|', '-', '=', '.')
- Clear the tile bar area before rendering to prevent stale characters

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
2026-04-11 00:06:48 -07:00
Edward Langley 0db89b1e3a chore: clippy + fmt 2026-04-04 09:59:01 -07:00
Edward Langley b7e4316cef feat(command): add CmdContext extensions and new effects
Add new fields to CmdContext for tracking search mode, panel cursors,
tile category index, named text buffers, and key information.

Add SetBuffer and SetTileCatIdx effects for managing application state.
Update TileBar to accept tile_cat_idx parameter for rendering.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
2026-04-04 09:58:30 -07:00
Edward Langley 5a251a1cbe feat: add Axis::None for hidden dimensions with implicit aggregation
Categories on the None axis are excluded from the grid and cell keys.
When evaluating cells, values across hidden dimensions are aggregated
using a per-measure function (default SUM). Adds evaluate_aggregated
to Model, none_cats to GridLayout, and 'n' shortcut in TileSelect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:38:35 -07:00
Edward Langley 183b2350f7 chore: reformat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 00:07:22 -07:00
Ed L 6038cb2d81 refactor: make active_view and axis_of infallible
Both functions previously returned Option despite their invariants
guaranteeing a value: active_view always names an existing view
(maintained by new/switch_view/delete_view), and axis_of only returns
None for categories never registered with the view (a programming error).

Callers no longer need to handle the impossible None case, eliminating
~15 match/if-let Option guards across app.rs, dispatch.rs, grid.rs,
tile_bar.rs, and category_panel.rs.

Also adds Model::evaluate_f64 (returns 0.0 for empty cells) and collapses
the double match-on-axis pattern in tile_bar/category_panel into a single
axis_display(Axis) helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:00:25 -07:00
Ed L e680b098ec refactor: remove Axis::Unassigned; axis_of returns Option<Axis>
Axis::Unassigned served two purposes that Option already covers:
  1. "this category has no assignment yet" → None
  2. "this category doesn't exist" → None

By removing the variant and changing axis_of to return Option<Axis>,
callers are forced by the compiler to handle the absent-category case
explicitly (via match or unwrap_or), rather than silently treating it
like a real axis value.

SetAxis { axis: String } also upgraded to SetAxis { axis: Axis }.
Previously, constructing SetAxis with an invalid string (e.g. "diagonal")
would compile and then silently fail at dispatch. Now the type only admits
valid axis values; the dispatch string-parser is gone.

Axis gains #[serde(rename_all = "lowercase")] so existing JSON command
files (smoke.jsonl, etc.) using "row"/"column"/"page" continue to work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 07:32:48 -07:00
Ed L eae00522e2 Initial implementation of Improvise TUI
Multi-dimensional data modeling terminal application with:
- Core data model: categories, items, groups, sparse cell store
- Formula system: recursive-descent parser, named formulas, WHERE clauses
- View system: Row/Column/Page axes, tile-based pivot, page slicing
- JSON import wizard (interactive TUI + headless auto-mode)
- Command layer: all mutations via typed Command enum for headless replay
- TUI: Ratatui grid, tile bar, formula/category/view panels, help overlay
- Persistence: .improv (JSON), .improv.gz (gzip), CSV export, autosave
- Static binary via x86_64-unknown-linux-musl + nix flake devShell
- Headless mode: --cmd '{"op":"..."}' and --script file.jsonl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:11:55 -07:00