From 7c00695398a1e5b91c3cda3a41a4d8f5e0e79c9b Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Wed, 15 Apr 2026 21:32:34 -0700 Subject: [PATCH] refactor(navigation): include AppMode in view navigation stack Introduce `ViewFrame` to store both the view name and the `AppMode` when pushing to the navigation stack. Update `view_back_stack` and `view_forward_stack` to use `ViewFrame` instead of `String` . Update `CmdContext` and `Effect` implementations (SwitchView, ViewBack, ViewForward) to handle the new `ViewFrame` structure. Add `is_editing()` helper to `AppMode` . Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-26B-A4B-it-UD-Q5_K_XL.gguf) --- src/command/cmd/core.rs | 4 ++-- src/command/cmd/grid.rs | 10 ++++++++-- src/ui/app.rs | 16 ++++++++++++++-- src/ui/effect.rs | 37 +++++++++++++++++++++++++------------ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/command/cmd/core.rs b/src/command/cmd/core.rs index d9d2742..63c795c 100644 --- a/src/command/cmd/core.rs +++ b/src/command/cmd/core.rs @@ -34,8 +34,8 @@ pub struct CmdContext<'a> { /// Named text buffers pub buffers: &'a HashMap, /// View navigation stacks (for drill back/forward) - pub view_back_stack: &'a [String], - pub view_forward_stack: &'a [String], + pub view_back_stack: &'a [crate::ui::app::ViewFrame], + pub view_forward_stack: &'a [crate::ui::app::ViewFrame], /// Whether the app currently has an active drill snapshot. pub has_drill_state: bool, /// Display value at the cursor — works uniformly for pivot and records mode. diff --git a/src/command/cmd/grid.rs b/src/command/cmd/grid.rs index a0176ab..178b58d 100644 --- a/src/command/cmd/grid.rs +++ b/src/command/cmd/grid.rs @@ -67,7 +67,10 @@ mod tests { let m = two_cat_model(); let layout = make_layout(&m); let reg = make_registry(); - let fwd_stack = vec!["View 2".to_string()]; + let fwd_stack = vec![crate::ui::app::ViewFrame { + view_name: "View 2".to_string(), + mode: crate::ui::app::AppMode::Normal, + }]; let mut ctx = make_ctx(&m, &layout, ®); ctx.view_forward_stack = &fwd_stack; let cmd = ViewNavigate { forward: true }; @@ -84,7 +87,10 @@ mod tests { let m = two_cat_model(); let layout = make_layout(&m); let reg = make_registry(); - let back_stack = vec!["Default".to_string()]; + let back_stack = vec![crate::ui::app::ViewFrame { + view_name: "Default".to_string(), + mode: crate::ui::app::AppMode::Normal, + }]; let mut ctx = make_ctx(&m, &layout, ®); ctx.view_back_stack = &back_stack; let cmd = ViewNavigate { forward: false }; diff --git a/src/ui/app.rs b/src/ui/app.rs index 1d20ca7..1d75bcc 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -20,6 +20,13 @@ use crate::ui::grid::{ }; use crate::view::GridLayout; +/// A saved view+mode pair for the navigation stack. +#[derive(Debug, Clone, PartialEq)] +pub struct ViewFrame { + pub view_name: String, + pub mode: AppMode, +} + /// Drill-down state: frozen record snapshot + pending edits that have not /// yet been applied to the model. #[derive(Debug, Clone, Default)] @@ -88,6 +95,11 @@ impl AppMode { } } + /// True for any cell-editing mode (normal or records). + pub fn is_editing(&self) -> bool { + matches!(self, Self::Editing { .. } | Self::RecordsEditing { .. }) + } + pub fn editing() -> Self { Self::Editing { minibuf: MinibufferConfig { @@ -173,9 +185,9 @@ pub struct App { pub tile_cat_idx: usize, /// View navigation history: views visited before the current one. /// Pushed on SwitchView, popped by `<` (back). - pub view_back_stack: Vec, + pub view_back_stack: Vec, /// Views that were "back-ed" from, available for forward navigation (`>`). - pub view_forward_stack: Vec, + pub view_forward_stack: Vec, /// Frozen records list for the drill view. When present, this is the /// snapshot that records-mode layouts iterate — records don't disappear /// when filters would change. Pending edits are stored alongside and diff --git a/src/ui/effect.rs b/src/ui/effect.rs index adfca98..ae2d801 100644 --- a/src/ui/effect.rs +++ b/src/ui/effect.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use crate::model::cell::{CellKey, CellValue}; use crate::view::Axis; -use super::app::{App, AppMode}; +use super::app::{App, AppMode, ViewFrame}; pub(crate) const RECORD_COORDS_CANNOT_BE_EMPTY: &str = "Record coordinates cannot be empty"; @@ -193,7 +193,10 @@ impl Effect for SwitchView { fn apply(&self, app: &mut App) { let current = app.model.active_view.clone(); if current != self.0 { - app.view_back_stack.push(current); + app.view_back_stack.push(ViewFrame { + view_name: current, + mode: app.mode.clone(), + }); app.view_forward_stack.clear(); } let _ = app.model.switch_view(&self.0); @@ -205,10 +208,14 @@ impl Effect for SwitchView { pub struct ViewBack; impl Effect for ViewBack { fn apply(&self, app: &mut App) { - if let Some(prev) = app.view_back_stack.pop() { + if let Some(frame) = app.view_back_stack.pop() { let current = app.model.active_view.clone(); - app.view_forward_stack.push(current); - let _ = app.model.switch_view(&prev); + app.view_forward_stack.push(ViewFrame { + view_name: current, + mode: app.mode.clone(), + }); + let _ = app.model.switch_view(&frame.view_name); + app.mode = frame.mode; } } } @@ -218,10 +225,14 @@ impl Effect for ViewBack { pub struct ViewForward; impl Effect for ViewForward { fn apply(&self, app: &mut App) { - if let Some(next) = app.view_forward_stack.pop() { + if let Some(frame) = app.view_forward_stack.pop() { let current = app.model.active_view.clone(); - app.view_back_stack.push(current); - let _ = app.model.switch_view(&next); + app.view_back_stack.push(ViewFrame { + view_name: current, + mode: app.mode.clone(), + }); + let _ = app.model.switch_view(&frame.view_name); + app.mode = frame.mode; } } } @@ -1134,8 +1145,8 @@ mod tests { SwitchView("View 2".to_string()).apply(&mut app); assert_eq!(app.model.active_view.as_str(), "View 2"); - assert_eq!(app.view_back_stack, vec!["Default".to_string()]); - // Forward stack should be cleared + assert_eq!(app.view_back_stack.len(), 1); + assert_eq!(app.view_back_stack[0].view_name, "Default"); assert!(app.view_forward_stack.is_empty()); } @@ -1156,13 +1167,15 @@ mod tests { // Go back ViewBack.apply(&mut app); assert_eq!(app.model.active_view.as_str(), "Default"); - assert_eq!(app.view_forward_stack, vec!["View 2".to_string()]); + assert_eq!(app.view_forward_stack.len(), 1); + assert_eq!(app.view_forward_stack[0].view_name, "View 2"); assert!(app.view_back_stack.is_empty()); // Go forward ViewForward.apply(&mut app); assert_eq!(app.model.active_view.as_str(), "View 2"); - assert_eq!(app.view_back_stack, vec!["Default".to_string()]); + assert_eq!(app.view_back_stack.len(), 1); + assert_eq!(app.view_back_stack[0].view_name, "Default"); assert!(app.view_forward_stack.is_empty()); }