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:
121
src/model/category.rs
Normal file
121
src/model/category.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user