refactor(ui): move UI session fields into ViewState (improvise-ew0)
Step 3 of vb4. Populates ViewState with the 20 UI session fields (mode, status_msg, wizard, search_query, search_mode, three panel-open flags, three panel cursors, formula_cursor, yanked, tile_cat_idx, two view nav stacks, drill_state, help_page, expanded_cats, buffers, transient_keymap) and routes every read/write site through app.view_state.X. App now contains only model_state, view_state, and the runtime/derived residue (term dims, layout, last_autosave, abort_effects, keymap_set). ViewState gets a manual Default impl mirroring the previous App::new field initialisers; AppMode has no Default of its own so AppMode::Normal is the explicit baseline. Effect::apply still takes &mut App; narrowing remains step 5 (improvise-drg). A structural test (app_view_state_owns_ui_session_fields) locks in the 20-field layout. 624 tests pass workspace-wide (+1 new). cargo clippy --workspace --tests clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+149
-112
@@ -206,17 +206,12 @@ impl Default for ModelState {
|
||||
}
|
||||
|
||||
/// UI session-state slice: mode, cursors, panels, buffers, navigation stacks,
|
||||
/// and other per-session state that does not persist to disk. Filled in by
|
||||
/// improvise-ew0 (vb4 step 3).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ViewState {}
|
||||
|
||||
pub struct App {
|
||||
pub model_state: ModelState,
|
||||
/// and other per-session state that does not persist to disk.
|
||||
#[derive(Debug)]
|
||||
pub struct ViewState {
|
||||
pub mode: AppMode,
|
||||
pub status_msg: String,
|
||||
pub wizard: Option<ImportWizard>,
|
||||
pub last_autosave: Instant,
|
||||
pub search_query: String,
|
||||
pub search_mode: bool,
|
||||
pub formula_panel_open: bool,
|
||||
@@ -241,15 +236,48 @@ pub struct App {
|
||||
pub drill_state: Option<DrillState>,
|
||||
/// Current page index in the Help screen (0-based).
|
||||
pub help_page: usize,
|
||||
/// Terminal dimensions (updated on resize and at startup).
|
||||
pub term_width: u16,
|
||||
pub term_height: u16,
|
||||
/// Categories expanded in the category panel tree view.
|
||||
pub expanded_cats: std::collections::HashSet<String>,
|
||||
/// Named text buffers for text-entry modes
|
||||
pub buffers: HashMap<String, String>,
|
||||
/// Transient keymap for Emacs-style prefix key sequences (g→gg, y→yy, etc.)
|
||||
pub transient_keymap: Option<Arc<Keymap>>,
|
||||
}
|
||||
|
||||
impl Default for ViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: AppMode::Normal,
|
||||
status_msg: String::new(),
|
||||
wizard: None,
|
||||
search_query: String::new(),
|
||||
search_mode: false,
|
||||
formula_panel_open: false,
|
||||
category_panel_open: false,
|
||||
view_panel_open: false,
|
||||
cat_panel_cursor: 0,
|
||||
view_panel_cursor: 0,
|
||||
formula_cursor: 0,
|
||||
yanked: None,
|
||||
tile_cat_idx: 0,
|
||||
view_back_stack: Vec::new(),
|
||||
view_forward_stack: Vec::new(),
|
||||
drill_state: None,
|
||||
help_page: 0,
|
||||
expanded_cats: std::collections::HashSet::new(),
|
||||
buffers: HashMap::new(),
|
||||
transient_keymap: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub model_state: ModelState,
|
||||
pub view_state: ViewState,
|
||||
pub last_autosave: Instant,
|
||||
/// Terminal dimensions (updated on resize and at startup).
|
||||
pub term_width: u16,
|
||||
pub term_height: u16,
|
||||
/// Current grid layout, derived from model + view + drill_state.
|
||||
/// Rebuilt via `rebuild_layout()` after state changes.
|
||||
pub layout: GridLayout,
|
||||
@@ -280,29 +308,10 @@ impl App {
|
||||
file_path,
|
||||
dirty: false,
|
||||
},
|
||||
mode: AppMode::Normal,
|
||||
status_msg: String::new(),
|
||||
wizard: None,
|
||||
view_state: ViewState::default(),
|
||||
last_autosave: Instant::now(),
|
||||
search_query: String::new(),
|
||||
search_mode: false,
|
||||
formula_panel_open: false,
|
||||
category_panel_open: false,
|
||||
view_panel_open: false,
|
||||
cat_panel_cursor: 0,
|
||||
view_panel_cursor: 0,
|
||||
formula_cursor: 0,
|
||||
yanked: None,
|
||||
tile_cat_idx: 0,
|
||||
view_back_stack: Vec::new(),
|
||||
view_forward_stack: Vec::new(),
|
||||
drill_state: None,
|
||||
help_page: 0,
|
||||
term_width: crossterm::terminal::size().map(|(w, _)| w).unwrap_or(80),
|
||||
term_height: crossterm::terminal::size().map(|(_, h)| h).unwrap_or(24),
|
||||
expanded_cats: std::collections::HashSet::new(),
|
||||
buffers: HashMap::new(),
|
||||
transient_keymap: None,
|
||||
layout,
|
||||
abort_effects: false,
|
||||
keymap_set: KeymapSet::default_keymaps(),
|
||||
@@ -315,7 +324,7 @@ impl App {
|
||||
let none_cats = self.model_state.workbook.active_view().none_cats();
|
||||
self.model_state.workbook.model.recompute_formulas(&none_cats);
|
||||
let view = self.model_state.workbook.active_view();
|
||||
let frozen = self.drill_state.as_ref().map(|s| Rc::clone(&s.records));
|
||||
let frozen = self.view_state.drill_state.as_ref().map(|s| Rc::clone(&s.records));
|
||||
self.layout = GridLayout::with_frozen_records(&self.model_state.workbook.model, view, frozen);
|
||||
}
|
||||
|
||||
@@ -329,30 +338,30 @@ impl App {
|
||||
view,
|
||||
layout,
|
||||
registry: self.keymap_set.registry(),
|
||||
mode: &self.mode,
|
||||
mode: &self.view_state.mode,
|
||||
selected: view.selected,
|
||||
row_offset: view.row_offset,
|
||||
col_offset: view.col_offset,
|
||||
search_query: &self.search_query,
|
||||
yanked: &self.yanked,
|
||||
search_query: &self.view_state.search_query,
|
||||
yanked: &self.view_state.yanked,
|
||||
dirty: self.model_state.dirty,
|
||||
search_mode: self.search_mode,
|
||||
formula_panel_open: self.formula_panel_open,
|
||||
category_panel_open: self.category_panel_open,
|
||||
view_panel_open: self.view_panel_open,
|
||||
buffers: &self.buffers,
|
||||
formula_cursor: self.formula_cursor,
|
||||
cat_panel_cursor: self.cat_panel_cursor,
|
||||
view_panel_cursor: self.view_panel_cursor,
|
||||
tile_cat_idx: self.tile_cat_idx,
|
||||
view_back_stack: &self.view_back_stack,
|
||||
view_forward_stack: &self.view_forward_stack,
|
||||
has_drill_state: self.drill_state.is_some(),
|
||||
search_mode: self.view_state.search_mode,
|
||||
formula_panel_open: self.view_state.formula_panel_open,
|
||||
category_panel_open: self.view_state.category_panel_open,
|
||||
view_panel_open: self.view_state.view_panel_open,
|
||||
buffers: &self.view_state.buffers,
|
||||
formula_cursor: self.view_state.formula_cursor,
|
||||
cat_panel_cursor: self.view_state.cat_panel_cursor,
|
||||
view_panel_cursor: self.view_state.view_panel_cursor,
|
||||
tile_cat_idx: self.view_state.tile_cat_idx,
|
||||
view_back_stack: &self.view_state.view_back_stack,
|
||||
view_forward_stack: &self.view_state.view_forward_stack,
|
||||
has_drill_state: self.view_state.drill_state.is_some(),
|
||||
display_value: {
|
||||
let key = layout.cell_key(sel_row, sel_col);
|
||||
if let Some(k) = &key {
|
||||
if let Some((idx, dim)) = crate::view::synthetic_record_info(k) {
|
||||
self.drill_state
|
||||
self.view_state.drill_state
|
||||
.as_ref()
|
||||
.and_then(|s| s.pending_edits.get(&(idx, dim)).cloned())
|
||||
.or_else(|| layout.resolve_display(k))
|
||||
@@ -381,7 +390,7 @@ impl App {
|
||||
view.col_offset,
|
||||
)
|
||||
},
|
||||
expanded_cats: &self.expanded_cats,
|
||||
expanded_cats: &self.view_state.expanded_cats,
|
||||
key_code: key,
|
||||
}
|
||||
}
|
||||
@@ -419,7 +428,7 @@ impl App {
|
||||
self.rebuild_layout();
|
||||
|
||||
// Transient keymap (prefix key sequence) takes priority
|
||||
if let Some(transient) = self.transient_keymap.take() {
|
||||
if let Some(transient) = self.view_state.transient_keymap.take() {
|
||||
let effects = {
|
||||
let ctx = self.cmd_context(key.code, key.modifiers);
|
||||
self.keymap_set
|
||||
@@ -455,13 +464,13 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn start_import_wizard(&mut self, json: serde_json::Value) {
|
||||
self.wizard = Some(ImportWizard::new(json));
|
||||
self.mode = AppMode::ImportWizard;
|
||||
self.view_state.wizard = Some(ImportWizard::new(json));
|
||||
self.view_state.mode = AppMode::ImportWizard;
|
||||
}
|
||||
|
||||
/// Hint text for the status bar (context-sensitive)
|
||||
pub fn hint_text(&self) -> &'static str {
|
||||
match &self.mode {
|
||||
match &self.view_state.mode {
|
||||
AppMode::Normal => {
|
||||
"hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd"
|
||||
}
|
||||
@@ -517,6 +526,34 @@ mod tests {
|
||||
let _: bool = app.model_state.dirty;
|
||||
}
|
||||
|
||||
/// improvise-ew0: ViewState owns the UI session slice — mode, status,
|
||||
/// search, panels, navigation, drill, yanked, buffers, etc. App
|
||||
/// accesses them through view_state.
|
||||
#[test]
|
||||
fn app_view_state_owns_ui_session_fields() {
|
||||
let app = App::new(Workbook::new("T"), None);
|
||||
let _: &AppMode = &app.view_state.mode;
|
||||
let _: &str = &app.view_state.status_msg;
|
||||
let _: &Option<ImportWizard> = &app.view_state.wizard;
|
||||
let _: &str = &app.view_state.search_query;
|
||||
let _: bool = app.view_state.search_mode;
|
||||
let _: bool = app.view_state.formula_panel_open;
|
||||
let _: bool = app.view_state.category_panel_open;
|
||||
let _: bool = app.view_state.view_panel_open;
|
||||
let _: usize = app.view_state.cat_panel_cursor;
|
||||
let _: usize = app.view_state.view_panel_cursor;
|
||||
let _: usize = app.view_state.formula_cursor;
|
||||
let _: &Option<CellValue> = &app.view_state.yanked;
|
||||
let _: usize = app.view_state.tile_cat_idx;
|
||||
let _: &Vec<ViewFrame> = &app.view_state.view_back_stack;
|
||||
let _: &Vec<ViewFrame> = &app.view_state.view_forward_stack;
|
||||
let _: &Option<DrillState> = &app.view_state.drill_state;
|
||||
let _: usize = app.view_state.help_page;
|
||||
let _: &std::collections::HashSet<String> = &app.view_state.expanded_cats;
|
||||
let _: &HashMap<String, String> = &app.view_state.buffers;
|
||||
let _: &Option<Arc<Keymap>> = &app.view_state.transient_keymap;
|
||||
}
|
||||
|
||||
fn two_col_model() -> App {
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Row").unwrap(); // → Row axis
|
||||
@@ -588,7 +625,7 @@ mod tests {
|
||||
let json: serde_json::Value = serde_json::json!([{"cat": "A", "val": 1}]);
|
||||
app.start_import_wizard(json);
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::ImportWizard),
|
||||
matches!(app.view_state.mode, AppMode::ImportWizard),
|
||||
"mode should be ImportWizard after start_import_wizard"
|
||||
);
|
||||
}
|
||||
@@ -600,7 +637,7 @@ mod tests {
|
||||
app.start_import_wizard(serde_json::json!([{"x": 1}]));
|
||||
// After the command the mode must NOT be reset to Normal
|
||||
assert!(
|
||||
!matches!(app.mode, AppMode::Normal),
|
||||
!matches!(app.view_state.mode, AppMode::Normal),
|
||||
"mode must not be Normal after import wizard is opened"
|
||||
);
|
||||
}
|
||||
@@ -612,13 +649,13 @@ mod tests {
|
||||
// Enter command mode with ':'
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(matches!(app.mode, AppMode::CommandMode { .. }));
|
||||
assert_eq!(app.buffers.get("command").map(|s| s.as_str()), Some(""));
|
||||
assert!(matches!(app.view_state.mode, AppMode::CommandMode { .. }));
|
||||
assert_eq!(app.view_state.buffers.get("command").map(|s| s.as_str()), Some(""));
|
||||
|
||||
// Type 'q'
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.buffers.get("command").map(|s| s.as_str()), Some("q"));
|
||||
assert_eq!(app.view_state.buffers.get("command").map(|s| s.as_str()), Some("q"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -789,7 +826,7 @@ mod tests {
|
||||
// Enter edit mode
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(matches!(app.mode, AppMode::Editing { .. }));
|
||||
assert!(matches!(app.view_state.mode, AppMode::Editing { .. }));
|
||||
// Type a digit
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
@@ -798,9 +835,9 @@ mod tests {
|
||||
.unwrap();
|
||||
// Should be in edit mode on column 1
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::Editing { .. }),
|
||||
matches!(app.view_state.mode, AppMode::Editing { .. }),
|
||||
"should be in edit mode after Tab, but mode is {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
assert_eq!(
|
||||
app.model_state.workbook.active_view().selected.1,
|
||||
@@ -869,9 +906,9 @@ mod tests {
|
||||
"o should create the first record row in an empty records view"
|
||||
);
|
||||
assert!(
|
||||
app.mode.is_editing(),
|
||||
app.view_state.mode.is_editing(),
|
||||
"o should leave the app in edit mode, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1021,19 +1058,19 @@ mod tests {
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(app.mode.is_editing(), "setup: should be editing");
|
||||
assert!(app.view_state.mode.is_editing(), "setup: should be editing");
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!app.mode.is_editing(),
|
||||
!app.view_state.mode.is_editing(),
|
||||
"Enter at bottom-right should exit editing, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::RecordsNormal),
|
||||
matches!(app.view_state.mode, AppMode::RecordsNormal),
|
||||
"should return to RecordsNormal, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1053,7 +1090,7 @@ mod tests {
|
||||
// Enter edit mode on the bottom-right cell
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(app.mode.is_editing(), "setup: should be editing");
|
||||
assert!(app.view_state.mode.is_editing(), "setup: should be editing");
|
||||
|
||||
// TAB should commit, insert below, move to first cell of new row
|
||||
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
|
||||
@@ -1070,9 +1107,9 @@ mod tests {
|
||||
"TAB should move to first cell of the new row"
|
||||
);
|
||||
assert!(
|
||||
app.mode.is_editing(),
|
||||
app.view_state.mode.is_editing(),
|
||||
"should enter edit mode on the new cell, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1097,7 +1134,7 @@ mod tests {
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('>'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(app.drill_state.is_some(), "drill should create drill state");
|
||||
assert!(app.view_state.drill_state.is_some(), "drill should create drill state");
|
||||
let value_col = (0..app.layout.col_count())
|
||||
.find(|&col| app.layout.col_label(col) == "Value")
|
||||
.expect("drill view should include a Value column");
|
||||
@@ -1117,7 +1154,7 @@ mod tests {
|
||||
"drill edit should remain staged until leaving the drill view"
|
||||
);
|
||||
assert_eq!(
|
||||
app.drill_state
|
||||
app.view_state.drill_state
|
||||
.as_ref()
|
||||
.and_then(|s| s.pending_edits.get(&(0, "Value".to_string()))),
|
||||
Some(&"9".to_string()),
|
||||
@@ -1182,14 +1219,14 @@ mod tests {
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.buffers.get("command").map(|s| s.as_str()), Some("x"));
|
||||
assert_eq!(app.view_state.buffers.get("command").map(|s| s.as_str()), Some("x"));
|
||||
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
|
||||
// Re-enter command mode — buffer should be cleared
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.buffers.get("command").map(|s| s.as_str()), Some(""));
|
||||
assert_eq!(app.view_state.buffers.get("command").map(|s| s.as_str()), Some(""));
|
||||
}
|
||||
|
||||
// ── is_empty_model ──────────────────────────────────────────────────
|
||||
@@ -1219,34 +1256,34 @@ mod tests {
|
||||
#[test]
|
||||
fn help_page_next_advances_page() {
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = 0;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
app.view_state.help_page = 0;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.help_page, 1, "l should advance to page 1");
|
||||
assert_eq!(app.view_state.help_page, 1, "l should advance to page 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_page_prev_goes_back() {
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = 2;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
app.view_state.help_page = 2;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.help_page, 1, "h should go back to page 1");
|
||||
assert_eq!(app.view_state.help_page, 1, "h should go back to page 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_page_clamps_at_zero() {
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = 0;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
app.view_state.help_page = 0;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.help_page, 0, "page should not go below 0");
|
||||
assert_eq!(app.view_state.help_page, 0, "page should not go below 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1254,13 +1291,13 @@ mod tests {
|
||||
use crate::ui::help::HELP_PAGE_COUNT;
|
||||
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = HELP_PAGE_COUNT - 1;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
app.view_state.help_page = HELP_PAGE_COUNT - 1;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
app.help_page,
|
||||
app.view_state.help_page,
|
||||
HELP_PAGE_COUNT - 1,
|
||||
"page should not exceed the last page"
|
||||
);
|
||||
@@ -1271,12 +1308,12 @@ mod tests {
|
||||
#[test]
|
||||
fn help_q_returns_to_normal() {
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::Normal),
|
||||
matches!(app.view_state.mode, AppMode::Normal),
|
||||
"q should return to Normal mode"
|
||||
);
|
||||
}
|
||||
@@ -1284,12 +1321,12 @@ mod tests {
|
||||
#[test]
|
||||
fn help_esc_returns_to_normal() {
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::Normal),
|
||||
matches!(app.view_state.mode, AppMode::Normal),
|
||||
"Esc should return to Normal mode"
|
||||
);
|
||||
}
|
||||
@@ -1297,14 +1334,14 @@ mod tests {
|
||||
#[test]
|
||||
fn help_colon_enters_command_mode() {
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.view_state.mode = AppMode::Help;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
||||
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||
"colon in Help mode should enter CommandMode, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1320,9 +1357,9 @@ mod tests {
|
||||
};
|
||||
effect.apply(&mut app);
|
||||
assert!(
|
||||
app.status_msg.contains("Unknown category"),
|
||||
app.view_state.status_msg.contains("Unknown category"),
|
||||
"should report unknown category, got: {:?}",
|
||||
app.status_msg
|
||||
app.view_state.status_msg
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1336,9 +1373,9 @@ mod tests {
|
||||
};
|
||||
effect.apply(&mut app);
|
||||
assert!(
|
||||
app.status_msg.contains("Formula error"),
|
||||
app.view_state.status_msg.contains("Formula error"),
|
||||
"should report formula error, got: {:?}",
|
||||
app.status_msg
|
||||
app.view_state.status_msg
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1347,19 +1384,19 @@ mod tests {
|
||||
#[test]
|
||||
fn tile_axis_change_stays_in_tile_select() {
|
||||
let mut app = two_col_model();
|
||||
app.mode = AppMode::TileSelect;
|
||||
app.tile_cat_idx = 0;
|
||||
app.view_state.mode = AppMode::TileSelect;
|
||||
app.view_state.tile_cat_idx = 0;
|
||||
|
||||
// Press 'r' to set axis to Row — should stay in TileSelect
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::TileSelect),
|
||||
matches!(app.view_state.mode, AppMode::TileSelect),
|
||||
"should stay in TileSelect after axis change, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
assert!(
|
||||
!app.status_msg.is_empty(),
|
||||
!app.view_state.status_msg.is_empty(),
|
||||
"should show status feedback after axis change"
|
||||
);
|
||||
}
|
||||
@@ -1369,42 +1406,42 @@ mod tests {
|
||||
#[test]
|
||||
fn category_panel_colon_enters_command_mode() {
|
||||
let mut app = two_col_model();
|
||||
app.mode = AppMode::CategoryPanel;
|
||||
app.view_state.mode = AppMode::CategoryPanel;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
||||
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||
"colon in CategoryPanel should enter CommandMode, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_panel_colon_enters_command_mode() {
|
||||
let mut app = two_col_model();
|
||||
app.mode = AppMode::ViewPanel;
|
||||
app.view_state.mode = AppMode::ViewPanel;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
||||
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||
"colon in ViewPanel should enter CommandMode, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tile_select_colon_enters_command_mode() {
|
||||
let mut app = two_col_model();
|
||||
app.mode = AppMode::TileSelect;
|
||||
app.view_state.mode = AppMode::TileSelect;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
||||
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||
"colon in TileSelect should enter CommandMode, got {:?}",
|
||||
app.mode
|
||||
app.view_state.mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user