- 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>
musl tooling is Linux-only, so guard it behind an isLinux check in
flake.nix and remove the hardcoded musl build target from .cargo/config.toml
(the nix devShell already sets CARGO_BUILD_TARGET on Linux).
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>