Compare commits

9 Commits

Author SHA1 Message Date
9fc3f0b5d6 refactor: synthesize previous refactors 2026-04-01 01:01:19 -07:00
3f84ba03cb Revert "refactor: mystery model 3"
This reverts commit 4b721f7543.
2026-04-01 00:46:55 -07:00
4b721f7543 refactor: mystery model 3 2026-04-01 00:46:25 -07:00
6d88de3020 Revert "refactor: mystery model #2"
This reverts commit 87fd6a1620.
2026-04-01 00:41:25 -07:00
87fd6a1620 refactor: mystery model #2 2026-04-01 00:40:22 -07:00
a57d3ed294 Revert "refactor: mystery model #1"
This reverts commit bbebc3344c.
2026-04-01 00:32:12 -07:00
bbebc3344c refactor: mystery model #1 2026-04-01 00:32:07 -07:00
ff08e3c2c2 chore: Revert refactors to give claude a clean slate 2026-04-01 00:26:55 -07:00
8c84256ebc refactor: merge using claude sonnet 2026-04-01 00:25:19 -07:00

View File

@ -277,165 +277,29 @@ fn run_tui(
// ── Drawing ────────────────────────────────────────────────────────────────── // ── Drawing ──────────────────────────────────────────────────────────────────
fn draw(f: &mut Frame, app: &App) { fn fill_line(left: String, right: &str, width: u16) -> String {
let size = f.area(); let pad = " ".repeat((width as usize).saturating_sub(left.len() + right.len()));
format!("{left}{pad}{right}")
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // title bar
Constraint::Min(0), // content
Constraint::Length(1), // tile bar
Constraint::Length(1), // status / command bar
])
.split(size);
draw_title(f, main_chunks[0], app);
draw_content(f, main_chunks[1], app);
draw_tile_bar(f, main_chunks[2], app);
draw_bottom_bar(f, main_chunks[3], app);
// Overlays (rendered last so they appear on top)
if matches!(app.mode, AppMode::Help) {
f.render_widget(HelpWidget, size);
}
if matches!(app.mode, AppMode::ImportWizard) {
if let Some(wizard) = &app.wizard {
f.render_widget(ImportWizardWidget::new(wizard), size);
}
}
if matches!(app.mode, AppMode::ExportPrompt { .. }) {
draw_export_prompt(f, app);
}
if app.is_empty_model() && matches!(app.mode, AppMode::Normal | AppMode::CommandMode { .. }) {
draw_welcome(f, main_chunks[1]);
}
} }
fn draw_title(f: &mut Frame, area: Rect, app: &App) { fn centered_popup(area: Rect, width: u16, height: u16) -> Rect {
let dirty = if app.dirty { " [+]" } else { "" }; let w = width.min(area.width);
let file = app.file_path.as_ref().and_then(|p| p.file_name()).and_then(|n| n.to_str()).map(|n| format!(" ({n})")).unwrap_or_default(); let h = height.min(area.height);
let title = format!(" improvise · {}{}{} ", app.model.name, file, dirty);
let right = " ?:help :q quit ";
let pad = " ".repeat((area.width as usize).saturating_sub(title.len() + right.len()));
let line = format!("{title}{pad}{right}");
f.render_widget(Paragraph::new(line).style(Style::default().fg(Color::Black).bg(Color::Blue).add_modifier(Modifier::BOLD)), area);
}
fn draw_content(f: &mut Frame, area: Rect, app: &App) {
if app.formula_panel_open || app.category_panel_open || app.view_panel_open {
let side_w = 32u16;
let chunks = Layout::default().direction(Direction::Horizontal).constraints([Constraint::Min(40), Constraint::Length(side_w)]).split(area);
f.render_widget(GridWidget::new(&app.model, &app.mode, &app.search_query), chunks[0]);
let side = chunks[1];
let open_panels = [app.formula_panel_open, app.category_panel_open, app.view_panel_open];
let panel_count = open_panels.iter().filter(|&&b| b).count() as u16;
let ph = side.height / panel_count.max(1);
let mut y = side.y;
if app.formula_panel_open {
f.render_widget(FormulaPanel::new(&app.model, &app.mode, app.formula_cursor), Rect::new(side.x, y, side.width, ph));
y += ph;
}
if app.category_panel_open {
f.render_widget(CategoryPanel::new(&app.model, &app.mode, app.cat_panel_cursor), Rect::new(side.x, y, side.width, ph));
y += ph;
}
if app.view_panel_open {
f.render_widget(ViewPanel::new(&app.model, &app.mode, app.view_panel_cursor), Rect::new(side.x, y, side.width, ph));
}
} else {
f.render_widget(GridWidget::new(&app.model, &app.mode, &app.search_query), area);
}
}
fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(TileBar::new(&app.model, &app.mode), area);
}
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
match app.mode {
AppMode::CommandMode { ref buffer } => draw_command_bar(f, area, buffer),
_ => draw_status(f, area, app),
}
}
fn draw_status(f: &mut Frame, area: Rect, app: &App) {
let mode_badge = mode_name(&app.mode);
let search_part = if app.search_mode { format!(" /{}", app.search_query) } else { String::new() };
let msg = if !app.status_msg.is_empty() { app.status_msg.as_str() } else { app.hint_text() };
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
let view_badge = format!(" {}{} ", app.model.active_view, yank_indicator);
let left = format!(" {mode_badge}{search_part} {msg}");
let right = view_badge;
let pad = " ".repeat((area.width as usize).saturating_sub(left.len() + right.len()));
let line = format!("{left}{pad}{right}");
let badge_style = mode_style(&app.mode);
f.render_widget(Paragraph::new(line).style(badge_style), area);
}
fn draw_command_bar(f: &mut Frame, area: Rect, buffer: &str) {
f.render_widget(Paragraph::new(format!(":{buffer}")).style(Style::default().fg(Color::White).bg(Color::Black)), area);
}
fn draw_export_prompt(f: &mut Frame, app: &App) {
let area = f.area();
let popup_w = 64u16.min(area.width);
let x = area.x + area.width.saturating_sub(popup_w) / 2;
let y = area.y + area.height / 2;
let popup_area = Rect::new(x, y, popup_w, 3);
f.render_widget(Clear, popup_area);
let block = Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Yellow)).title(" Export CSV — path (Esc cancel) ");
let inner = block.inner(popup_area);
f.render_widget(block, popup_area);
let buf = if let AppMode::ExportPrompt { buffer } = &app.mode { buffer } else { "" };
f.render_widget(Paragraph::new(format!("{buf}")).style(Style::default().fg(Color::Green)), inner);
}
fn draw_welcome(f: &mut Frame, area: Rect) {
let w = 58u16.min(area.width.saturating_sub(4));
let h = 20u16.min(area.height.saturating_sub(2));
let x = area.x + area.width.saturating_sub(w) / 2; let x = area.x + area.width.saturating_sub(w) / 2;
let y = area.y + area.height.saturating_sub(h) / 2; let y = area.y + area.height.saturating_sub(h) / 2;
let popup = Rect::new(x, y, w, h); Rect::new(x, y, w, h)
f.render_widget(Clear, popup);
let block = Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Blue)).title(" Welcome to improvise ");
let inner = block.inner(popup);
f.render_widget(block, popup);
let lines: &[(&str, Style)] = &[
("Multi-dimensional data modeling — in your terminal.", Style::default().fg(Color::White)),
("", Style::default()),
("Getting started", Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)),
("", Style::default()),
(":import <file.json> Import a JSON file", Style::default().fg(Color::Cyan)),
(":add-cat <name> Add a category (dimension)", Style::default().fg(Color::Cyan)),
(":add-item <cat> <name> Add an item to a category", Style::default().fg(Color::Cyan)),
(":formula <cat> <expr> Add a formula, e.g.:", Style::default().fg(Color::Cyan)),
(" Profit = Revenue - Cost", Style::default().fg(Color::Green)),
(":w <file.improv> Save your model", Style::default().fg(Color::Cyan)),
("", Style::default()),
("Navigation", Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)),
("", Style::default()),
("F C V Open panels (Formulas/Categories/Views)", Style::default()),
("T Tile-select: pivot rows ↔ cols ↔ page", Style::default()),
("i Enter Edit a cell", Style::default()),
("[ ] Cycle the page-axis filter", Style::default()),
("? or :help Full key reference", Style::default()),
(":q Quit", Style::default()),
];
for (i, (text, style)) in lines.iter().enumerate() {
if i >= inner.height as usize { break; }
f.render_widget(Paragraph::new(*text).style(*style), Rect::new(inner.x + 1, inner.y + i as u16, inner.width.saturating_sub(2), 1));
}
} }
// ── Helpers ────────────────────────────────────────────────────────────────── fn draw_popup_frame(f: &mut Frame, popup: Rect, title: &str, border_color: Color) -> Rect {
f.render_widget(Clear, popup);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.title(title);
let inner = block.inner(popup);
f.render_widget(block, popup);
inner
}
fn mode_name(mode: &AppMode) -> &'static str { fn mode_name(mode: &AppMode) -> &'static str {
match mode { match mode {
@ -465,4 +329,258 @@ fn mode_style(mode: &AppMode) -> Style {
} }
} }
fn draw(f: &mut Frame, app: &App) {
let size = f.area();
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // title bar
Constraint::Min(0), // content
Constraint::Length(1), // tile bar
Constraint::Length(1), // status / command bar
])
.split(size);
draw_title(f, main_chunks[0], app);
draw_content(f, main_chunks[1], app);
draw_tile_bar(f, main_chunks[2], app);
draw_bottom_bar(f, main_chunks[3], app);
// Overlays (rendered last so they appear on top)
if matches!(app.mode, AppMode::Help) {
f.render_widget(HelpWidget, size);
}
if matches!(app.mode, AppMode::ImportWizard) {
if let Some(wizard) = &app.wizard {
f.render_widget(ImportWizardWidget::new(wizard), size);
}
}
if matches!(app.mode, AppMode::ExportPrompt { .. }) {
draw_export_prompt(f, size, app);
}
if app.is_empty_model() && matches!(app.mode, AppMode::Normal | AppMode::CommandMode { .. }) {
draw_welcome(f, main_chunks[1]);
}
}
fn draw_title(f: &mut Frame, area: Rect, app: &App) {
let dirty = if app.dirty { " [+]" } else { "" };
let file = app
.file_path
.as_ref()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(|n| format!(" ({n})"))
.unwrap_or_default();
let title = format!(" improvise · {}{}{} ", app.model.name, file, dirty);
let right = " ?:help :q quit ";
let line = fill_line(title, right, area.width);
f.render_widget(
Paragraph::new(line).style(
Style::default()
.fg(Color::Black)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
area,
);
}
fn draw_content(f: &mut Frame, area: Rect, app: &App) {
let side_open = app.formula_panel_open || app.category_panel_open || app.view_panel_open;
let grid_area;
if side_open {
let side_w = 32u16;
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(40), Constraint::Length(side_w)])
.split(area);
grid_area = chunks[0];
let side = chunks[1];
let panel_count = [
app.formula_panel_open,
app.category_panel_open,
app.view_panel_open,
]
.iter()
.filter(|&&b| b)
.count() as u16;
let ph = side.height / panel_count.max(1);
let mut y = side.y;
if app.formula_panel_open {
let a = Rect::new(side.x, y, side.width, ph);
f.render_widget(
FormulaPanel::new(&app.model, &app.mode, app.formula_cursor),
a,
);
y += ph;
}
if app.category_panel_open {
let a = Rect::new(side.x, y, side.width, ph);
f.render_widget(
CategoryPanel::new(&app.model, &app.mode, app.cat_panel_cursor),
a,
);
y += ph;
}
if app.view_panel_open {
let a = Rect::new(side.x, y, side.width, ph);
f.render_widget(
ViewPanel::new(&app.model, &app.mode, app.view_panel_cursor),
a,
);
}
} else {
grid_area = area;
}
f.render_widget(
GridWidget::new(&app.model, &app.mode, &app.search_query),
grid_area,
);
}
fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(TileBar::new(&app.model, &app.mode), area);
}
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
match app.mode {
AppMode::CommandMode { ref buffer } => draw_command_bar(f, area, buffer),
_ => draw_status(f, area, app),
}
}
fn draw_status(f: &mut Frame, area: Rect, app: &App) {
let search_part = if app.search_mode {
format!(" /{}", app.search_query)
} else {
String::new()
};
let msg = if !app.status_msg.is_empty() {
app.status_msg.as_str()
} else {
app.hint_text()
};
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
let view_badge = format!(" {}{} ", app.model.active_view, yank_indicator);
let left = format!(" {}{search_part} {msg}", mode_name(&app.mode));
let line = fill_line(left, &view_badge, area.width);
f.render_widget(Paragraph::new(line).style(mode_style(&app.mode)), area);
}
fn draw_command_bar(f: &mut Frame, area: Rect, buffer: &str) {
f.render_widget(
Paragraph::new(format!(":{buffer}"))
.style(Style::default().fg(Color::White).bg(Color::Black)),
area,
);
}
fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
let buf = if let AppMode::ExportPrompt { buffer } = &app.mode {
buffer.as_str()
} else {
""
};
let popup = centered_popup(area, 64, 3);
let inner = draw_popup_frame(f, popup, " Export CSV — path (Esc cancel) ", Color::Yellow);
f.render_widget(
Paragraph::new(format!("{buf}")).style(Style::default().fg(Color::Green)),
inner,
);
}
fn draw_welcome(f: &mut Frame, area: Rect) {
let popup = centered_popup(area, 58, 20);
let inner = draw_popup_frame(f, popup, " Welcome to improvise ", Color::Blue);
let lines: &[(&str, Style)] = &[
(
"Multi-dimensional data modeling — in your terminal.",
Style::default().fg(Color::White),
),
("", Style::default()),
(
"Getting started",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
("", Style::default()),
(
":import <file.json> Import a JSON file",
Style::default().fg(Color::Cyan),
),
(
":add-cat <name> Add a category (dimension)",
Style::default().fg(Color::Cyan),
),
(
":add-item <cat> <name> Add an item to a category",
Style::default().fg(Color::Cyan),
),
(
":formula <cat> <expr> Add a formula, e.g.:",
Style::default().fg(Color::Cyan),
),
(
" Profit = Revenue - Cost",
Style::default().fg(Color::Green),
),
(
":w <file.improv> Save your model",
Style::default().fg(Color::Cyan),
),
("", Style::default()),
(
"Navigation",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
("", Style::default()),
(
"F C V Open panels (Formulas/Categories/Views)",
Style::default(),
),
(
"T Tile-select: pivot rows ↔ cols ↔ page",
Style::default(),
),
("i Enter Edit a cell", Style::default()),
(
"[ ] Cycle the page-axis filter",
Style::default(),
),
(
"? or :help Full key reference",
Style::default(),
),
(":q Quit", Style::default()),
];
for (i, (text, style)) in lines.iter().enumerate() {
if i >= inner.height as usize {
break;
}
f.render_widget(
Paragraph::new(*text).style(*style),
Rect::new(
inner.x + 1,
inner.y + i as u16,
inner.width.saturating_sub(2),
1,
),
);
}
}