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

77
src/ui/help.rs Normal file
View File

@ -0,0 +1,77 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, Borders, Clear, Widget},
};
pub struct HelpWidget;
impl Widget for HelpWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
// Center popup
let popup_w = 60u16.min(area.width);
let popup_h = 30u16.min(area.height);
let x = area.x + area.width.saturating_sub(popup_w) / 2;
let y = area.y + area.height.saturating_sub(popup_h) / 2;
let popup_area = Rect::new(x, y, popup_w, popup_h);
Clear.render(popup_area, buf);
let block = Block::default()
.borders(Borders::ALL)
.title(" Help — Improvise ")
.border_style(Style::default().fg(Color::Yellow));
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let help_text = [
("Navigation", ""),
(" ↑/↓/←/→ or hjkl", "Move cursor"),
(" Enter", "Edit selected cell"),
(" /", "Search in grid"),
(" [ / ]", "Prev/next page item"),
("", ""),
("Panels", ""),
(" Ctrl+F", "Toggle formula panel"),
(" Ctrl+C", "Toggle category panel"),
(" Ctrl+V", "Toggle view panel"),
(" Tab", "Focus next open panel"),
("", ""),
("Tiles / Pivot", ""),
(" Ctrl+Arrow", "Enter tile select mode"),
(" Enter/Space", "Cycle axis (Row→Col→Page)"),
(" r / c / p", "Set axis to Row/Col/Page"),
("", ""),
("File", ""),
(" Ctrl+S", "Save model"),
(" Ctrl+E", "Export CSV"),
("", ""),
("Headless / Batch", ""),
(" --cmd '{...}'", "Run a single JSON command"),
(" --script file", "Run commands from file"),
("", ""),
(" F1", "This help"),
(" Ctrl+Q", "Quit"),
("", ""),
(" Any key to close", ""),
];
for (i, (key, desc)) in help_text.iter().enumerate() {
if i >= inner.height as usize { break; }
let y = inner.y + i as u16;
if key.is_empty() {
continue;
}
if desc.is_empty() {
buf.set_string(inner.x, y, key, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
} else {
buf.set_string(inner.x, y, key, Style::default().fg(Color::Cyan));
let desc_x = inner.x + 26;
if desc_x < inner.x + inner.width {
buf.set_string(desc_x, y, desc, Style::default());
}
}
}
}
}