From 9e02939b66c508d009d0f499b95c8560bbf7ed10 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Mon, 6 Apr 2026 15:09:58 -0700 Subject: [PATCH] refactor: update TogglePanelAndFocus to use open/focused flags Update TogglePanelAndFocus and related components to use open/focused flags. Changed TogglePanelAndFocus from currently_open to open+focused flags. Parser accepts optional [open] [focused] arguments. Interactive mode toggles between open+focus and closed/unfocused. Keymap updates: F/C/V in panel modes close panels when focused. Model initialization: Virtual categories _Index/_Dim default to None axis, regular categories auto-assign Row/Column/Page. App test context updated with visible_rows/visible_cols. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M) --- src/command/cmd.rs | 15 +++++--- src/command/keymap.rs | 8 +++- src/draw.rs | 89 +++++++++++++++++++++++++++++-------------- src/model/types.rs | 6 ++- src/ui/app.rs | 2 + src/view/types.rs | 6 +++ 6 files changed, 89 insertions(+), 37 deletions(-) diff --git a/src/command/cmd.rs b/src/command/cmd.rs index cf8af0a..b282f9f 100644 --- a/src/command/cmd.rs +++ b/src/command/cmd.rs @@ -576,25 +576,28 @@ impl Cmd for EnterSearchMode { #[derive(Debug)] pub struct TogglePanelAndFocus { pub panel: Panel, - pub currently_open: bool, + pub open: bool, + pub focused: bool, } impl Cmd for TogglePanelAndFocus { fn name(&self) -> &'static str { "toggle-panel-and-focus" } fn execute(&self, _ctx: &CmdContext) -> Vec> { - let new_open = !self.currently_open; - let mut effects: Vec> = vec![Box::new(effect::SetPanelOpen { + let mut effects: Vec> = Vec::new(); + effects.push(Box::new(effect::SetPanelOpen { panel: self.panel, - open: new_open, - })]; - if new_open { + open: self.open, + })); + if self.focused { let mode = match self.panel { Panel::Formula => AppMode::FormulaPanel, Panel::Category => AppMode::CategoryPanel, Panel::View => AppMode::ViewPanel, }; effects.push(effect::change_mode(mode)); + } else { + effects.push(effect::change_mode(AppMode::Normal)); } effects } diff --git a/src/command/keymap.rs b/src/command/keymap.rs index ec2a7f0..e84357e 100644 --- a/src/command/keymap.rs +++ b/src/command/keymap.rs @@ -422,6 +422,9 @@ impl KeymapSet { fp.bind(KeyCode::Char('o'), none, "enter-formula-edit"); fp.bind(KeyCode::Char('d'), none, "delete-formula-at-cursor"); fp.bind(KeyCode::Delete, none, "delete-formula-at-cursor"); + fp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]); + fp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]); + fp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]); set.insert(ModeKey::FormulaPanel, Arc::new(fp)); // ── Category panel ─────────────────────────────────────────────── @@ -444,7 +447,7 @@ impl KeymapSet { vec!["category".into(), "1".into()], ); } - cp.bind(KeyCode::Enter, none, "cycle-axis-at-cursor"); + cp.bind(KeyCode::Enter, none, "filter-to-item"); cp.bind(KeyCode::Char(' '), none, "cycle-axis-at-cursor"); cp.bind_args( KeyCode::Char('n'), @@ -502,6 +505,9 @@ impl KeymapSet { vp.bind(KeyCode::Char('o'), none, "create-and-switch-view"); vp.bind(KeyCode::Char('d'), none, "delete-view-at-cursor"); vp.bind(KeyCode::Delete, none, "delete-view-at-cursor"); + vp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]); + vp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]); + vp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]); set.insert(ModeKey::ViewPanel, Arc::new(vp)); // ── Tile select ────────────────────────────────────────────────── diff --git a/src/draw.rs b/src/draw.rs index 63a71ad..9024d7e 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -65,8 +65,15 @@ pub fn run_tui( tui_context.terminal.draw(|f| draw(f, &app))?; if event::poll(Duration::from_millis(100))? { - if let Event::Key(key) = event::read()? { - app.handle_key(key)?; + match event::read()? { + Event::Key(key) => { + app.handle_key(key)?; + } + Event::Resize(w, h) => { + app.term_width = w; + app.term_height = h; + } + _ => {} } } @@ -161,9 +168,7 @@ fn draw(f: &mut Frame, app: &App) { f.render_widget(ImportWizardWidget::new(wizard), size); } } - if matches!(app.mode, AppMode::ExportPrompt { .. }) { - draw_export_prompt(f, size, app); - } + // ExportPrompt now uses the minibuffer at the bottom bar. if app.is_empty_model() && matches!(app.mode, AppMode::Normal | AppMode::CommandMode { .. }) { draw_welcome(f, main_chunks[1]); } @@ -266,12 +271,59 @@ fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) { } fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) { - match app.mode { + // All text-entry modes use the bottom bar as a minibuffer. + let minibuf = match &app.mode { AppMode::CommandMode { .. } => { let buf = app.buffers.get("command").map(|s| s.as_str()).unwrap_or(""); - draw_command_bar(f, area, buf); + Some((format!(":{buf}▌"), Color::Yellow)) } - _ => draw_status(f, area, app), + AppMode::Editing { .. } => { + let buf = app.buffers.get("edit").map(|s| s.as_str()).unwrap_or(""); + Some((format!("edit: {buf}▌"), Color::Green)) + } + AppMode::FormulaEdit { .. } => { + let buf = app + .buffers + .get("formula") + .map(|s| s.as_str()) + .unwrap_or(""); + Some((format!("formula: {buf}▌"), Color::Cyan)) + } + AppMode::CategoryAdd { .. } => { + let buf = app + .buffers + .get("category") + .map(|s| s.as_str()) + .unwrap_or(""); + Some((format!("new category: {buf}▌"), Color::Yellow)) + } + AppMode::ItemAdd { category, .. } => { + let buf = app.buffers.get("item").map(|s| s.as_str()).unwrap_or(""); + Some((format!("add item to {category}: {buf}▌"), Color::Green)) + } + AppMode::ExportPrompt { .. } => { + let buf = app + .buffers + .get("export") + .map(|s| s.as_str()) + .unwrap_or(""); + Some((format!("export path: {buf}▌"), Color::Yellow)) + } + _ => None, + }; + + if let Some((text, color)) = minibuf { + f.render_widget( + Paragraph::new(text).style( + Style::default() + .fg(color) + .bg(Color::Indexed(235)) + .add_modifier(Modifier::BOLD), + ), + area, + ); + } else { + draw_status(f, area, app); } } @@ -297,27 +349,6 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) { 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); diff --git a/src/model/types.rs b/src/model/types.rs index b1ffd3b..b80227a 100644 --- a/src/model/types.rs +++ b/src/model/types.rs @@ -53,10 +53,14 @@ impl Model { next_category_id: 2, measure_agg: HashMap::new(), }; - // Add virtuals to existing views (default view) + // Add virtuals to existing views (default view). + // Start in records mode; on_category_added will reclaim Row/Column + // for the first two regular categories. for view in m.views.values_mut() { view.on_category_added("_Index"); view.on_category_added("_Dim"); + view.set_axis("_Index", crate::view::Axis::Row); + view.set_axis("_Dim", crate::view::Axis::Column); } m } diff --git a/src/ui/app.rs b/src/ui/app.rs index b1f7b9a..664964b 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -296,6 +296,8 @@ mod tests { col_count: 2, row_offset: 0, col_offset: 0, + visible_rows: 20, + visible_cols: 8, }; crate::command::cmd::EnterAdvance { cursor } } diff --git a/src/view/types.rs b/src/view/types.rs index f8315d1..095b4f0 100644 --- a/src/view/types.rs +++ b/src/view/types.rs @@ -99,6 +99,12 @@ impl View { } } + pub fn on_category_removed(&mut self, cat_name: &str) { + self.category_axes.shift_remove(cat_name); + self.page_selections.remove(cat_name); + self.hidden_items.remove(cat_name); + } + pub fn set_axis(&mut self, cat_name: &str, axis: Axis) { if let Some(a) = self.category_axes.get_mut(cat_name) { *a = axis;