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>
This commit is contained in:
Ed L
2026-03-20 21:11:14 -07:00
parent 0ba39672d3
commit eae00522e2
35 changed files with 5413 additions and 0 deletions

121
src/model/category.rs Normal file
View File

@ -0,0 +1,121 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
pub type CategoryId = usize;
pub type ItemId = usize;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
pub id: ItemId,
pub name: String,
/// Parent group name, if any
pub group: Option<String>,
}
impl Item {
pub fn new(id: ItemId, name: impl Into<String>) -> Self {
Self { id, name: name.into(), group: None }
}
pub fn with_group(mut self, group: impl Into<String>) -> Self {
self.group = Some(group.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Group {
pub name: String,
/// Parent group name for nested hierarchies
pub parent: Option<String>,
}
impl Group {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), parent: None }
}
pub fn with_parent(mut self, parent: impl Into<String>) -> Self {
self.parent = Some(parent.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Category {
pub id: CategoryId,
pub name: String,
/// Items in insertion order
pub items: IndexMap<String, Item>,
/// Named groups (hierarchy nodes)
pub groups: Vec<Group>,
/// Next item id counter
next_item_id: ItemId,
}
impl Category {
pub fn new(id: CategoryId, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
items: IndexMap::new(),
groups: Vec::new(),
next_item_id: 0,
}
}
pub fn add_item(&mut self, name: impl Into<String>) -> ItemId {
let name = name.into();
if let Some(item) = self.items.get(&name) {
return item.id;
}
let id = self.next_item_id;
self.next_item_id += 1;
self.items.insert(name.clone(), Item::new(id, name));
id
}
pub fn add_item_in_group(&mut self, name: impl Into<String>, group: impl Into<String>) -> ItemId {
let name = name.into();
let group = group.into();
if let Some(item) = self.items.get(&name) {
return item.id;
}
let id = self.next_item_id;
self.next_item_id += 1;
self.items.insert(name.clone(), Item::new(id, name).with_group(group));
id
}
pub fn add_group(&mut self, group: Group) {
if !self.groups.iter().any(|g| g.name == group.name) {
self.groups.push(group);
}
}
pub fn item_by_name(&self, name: &str) -> Option<&Item> {
self.items.get(name)
}
pub fn item_index(&self, name: &str) -> Option<usize> {
self.items.get_index_of(name)
}
/// Returns item names in order, grouped hierarchically
pub fn ordered_item_names(&self) -> Vec<&str> {
self.items.keys().map(|s| s.as_str()).collect()
}
/// Returns unique group names at the top level
pub fn top_level_groups(&self) -> Vec<&str> {
let mut seen = Vec::new();
for item in self.items.values() {
if let Some(g) = &item.group {
if !seen.contains(&g.as_str()) {
seen.push(g.as_str());
}
}
}
seen
}
}