- Implemented a suite of new commands for panel visibility, editing, export
prompts, search navigation, page cycling, and grid operations.
- Updated tests to cover new command behavior.
- Adjusted command context usage accordingly.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
- Updated imports to include Panel and Axis.
- Added new fields to CmdContext: formula_panel_open, category_panel_open,
view_panel_open.
- Reformatted effect vectors for consistency.
- Minor formatting changes to improve readability.
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>
Replace JSON command syntax with prefix notation: `word arg1 arg2`.
Multiple commands per line separated by `.`. Coordinate pairs use
`Category/Item`. Quoted strings for multi-word values. set-cell
uses value-first: `set-cell 100 Region/East Measure/Revenue`.
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>
Extend FieldProposal with chrono-based date format detection and
configurable component extraction (Year, Month, Quarter). Add
ConfigureDates and DefineFormulas wizard steps to ImportPipeline.
build_model injects derived date categories and parses formula strings.
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>
- Use csv crate for robust CSV parsing (handles quoted fields, empty values, \r\n)
- Extend --import command to auto-detect format by file extension (.csv or .json)
- Reuse existing ImportPipeline and analyzer for field type detection
- Categories detected automatically (string fields), measures for numeric fields
- Updated help text and welcome screen to mention CSV support
All 201 tests pass.
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>
The two tests were previously silenced when item_by_name was removed.
Rewrites them using category.items.get() directly, restoring coverage
of item-name and item-group serialization round-trips.
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>
Previously --import ran ImportJson headless before the TUI started,
hitting the category limit and printing the error to stderr where it
was invisible. Now it parses the JSON and opens the ImportWizard on
startup, matching :import behavior.
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>
Root cause: set_axis silently ignores unregistered categories, so a
view section appearing before its categories would produce wrong axis
assignments when on_category_added later ran and assigned defaults.
Fix: collect all raw data in pass 1, then build the model in the
correct dependency order in pass 2 (categories → views → data/formulas).
The file can now list sections in any order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New format is diffable plain text with categories, items, formulas,
data cells, and views all readable without tooling. Legacy JSON files
(detected by leading '{') still load correctly for backwards compat.
Format overview:
# Model Name
## Category: Type
- Food
- Gas [Essentials]
## Data
Month=Jan, Type=Food = 100
## View: Default (active)
Type: row
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>
The CSV export used Box<dyn Iterator<Item = Option<usize>>> to unify
empty and non-empty row iteration, violating the CLAUDE.md rule that
Box/Rc container management should be split from logic. Replaced with a
direct for loop over row indices, removing both the Box and the Option
sentinels used to represent "placeholder empty row/col".
Also removes unused pub use cell::CellKey re-export and an unused
import in cell.rs tests.
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 Expr::BinOp(String, ...) accepted any string as an operator.
Invalid operators (e.g. "diagonal") would compile fine and silently
return CellValue::Empty at eval time.
Now BinOp is an enum with variants Add/Sub/Mul/Div/Pow/Eq/Ne/Lt/Gt/Le/Ge.
The parser produces enum variants directly; the evaluator pattern-matches
exhaustively with no fallback branch. An invalid operator is now a
compile error at the call site, and the compiler ensures every variant
is handled in both eval_expr and eval_bool.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>