Refactor the keymap system to use string-based command names instead of
concrete command struct instantiations. This introduces a Binding enum that
can represent either a command lookup (name + args) or a prefix sub-keymap.
Key changes:
- Keymap now stores Binding enum instead of Arc<dyn Cmd>
- dispatch() accepts CmdRegistry to resolve commands at runtime
- Added bind_args() for commands with arguments
- KeymapSet now owns the command registry
- Removed PrefixKey struct, inlined its logic
- Updated all default keymap bindings to use string names
This enables more flexible command configuration and easier testing.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
Update headless command execution to use the new architecture:
- Removed CommandResult import (no longer needed)
- Headless mode now creates an App instance and uses cmd_context()
- Commands are parsed and executed via the registry, effects applied
through app.apply_effects() instead of command::dispatch()
- Made cmd_context() public so headless mode can access it
- Updated persistence save to use app.model instead of direct model
Tests updated to use ExecuteCommand instead of QuitCmd, with proper
buffer setup for command parsing.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
Add two new effects for headless model operations:
- LoadModel: Loads a model from a file path, replacing the current one
- ImportJsonHeadless: Imports JSON/CSV files via the analyzer, builds
a new model from detected fields, and replaces the current model
These effects enable headless mode to load and import data without
interactive prompts. ImportJsonHeadless handles both CSV and JSON
files, auto-detects array paths, and uses the existing import pipeline.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
Remove unused imports for legacy code that is no longer needed.
Delete execute_command function that handled :q, :w, :import
commands via direct AppMode matching.
Delete handle_wizard_key function and associated fallback logic
for modes not yet migrated to keymaps. These are now handled
by the new keymap-based approach.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
Add WizardKey effect to handle key bindings for navigating
wizard steps: Preview, SelectArrayPath, ReviewProposals,
ConfigureDates, DefineFormulas, and NameModel.
Add StartImportWizard effect to initialize the wizard by
reading and parsing a JSON file.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
Update handle_key to pass key code and modifiers to cmd_context.
Update keymap_set.dispatch to pass search_mode from App state.
Remove old-style panel handlers (handle_edit_key, handle_formula_edit_key,
handle_formula_panel_key, etc.) - approximately 500 lines.
Update handle_command_mode_key to use buffers map for command execution.
All other modes now handled via keymap dispatch.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
Add tile_cat_idx field to track selected tile category index.
Add buffers HashMap for named text buffers used in text-entry modes.
Update AppMode::TileSelect to remove nested cat_idx struct.
Update cmd_context to accept key and modifiers parameters.
Update cmd_context to populate new fields from App state.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
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)
- Fixed transient keymap consumption logic: the transient keymap is now only
consumed when a match is found.
- Updated handle_key to retain transient keymap if no command matches, allowing
subsequent key presses to be processed by the main keymap set.
- Added issue noting the previous unintended consumption.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
- Reformatted method calls in RemoveFormula and SetAxis for consistency.
- Minor formatting changes to improve readability.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
- Updated imports to include Arc and removed KeyModifiers.
- Replaced pending_key with transient_keymap and keymap_set.
- Added KeymapSet for mode-specific keymaps.
- Removed legacy pending key logic and many helper methods.
- Updated tests to use new command execution pattern.
- Adjusted App struct and methods to align with new keymap system.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
Create keymap.rs with Keymap struct mapping (mode, key) to Cmd trait
objects. Wire into App::handle_key — keymap dispatch is tried first,
falling through to old handlers for unmigrated bindings. Normal mode
navigation, cell ops, mode switches, and Help mode are keymap-driven.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Define Effect trait in ui/effect.rs with concrete effect structs for
all model mutations, view changes, navigation, and app state updates.
Each effect implements apply(&self, &mut App). Add App::apply_effects
to apply a sequence of effects. No behavior change yet — existing
key handlers still work as before.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add key handling for ConfigureDates (space toggle components) and
DefineFormulas (n new, d delete, text input mode) wizard steps.
Render date component toggles, formula list with input area, and
sample formulas derived from detected measures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Add row_group_for/col_group_for to GridLayout, replacing inline
backward-search logic. Refactor grid renderer to use col_group_for
instead of pre-filtering col_items. Add gz keybinding for column
group collapse toggle, symmetric with z for rows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Builds out two half-finished view features:
Group collapse:
- AxisEntry enum distinguishes GroupHeader from DataItem on grid axes
- expand_category() emits group headers and filters collapsed items
- Grid renders inline group header rows with ▼/▶ indicator
- `z` keybinding toggles collapse of nearest group above cursor
Hide/show item:
- Restore show_item() (was commented out alongside hide_item)
- Add HideItem / ShowItem commands and dispatch
- `H` keybinding hides the current row item
- `:show-item <cat> <item>` command to restore hidden items
- Restore silenced test assertions for hide/show round-trip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes unused methods (sum_matching, get_mut, item_by_name, item_index,
top_level_groups, is_group_collapsed, show_item) and unused constants
(LABEL_THRESHOLD, MIN_COL_WIDTH).
The sum_matching tests in model.rs were bypassing the formula evaluator
entirely. Replaced them with equivalent tests that call evaluate() against
the existing Total = SUM(Revenue) formula, exercising the real aggregation
code path.
Also fixes a compile error in view.rs prop_tests where View/Axis imports
and a doc comment were incorrectly commented out.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the post-execution mode reset from the caller. execute_command
now sets mode = Normal at the top as the default; commands that open
a new mode (ImportWizard, Quit) simply override it. The caller no
longer needs a special-case exclusion list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
execute_command set mode to ImportWizard, but the caller immediately
reset it to Normal for any non-Quit mode. Added ImportWizard to the
exclusion list so the wizard survives the reset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing 't' swaps all Row-axis categories to Column and all
Column-axis categories to Row, leaving Page categories unchanged.
Selection and scroll offsets are reset to (0,0).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Column headers now render one row per column category instead of
joining with '/'. Row headers render one sub-column per row category.
Repeat suppression hides labels when the prefix is unchanged from
the previous row/column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Previously CellValue had three variants: Number, Text, and Empty.
The Empty variant acted as a null sentinel, but the compiler could not
distinguish between "this is a real value" and "this might be empty".
Code that received a CellValue could use it without checking for Empty,
because there was no type-level enforcement.
Now CellValue has only Number and Text. The absence of a value is
represented as None at every API boundary:
DataStore::get() → Option<&CellValue> (was &CellValue / Empty)
Model::get_cell() → Option<&CellValue> (was &CellValue / Empty)
Model::evaluate() → Option<CellValue> (was CellValue::Empty)
eval_formula() → Option<CellValue> (was CellValue::Empty)
Model gains clear_cell() for explicit key removal; ClearCell dispatch
calls it instead of set_cell(key, CellValue::Empty).
The compiler now forces every caller of evaluate/get_cell to handle
the None case explicitly — accidental use of an empty value as if it
were real is caught at compile time rather than silently computing
wrong results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Previously `pub formulas: Vec<Formula>` allowed any code to call
`model.formulas.push(formula)` directly, bypassing the dedup logic in
`add_formula` that enforces the (target, target_category) uniqueness
invariant.
Making the field private means the only mutation paths are
`add_formula` and `remove_formula`, both of which maintain the invariant.
A `pub fn formulas(&self) -> &[Formula]` accessor preserves read access
for the UI and tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three copies of cross_product existed (grid.rs, app.rs, persistence/mod.rs)
with slightly different signatures. Extracted into GridLayout in
src/view/layout.rs, which is now the single canonical mapping from a View
to a 2-D grid: row/col counts, labels, and cell_key(row, col) → CellKey.
All consumers updated to use GridLayout::new(model, view):
- grid.rs: render_grid, total-row computation, page bar
- persistence/mod.rs: export_csv
- app.rs: move_selection, jump_to_last_row/col, scroll_rows,
search_navigate, selected_cell_key
Also includes two app.rs UI bug fixes that were discovered while
refactoring:
- Ctrl+Arrow tile movement was unreachable (shadowed by plain arrow arms);
moved before plain arrow handlers
- RemoveFormula dispatch now passes target_category (required by the
formula management fix in the previous commit)
GridLayout has 6 unit tests covering counts, label formatting, cell_key
correctness, out-of-bounds, page coord inclusion, and evaluate round-trip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.rs: scroll_rows (Ctrl+D/U) now clamps to the cross-product row
count and follows the viewport, matching move_selection's behaviour.
Previously it could push selected past the last row, causing
selected_cell_key to return None and silently ignoring edits.
- model.rs: add normalize_view_state() which resets row/col offsets to
zero on all views.
- main.rs, dispatch.rs, app.rs: call normalize_view_state() after every
model replacement (initial load, :Load command, wizard import) so
stale offsets from a previous session can't hide the grid.
- app.rs: clamp formula_cursor to the current formula list length at the
top of handle_formula_panel_key so a model reload with fewer formulas
can't leave the cursor pointing past the end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[/] previously broke after the first page category due to a hard-coded
`break`. Replaced with odometer-style navigation: ] advances the last
page category, carrying into the previous when it wraps (like digit
incrementing). [ decrements the same way. Single-category behaviour is
unchanged except it now wraps around instead of clamping at the end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
move_selection, jump_to_last_row/col, and selected_cell_key all used
items.get(sel_row) on the first axis category, which returned None for
any cursor position beyond that category's item count. They now compute
the full Cartesian product (via cross_product_strs) and index into it,
so navigation and cell edits work correctly with multiple categories on
the same axis.
Also adds viewport-following scroll in move_selection/jump helpers so
the cursor stays visible when navigating past the visible window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- grid.rs: honour view.number_format (",.0" default, ",.2", ".4", etc.)
via parse_number_format/format_f64(n,comma,decimals); format_f64 now
pub so callers can reuse the same formatting logic.
- app.rs: n/N actually navigate to next/prev search match (cross-product
aware); fix dead unreachable N arm; add :set-format / :fmt command to
change the active view's number_format at runtime.
- persistence/mod.rs: CSV export now uses full cross-product of all
row/col-axis categories, matching grid rendering behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes dead use statements across dispatch, formula, import, model, and
UI modules. Prefixes intentionally unused variables with _ in app.rs,
analyzer.rs, and grid.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ImportPipeline holds all data and logic (raw JSON, records, proposals,
model_name, build_model). ImportWizard wraps it with UI-only state
(step, cursor, message). Rename WizardState → WizardStep throughout.
Headless import in dispatch.rs now constructs ImportPipeline directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
move_selection() only clamped at 0, letting the row/col index go past
the last valid item. Selected_cell_key() would then return None, leaving
the cursor in a phantom position with no selectable cell.
Now clamp both row and col against the actual item count so pressing
Enter on the last row keeps the cursor on that row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
N (from anywhere) or n (in Category panel) opens an inline prompt
to add categories one after another without typing :add-cat each time.
- Yellow border + prompt distinguishes it from item-add (green)
- Enter / Tab adds the category and clears the buffer, staying open
- Esc returns to the category list
- Cursor automatically moves to the newly added category
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new ways to add multiple items without repeating yourself:
1. :add-items <category> item1 item2 item3 ...
Adds all space-separated items in one command.
2. Category panel quick-add mode (press 'a' or 'o' on a category):
- Opens an inline prompt at the bottom of the panel
- Enter adds the item and clears the buffer — stays open for next entry
- Tab does the same as Enter
- Esc closes and returns to the category list
- The panel border turns green and the title updates to signal add mode
- Item count in the category list updates live as items are added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Welcome overlay shown when model has no categories, listing common
commands and navigation hints to orient new users
- Vim-style keybindings:
- i / a → Insert mode (edit cell); Esc → Normal
- x → clear cell; yy / p → yank / paste
- G / gg → last / first row; 0 / $ → first / last col
- Ctrl+D / Ctrl+U → half-page scroll
- n / N → next / prev search match
- T → tile-select mode (single key, no Ctrl needed)
- ZZ → save + quit
- F / C / V → toggle panels (no Ctrl needed)
- ? → help (in addition to F1)
- Command mode (:) for vim-style commands:
:q :q! :w [path] :wq ZZ
:import <file.json> :export [path]
:add-cat <name> :add-item <cat> <item>
:formula <cat> <Name=expr> :add-view [name] :help
- Status bar now context-sensitive: shows mode-specific hint text
instead of always showing the same generic shortcuts
- Mode label changed: "Editing" → "INSERT" to match vim convention
- Title bar shows filename in parentheses when model is backed by a file
- Help widget updated with full key reference in two-column layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>