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:
82
src/ui/tile_bar.rs
Normal file
82
src/ui/tile_bar.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::view::Axis;
|
||||
use crate::ui::app::AppMode;
|
||||
|
||||
pub struct TileBar<'a> {
|
||||
pub model: &'a Model,
|
||||
pub mode: &'a AppMode,
|
||||
}
|
||||
|
||||
impl<'a> TileBar<'a> {
|
||||
pub fn new(model: &'a Model, mode: &'a AppMode) -> Self {
|
||||
Self { model, mode }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for TileBar<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let view = match self.model.active_view() {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let selected_cat_idx = if let AppMode::TileSelect { cat_idx } = self.mode {
|
||||
Some(*cat_idx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut x = area.x + 1;
|
||||
buf.set_string(area.x, area.y, " Tiles: ", Style::default().fg(Color::Gray));
|
||||
x += 8;
|
||||
|
||||
let cat_names: Vec<&str> = self.model.category_names();
|
||||
for (i, cat_name) in cat_names.iter().enumerate() {
|
||||
let axis = view.axis_of(cat_name);
|
||||
let axis_symbol = match axis {
|
||||
Axis::Row => "↕",
|
||||
Axis::Column => "↔",
|
||||
Axis::Page => "☰",
|
||||
Axis::Unassigned => "─",
|
||||
};
|
||||
|
||||
let label = format!(" [{cat_name} {axis_symbol}] ");
|
||||
let is_selected = selected_cat_idx == Some(i);
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
match axis {
|
||||
Axis::Row => Style::default().fg(Color::Green),
|
||||
Axis::Column => Style::default().fg(Color::Blue),
|
||||
Axis::Page => Style::default().fg(Color::Magenta),
|
||||
Axis::Unassigned => Style::default().fg(Color::DarkGray),
|
||||
}
|
||||
};
|
||||
|
||||
if x + label.len() as u16 > area.x + area.width { break; }
|
||||
buf.set_string(x, area.y, &label, style);
|
||||
x += label.len() as u16;
|
||||
}
|
||||
|
||||
// Hint
|
||||
if matches!(self.mode, AppMode::TileSelect { .. }) {
|
||||
let hint = " [Enter] cycle axis [r/c/p] set axis [←→] select [Esc] cancel";
|
||||
if x + hint.len() as u16 <= area.x + area.width {
|
||||
buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray));
|
||||
}
|
||||
} else {
|
||||
let hint = " Ctrl+↑↓←→ to move tiles";
|
||||
if x + hint.len() as u16 <= area.x + area.width {
|
||||
buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user