Improve UX: welcome screen, vim keybindings, command mode

- Welcome overlay shown when model has no categories, listing common
  commands and navigation hints to orient new users
- Vim-style keybindings:
  - i / a → Insert mode (edit cell); Esc → Normal
  - x → clear cell; yy / p → yank / paste
  - G / gg → last / first row; 0 / $ → first / last col
  - Ctrl+D / Ctrl+U → half-page scroll
  - n / N → next / prev search match
  - T → tile-select mode (single key, no Ctrl needed)
  - ZZ → save + quit
  - F / C / V → toggle panels (no Ctrl needed)
  - ? → help (in addition to F1)
- Command mode (:) for vim-style commands:
  :q  :q!  :w [path]  :wq  ZZ
  :import <file.json>  :export [path]
  :add-cat <name>  :add-item <cat> <item>
  :formula <cat> <Name=expr>  :add-view [name]  :help
- Status bar now context-sensitive: shows mode-specific hint text
  instead of always showing the same generic shortcuts
- Mode label changed: "Editing" → "INSERT" to match vim convention
- Title bar shows filename in parentheses when model is backed by a file
- Help widget updated with full key reference in two-column layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ed L
2026-03-21 22:41:35 -07:00
parent eae00522e2
commit 66dfdf705f
5 changed files with 748 additions and 394 deletions

File diff suppressed because it is too large Load Diff

View File

@ -9,67 +9,83 @@ 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 popup_w = 66u16.min(area.width);
let popup_h = 36u16.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));
.title(" improvise — key reference (any key to close) ")
.border_style(Style::default().fg(Color::Blue));
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", ""),
let head = Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD);
let key = Style::default().fg(Color::Cyan);
let dim = Style::default().fg(Color::DarkGray);
let norm = Style::default();
// (key_col, desc_col, style)
let rows: &[(&str, &str, Style)] = &[
("Navigation", "", head),
(" hjkl / ↑↓←→", "Move cursor", key),
(" gg / G", "First / last row", key),
(" 0 / $", "First / last column", key),
(" Ctrl+D / Ctrl+U", "Scroll ½-page down / up", key),
(" [ / ]", "Cycle page-axis filter", key),
("", "", norm),
("Editing", "", head),
(" i / a / Enter", "Enter Insert mode", key),
(" Esc", "Return to Normal mode", key),
(" x", "Clear cell", key),
(" yy", "Yank (copy) cell value", key),
(" p", "Paste yanked value", key),
("", "", norm),
("Search", "", head),
(" /", "Enter search, highlight matches", key),
(" n / N", "Next / previous match", key),
(" Esc or Enter", "Exit search", key),
("", "", norm),
("Panels", "", head),
(" F", "Toggle Formula panel (n:new d:del)", key),
(" C", "Toggle Category panel (Space:cycle-axis)", key),
(" V", "Toggle View panel (n:new d:del Enter:switch)", key),
(" Tab", "Focus next open panel", key),
("", "", norm),
("Pivot / Tiles", "", head),
(" T", "Tile-select mode", key),
(" ← h / → l", "Select previous/next tile", dim),
(" Space / Enter", "Cycle axis (Row→Col→Page)", dim),
(" r / c / p", "Set axis to Row / Col / Page", dim),
("", "", norm),
("Command line ( : )", "", head),
(" :q :q! :wq ZZ", "Quit / force-quit / save+quit", key),
(" :w [path]", "Save (path optional)", key),
(" :import <path.json>", "Open JSON import wizard", key),
(" :export [path.csv]", "Export active view to CSV", key),
(" :add-cat <name>", "Add a category", key),
(" :add-item <cat> <item>", "Add an item to a category", key),
(" :formula <cat> <Name=expr>", "Add a formula", key),
(" :add-view [name]", "Create a new view", key),
("", "", norm),
(" ? or F1", "This help", key),
(" Ctrl+S", "Save (same as :w)", key),
];
for (i, (key, desc)) in help_text.iter().enumerate() {
let key_col_w = 32usize;
for (i, (k, d, style)) in rows.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));
if d.is_empty() {
buf.set_string(inner.x, y, k, *style);
} 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());
buf.set_string(inner.x, y, k, *style);
let dx = inner.x + key_col_w as u16;
if dx < inner.x + inner.width {
buf.set_string(dx, y, d, norm);
}
}
}