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:
@@ -276,7 +276,7 @@ pub struct SetTransientKeymap(pub Arc<Keymap>);
|
|||||||
|
|
||||||
impl Effect for SetTransientKeymap {
|
impl Effect for SetTransientKeymap {
|
||||||
fn apply(&self, app: &mut crate::ui::app::App) {
|
fn apply(&self, app: &mut crate::ui::app::App) {
|
||||||
app.transient_keymap = Some(self.0.clone());
|
app.view_state.transient_keymap = Some(self.0.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-34
@@ -61,7 +61,7 @@ pub fn run_tui(
|
|||||||
if let Some(json) = import_value {
|
if let Some(json) = import_value {
|
||||||
app.start_import_wizard(json);
|
app.start_import_wizard(json);
|
||||||
} else if app.is_empty_model() {
|
} else if app.is_empty_model() {
|
||||||
app.mode = AppMode::Help;
|
app.view_state.mode = AppMode::Help;
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -82,7 +82,7 @@ pub fn run_tui(
|
|||||||
|
|
||||||
app.autosave_if_needed();
|
app.autosave_if_needed();
|
||||||
|
|
||||||
if matches!(app.mode, AppMode::Quit) {
|
if matches!(app.view_state.mode, AppMode::Quit) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,21 +167,21 @@ fn draw(f: &mut Frame, app: &App) {
|
|||||||
draw_bottom_bar(f, main_chunks[3], app);
|
draw_bottom_bar(f, main_chunks[3], app);
|
||||||
|
|
||||||
// Overlays (rendered last so they appear on top)
|
// Overlays (rendered last so they appear on top)
|
||||||
if matches!(app.mode, AppMode::Help) {
|
if matches!(app.view_state.mode, AppMode::Help) {
|
||||||
f.render_widget(HelpWidget::new(app.help_page), size);
|
f.render_widget(HelpWidget::new(app.view_state.help_page), size);
|
||||||
}
|
}
|
||||||
if matches!(app.mode, AppMode::ImportWizard)
|
if matches!(app.view_state.mode, AppMode::ImportWizard)
|
||||||
&& let Some(wizard) = &app.wizard
|
&& let Some(wizard) = &app.view_state.wizard
|
||||||
{
|
{
|
||||||
f.render_widget(ImportWizardWidget::new(wizard), size);
|
f.render_widget(ImportWizardWidget::new(wizard), size);
|
||||||
}
|
}
|
||||||
// ExportPrompt now uses the minibuffer at the bottom bar.
|
// ExportPrompt now uses the minibuffer at the bottom bar.
|
||||||
if app.is_empty_model() && matches!(app.mode, AppMode::Normal | AppMode::CommandMode { .. }) {
|
if app.is_empty_model() && matches!(app.view_state.mode, AppMode::Normal | AppMode::CommandMode { .. }) {
|
||||||
draw_welcome(f, main_chunks[1]);
|
draw_welcome(f, main_chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Which-key popup: show available completions after a prefix key
|
// Which-key popup: show available completions after a prefix key
|
||||||
if let Some(ref km) = app.transient_keymap {
|
if let Some(ref km) = app.view_state.transient_keymap {
|
||||||
let hints = km.binding_hints();
|
let hints = km.binding_hints();
|
||||||
f.render_widget(WhichKeyWidget::new(&hints), size);
|
f.render_widget(WhichKeyWidget::new(&hints), size);
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ fn draw_title(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
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 side_open = app.view_state.formula_panel_open || app.view_state.category_panel_open || app.view_state.view_panel_open;
|
||||||
|
|
||||||
let grid_area;
|
let grid_area;
|
||||||
if side_open {
|
if side_open {
|
||||||
@@ -229,9 +229,9 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
|
|
||||||
let side = chunks[1];
|
let side = chunks[1];
|
||||||
let panel_count = [
|
let panel_count = [
|
||||||
app.formula_panel_open,
|
app.view_state.formula_panel_open,
|
||||||
app.category_panel_open,
|
app.view_state.category_panel_open,
|
||||||
app.view_panel_open,
|
app.view_state.view_panel_open,
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&&b| b)
|
.filter(|&&b| b)
|
||||||
@@ -239,26 +239,26 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let ph = side.height / panel_count.max(1);
|
let ph = side.height / panel_count.max(1);
|
||||||
let mut y = side.y;
|
let mut y = side.y;
|
||||||
|
|
||||||
if app.formula_panel_open {
|
if app.view_state.formula_panel_open {
|
||||||
let a = Rect::new(side.x, y, side.width, ph);
|
let a = Rect::new(side.x, y, side.width, ph);
|
||||||
let content = FormulaContent::new(&app.model_state.workbook.model, &app.mode);
|
let content = FormulaContent::new(&app.model_state.workbook.model, &app.view_state.mode);
|
||||||
f.render_widget(Panel::new(content, &app.mode, app.formula_cursor), a);
|
f.render_widget(Panel::new(content, &app.view_state.mode, app.view_state.formula_cursor), a);
|
||||||
y += ph;
|
y += ph;
|
||||||
}
|
}
|
||||||
if app.category_panel_open {
|
if app.view_state.category_panel_open {
|
||||||
let a = Rect::new(side.x, y, side.width, ph);
|
let a = Rect::new(side.x, y, side.width, ph);
|
||||||
let content = CategoryContent::new(
|
let content = CategoryContent::new(
|
||||||
&app.model_state.workbook.model,
|
&app.model_state.workbook.model,
|
||||||
app.model_state.workbook.active_view(),
|
app.model_state.workbook.active_view(),
|
||||||
&app.expanded_cats,
|
&app.view_state.expanded_cats,
|
||||||
);
|
);
|
||||||
f.render_widget(Panel::new(content, &app.mode, app.cat_panel_cursor), a);
|
f.render_widget(Panel::new(content, &app.view_state.mode, app.view_state.cat_panel_cursor), a);
|
||||||
y += ph;
|
y += ph;
|
||||||
}
|
}
|
||||||
if app.view_panel_open {
|
if app.view_state.view_panel_open {
|
||||||
let a = Rect::new(side.x, y, side.width, ph);
|
let a = Rect::new(side.x, y, side.width, ph);
|
||||||
let content = ViewContent::new(&app.model_state.workbook);
|
let content = ViewContent::new(&app.model_state.workbook);
|
||||||
f.render_widget(Panel::new(content, &app.mode, app.view_panel_cursor), a);
|
f.render_widget(Panel::new(content, &app.view_state.mode, app.view_state.view_panel_cursor), a);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
grid_area = area;
|
grid_area = area;
|
||||||
@@ -270,10 +270,10 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
app.model_state.workbook.active_view(),
|
app.model_state.workbook.active_view(),
|
||||||
&app.model_state.workbook.active_view,
|
&app.model_state.workbook.active_view,
|
||||||
&app.layout,
|
&app.layout,
|
||||||
&app.mode,
|
&app.view_state.mode,
|
||||||
&app.search_query,
|
&app.view_state.search_query,
|
||||||
&app.buffers,
|
&app.view_state.buffers,
|
||||||
app.drill_state.as_ref(),
|
app.view_state.drill_state.as_ref(),
|
||||||
),
|
),
|
||||||
grid_area,
|
grid_area,
|
||||||
);
|
);
|
||||||
@@ -284,16 +284,17 @@ fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
TileBar::new(
|
TileBar::new(
|
||||||
&app.model_state.workbook.model,
|
&app.model_state.workbook.model,
|
||||||
app.model_state.workbook.active_view(),
|
app.model_state.workbook.active_view(),
|
||||||
&app.mode,
|
&app.view_state.mode,
|
||||||
app.tile_cat_idx,
|
app.view_state.tile_cat_idx,
|
||||||
),
|
),
|
||||||
area,
|
area,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||||
if let Some(mb) = app.mode.minibuffer() {
|
if let Some(mb) = app.view_state.mode.minibuffer() {
|
||||||
let buf = app
|
let buf = app
|
||||||
|
.view_state
|
||||||
.buffers
|
.buffers
|
||||||
.get(mb.buffer_key)
|
.get(mb.buffer_key)
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
@@ -314,25 +315,25 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
||||||
let search_part = if app.search_mode {
|
let search_part = if app.view_state.search_mode {
|
||||||
format!(" /{}▌", app.search_query)
|
format!(" /{}▌", app.view_state.search_query)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = if !app.status_msg.is_empty() {
|
let msg = if !app.view_state.status_msg.is_empty() {
|
||||||
app.status_msg.as_str()
|
app.view_state.status_msg.as_str()
|
||||||
} else {
|
} else {
|
||||||
app.hint_text()
|
app.hint_text()
|
||||||
};
|
};
|
||||||
|
|
||||||
let yank_indicator = if app.yanked.is_some() { " [yank]" } else { "" };
|
let yank_indicator = if app.view_state.yanked.is_some() { " [yank]" } else { "" };
|
||||||
let view_badge = format!(" {}{} ", app.model_state.workbook.active_view, yank_indicator);
|
let view_badge = format!(" {}{} ", app.model_state.workbook.active_view, yank_indicator);
|
||||||
|
|
||||||
let left = format!(" {}{search_part} {msg}", mode_name(&app.mode));
|
let left = format!(" {}{search_part} {msg}", mode_name(&app.view_state.mode));
|
||||||
let line = fill_line(left, &view_badge, area.width);
|
let line = fill_line(left, &view_badge, area.width);
|
||||||
|
|
||||||
f.render_widget(Paragraph::new(line).style(mode_style(&app.mode)), area);
|
f.render_widget(Paragraph::new(line).style(mode_style(&app.view_state.mode)), area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_welcome(f: &mut Frame, area: Rect) {
|
fn draw_welcome(f: &mut Frame, area: Rect) {
|
||||||
|
|||||||
+149
-112
@@ -206,17 +206,12 @@ impl Default for ModelState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// UI session-state slice: mode, cursors, panels, buffers, navigation stacks,
|
/// UI session-state slice: mode, cursors, panels, buffers, navigation stacks,
|
||||||
/// and other per-session state that does not persist to disk. Filled in by
|
/// and other per-session state that does not persist to disk.
|
||||||
/// improvise-ew0 (vb4 step 3).
|
#[derive(Debug)]
|
||||||
#[derive(Debug, Default)]
|
pub struct ViewState {
|
||||||
pub struct ViewState {}
|
|
||||||
|
|
||||||
pub struct App {
|
|
||||||
pub model_state: ModelState,
|
|
||||||
pub mode: AppMode,
|
pub mode: AppMode,
|
||||||
pub status_msg: String,
|
pub status_msg: String,
|
||||||
pub wizard: Option<ImportWizard>,
|
pub wizard: Option<ImportWizard>,
|
||||||
pub last_autosave: Instant,
|
|
||||||
pub search_query: String,
|
pub search_query: String,
|
||||||
pub search_mode: bool,
|
pub search_mode: bool,
|
||||||
pub formula_panel_open: bool,
|
pub formula_panel_open: bool,
|
||||||
@@ -241,15 +236,48 @@ pub struct App {
|
|||||||
pub drill_state: Option<DrillState>,
|
pub drill_state: Option<DrillState>,
|
||||||
/// Current page index in the Help screen (0-based).
|
/// Current page index in the Help screen (0-based).
|
||||||
pub help_page: usize,
|
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.
|
/// Categories expanded in the category panel tree view.
|
||||||
pub expanded_cats: std::collections::HashSet<String>,
|
pub expanded_cats: std::collections::HashSet<String>,
|
||||||
/// Named text buffers for text-entry modes
|
/// Named text buffers for text-entry modes
|
||||||
pub buffers: HashMap<String, String>,
|
pub buffers: HashMap<String, String>,
|
||||||
/// Transient keymap for Emacs-style prefix key sequences (g→gg, y→yy, etc.)
|
/// Transient keymap for Emacs-style prefix key sequences (g→gg, y→yy, etc.)
|
||||||
pub transient_keymap: Option<Arc<Keymap>>,
|
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.
|
/// Current grid layout, derived from model + view + drill_state.
|
||||||
/// Rebuilt via `rebuild_layout()` after state changes.
|
/// Rebuilt via `rebuild_layout()` after state changes.
|
||||||
pub layout: GridLayout,
|
pub layout: GridLayout,
|
||||||
@@ -280,29 +308,10 @@ impl App {
|
|||||||
file_path,
|
file_path,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
},
|
},
|
||||||
mode: AppMode::Normal,
|
view_state: ViewState::default(),
|
||||||
status_msg: String::new(),
|
|
||||||
wizard: None,
|
|
||||||
last_autosave: Instant::now(),
|
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_width: crossterm::terminal::size().map(|(w, _)| w).unwrap_or(80),
|
||||||
term_height: crossterm::terminal::size().map(|(_, h)| h).unwrap_or(24),
|
term_height: crossterm::terminal::size().map(|(_, h)| h).unwrap_or(24),
|
||||||
expanded_cats: std::collections::HashSet::new(),
|
|
||||||
buffers: HashMap::new(),
|
|
||||||
transient_keymap: None,
|
|
||||||
layout,
|
layout,
|
||||||
abort_effects: false,
|
abort_effects: false,
|
||||||
keymap_set: KeymapSet::default_keymaps(),
|
keymap_set: KeymapSet::default_keymaps(),
|
||||||
@@ -315,7 +324,7 @@ impl App {
|
|||||||
let none_cats = self.model_state.workbook.active_view().none_cats();
|
let none_cats = self.model_state.workbook.active_view().none_cats();
|
||||||
self.model_state.workbook.model.recompute_formulas(&none_cats);
|
self.model_state.workbook.model.recompute_formulas(&none_cats);
|
||||||
let view = self.model_state.workbook.active_view();
|
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);
|
self.layout = GridLayout::with_frozen_records(&self.model_state.workbook.model, view, frozen);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,30 +338,30 @@ impl App {
|
|||||||
view,
|
view,
|
||||||
layout,
|
layout,
|
||||||
registry: self.keymap_set.registry(),
|
registry: self.keymap_set.registry(),
|
||||||
mode: &self.mode,
|
mode: &self.view_state.mode,
|
||||||
selected: view.selected,
|
selected: view.selected,
|
||||||
row_offset: view.row_offset,
|
row_offset: view.row_offset,
|
||||||
col_offset: view.col_offset,
|
col_offset: view.col_offset,
|
||||||
search_query: &self.search_query,
|
search_query: &self.view_state.search_query,
|
||||||
yanked: &self.yanked,
|
yanked: &self.view_state.yanked,
|
||||||
dirty: self.model_state.dirty,
|
dirty: self.model_state.dirty,
|
||||||
search_mode: self.search_mode,
|
search_mode: self.view_state.search_mode,
|
||||||
formula_panel_open: self.formula_panel_open,
|
formula_panel_open: self.view_state.formula_panel_open,
|
||||||
category_panel_open: self.category_panel_open,
|
category_panel_open: self.view_state.category_panel_open,
|
||||||
view_panel_open: self.view_panel_open,
|
view_panel_open: self.view_state.view_panel_open,
|
||||||
buffers: &self.buffers,
|
buffers: &self.view_state.buffers,
|
||||||
formula_cursor: self.formula_cursor,
|
formula_cursor: self.view_state.formula_cursor,
|
||||||
cat_panel_cursor: self.cat_panel_cursor,
|
cat_panel_cursor: self.view_state.cat_panel_cursor,
|
||||||
view_panel_cursor: self.view_panel_cursor,
|
view_panel_cursor: self.view_state.view_panel_cursor,
|
||||||
tile_cat_idx: self.tile_cat_idx,
|
tile_cat_idx: self.view_state.tile_cat_idx,
|
||||||
view_back_stack: &self.view_back_stack,
|
view_back_stack: &self.view_state.view_back_stack,
|
||||||
view_forward_stack: &self.view_forward_stack,
|
view_forward_stack: &self.view_state.view_forward_stack,
|
||||||
has_drill_state: self.drill_state.is_some(),
|
has_drill_state: self.view_state.drill_state.is_some(),
|
||||||
display_value: {
|
display_value: {
|
||||||
let key = layout.cell_key(sel_row, sel_col);
|
let key = layout.cell_key(sel_row, sel_col);
|
||||||
if let Some(k) = &key {
|
if let Some(k) = &key {
|
||||||
if let Some((idx, dim)) = crate::view::synthetic_record_info(k) {
|
if let Some((idx, dim)) = crate::view::synthetic_record_info(k) {
|
||||||
self.drill_state
|
self.view_state.drill_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|s| s.pending_edits.get(&(idx, dim)).cloned())
|
.and_then(|s| s.pending_edits.get(&(idx, dim)).cloned())
|
||||||
.or_else(|| layout.resolve_display(k))
|
.or_else(|| layout.resolve_display(k))
|
||||||
@@ -381,7 +390,7 @@ impl App {
|
|||||||
view.col_offset,
|
view.col_offset,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
expanded_cats: &self.expanded_cats,
|
expanded_cats: &self.view_state.expanded_cats,
|
||||||
key_code: key,
|
key_code: key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,7 +428,7 @@ impl App {
|
|||||||
self.rebuild_layout();
|
self.rebuild_layout();
|
||||||
|
|
||||||
// Transient keymap (prefix key sequence) takes priority
|
// 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 effects = {
|
||||||
let ctx = self.cmd_context(key.code, key.modifiers);
|
let ctx = self.cmd_context(key.code, key.modifiers);
|
||||||
self.keymap_set
|
self.keymap_set
|
||||||
@@ -455,13 +464,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_import_wizard(&mut self, json: serde_json::Value) {
|
pub fn start_import_wizard(&mut self, json: serde_json::Value) {
|
||||||
self.wizard = Some(ImportWizard::new(json));
|
self.view_state.wizard = Some(ImportWizard::new(json));
|
||||||
self.mode = AppMode::ImportWizard;
|
self.view_state.mode = AppMode::ImportWizard;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hint text for the status bar (context-sensitive)
|
/// Hint text for the status bar (context-sensitive)
|
||||||
pub fn hint_text(&self) -> &'static str {
|
pub fn hint_text(&self) -> &'static str {
|
||||||
match &self.mode {
|
match &self.view_state.mode {
|
||||||
AppMode::Normal => {
|
AppMode::Normal => {
|
||||||
"hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd"
|
"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;
|
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 {
|
fn two_col_model() -> App {
|
||||||
let mut wb = Workbook::new("T");
|
let mut wb = Workbook::new("T");
|
||||||
wb.add_category("Row").unwrap(); // → Row axis
|
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}]);
|
let json: serde_json::Value = serde_json::json!([{"cat": "A", "val": 1}]);
|
||||||
app.start_import_wizard(json);
|
app.start_import_wizard(json);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::ImportWizard),
|
matches!(app.view_state.mode, AppMode::ImportWizard),
|
||||||
"mode should be ImportWizard after start_import_wizard"
|
"mode should be ImportWizard after start_import_wizard"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -600,7 +637,7 @@ mod tests {
|
|||||||
app.start_import_wizard(serde_json::json!([{"x": 1}]));
|
app.start_import_wizard(serde_json::json!([{"x": 1}]));
|
||||||
// After the command the mode must NOT be reset to Normal
|
// After the command the mode must NOT be reset to Normal
|
||||||
assert!(
|
assert!(
|
||||||
!matches!(app.mode, AppMode::Normal),
|
!matches!(app.view_state.mode, AppMode::Normal),
|
||||||
"mode must not be Normal after import wizard is opened"
|
"mode must not be Normal after import wizard is opened"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -612,13 +649,13 @@ mod tests {
|
|||||||
// Enter command mode with ':'
|
// Enter command mode with ':'
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(matches!(app.mode, AppMode::CommandMode { .. }));
|
assert!(matches!(app.view_state.mode, AppMode::CommandMode { .. }));
|
||||||
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(""));
|
||||||
|
|
||||||
// Type 'q'
|
// Type 'q'
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
@@ -789,7 +826,7 @@ mod tests {
|
|||||||
// Enter edit mode
|
// Enter edit mode
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(matches!(app.mode, AppMode::Editing { .. }));
|
assert!(matches!(app.view_state.mode, AppMode::Editing { .. }));
|
||||||
// Type a digit
|
// Type a digit
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -798,9 +835,9 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
// Should be in edit mode on column 1
|
// Should be in edit mode on column 1
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::Editing { .. }),
|
matches!(app.view_state.mode, AppMode::Editing { .. }),
|
||||||
"should be in edit mode after Tab, but mode is {:?}",
|
"should be in edit mode after Tab, but mode is {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.model_state.workbook.active_view().selected.1,
|
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"
|
"o should create the first record row in an empty records view"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
app.mode.is_editing(),
|
app.view_state.mode.is_editing(),
|
||||||
"o should leave the app in edit mode, got {:?}",
|
"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))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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))
|
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!app.mode.is_editing(),
|
!app.view_state.mode.is_editing(),
|
||||||
"Enter at bottom-right should exit editing, got {:?}",
|
"Enter at bottom-right should exit editing, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::RecordsNormal),
|
matches!(app.view_state.mode, AppMode::RecordsNormal),
|
||||||
"should return to RecordsNormal, got {:?}",
|
"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
|
// Enter edit mode on the bottom-right cell
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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
|
// TAB should commit, insert below, move to first cell of new row
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
|
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"
|
"TAB should move to first cell of the new row"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
app.mode.is_editing(),
|
app.view_state.mode.is_editing(),
|
||||||
"should enter edit mode on the new cell, got {:?}",
|
"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))
|
app.handle_key(KeyEvent::new(KeyCode::Char('>'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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())
|
let value_col = (0..app.layout.col_count())
|
||||||
.find(|&col| app.layout.col_label(col) == "Value")
|
.find(|&col| app.layout.col_label(col) == "Value")
|
||||||
.expect("drill view should include a Value column");
|
.expect("drill view should include a Value column");
|
||||||
@@ -1117,7 +1154,7 @@ mod tests {
|
|||||||
"drill edit should remain staged until leaving the drill view"
|
"drill edit should remain staged until leaving the drill view"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.drill_state
|
app.view_state.drill_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|s| s.pending_edits.get(&(0, "Value".to_string()))),
|
.and_then(|s| s.pending_edits.get(&(0, "Value".to_string()))),
|
||||||
Some(&"9".to_string()),
|
Some(&"9".to_string()),
|
||||||
@@ -1182,14 +1219,14 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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))
|
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Re-enter command mode — buffer should be cleared
|
// Re-enter command mode — buffer should be cleared
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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 ──────────────────────────────────────────────────
|
// ── is_empty_model ──────────────────────────────────────────────────
|
||||||
@@ -1219,34 +1256,34 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn help_page_next_advances_page() {
|
fn help_page_next_advances_page() {
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
let mut app = App::new(Workbook::new("T"), None);
|
||||||
app.mode = AppMode::Help;
|
app.view_state.mode = AppMode::Help;
|
||||||
app.help_page = 0;
|
app.view_state.help_page = 0;
|
||||||
|
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
fn help_page_prev_goes_back() {
|
fn help_page_prev_goes_back() {
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
let mut app = App::new(Workbook::new("T"), None);
|
||||||
app.mode = AppMode::Help;
|
app.view_state.mode = AppMode::Help;
|
||||||
app.help_page = 2;
|
app.view_state.help_page = 2;
|
||||||
|
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
fn help_page_clamps_at_zero() {
|
fn help_page_clamps_at_zero() {
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
let mut app = App::new(Workbook::new("T"), None);
|
||||||
app.mode = AppMode::Help;
|
app.view_state.mode = AppMode::Help;
|
||||||
app.help_page = 0;
|
app.view_state.help_page = 0;
|
||||||
|
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
@@ -1254,13 +1291,13 @@ mod tests {
|
|||||||
use crate::ui::help::HELP_PAGE_COUNT;
|
use crate::ui::help::HELP_PAGE_COUNT;
|
||||||
|
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
let mut app = App::new(Workbook::new("T"), None);
|
||||||
app.mode = AppMode::Help;
|
app.view_state.mode = AppMode::Help;
|
||||||
app.help_page = HELP_PAGE_COUNT - 1;
|
app.view_state.help_page = HELP_PAGE_COUNT - 1;
|
||||||
|
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.help_page,
|
app.view_state.help_page,
|
||||||
HELP_PAGE_COUNT - 1,
|
HELP_PAGE_COUNT - 1,
|
||||||
"page should not exceed the last page"
|
"page should not exceed the last page"
|
||||||
);
|
);
|
||||||
@@ -1271,12 +1308,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn help_q_returns_to_normal() {
|
fn help_q_returns_to_normal() {
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
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))
|
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::Normal),
|
matches!(app.view_state.mode, AppMode::Normal),
|
||||||
"q should return to Normal mode"
|
"q should return to Normal mode"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1284,12 +1321,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn help_esc_returns_to_normal() {
|
fn help_esc_returns_to_normal() {
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
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))
|
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::Normal),
|
matches!(app.view_state.mode, AppMode::Normal),
|
||||||
"Esc should return to Normal mode"
|
"Esc should return to Normal mode"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1297,14 +1334,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn help_colon_enters_command_mode() {
|
fn help_colon_enters_command_mode() {
|
||||||
let mut app = App::new(Workbook::new("T"), None);
|
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))
|
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||||
"colon in Help mode should enter CommandMode, got {:?}",
|
"colon in Help mode should enter CommandMode, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,9 +1357,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
effect.apply(&mut app);
|
effect.apply(&mut app);
|
||||||
assert!(
|
assert!(
|
||||||
app.status_msg.contains("Unknown category"),
|
app.view_state.status_msg.contains("Unknown category"),
|
||||||
"should report unknown category, got: {:?}",
|
"should report unknown category, got: {:?}",
|
||||||
app.status_msg
|
app.view_state.status_msg
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1336,9 +1373,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
effect.apply(&mut app);
|
effect.apply(&mut app);
|
||||||
assert!(
|
assert!(
|
||||||
app.status_msg.contains("Formula error"),
|
app.view_state.status_msg.contains("Formula error"),
|
||||||
"should report formula error, got: {:?}",
|
"should report formula error, got: {:?}",
|
||||||
app.status_msg
|
app.view_state.status_msg
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1347,19 +1384,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn tile_axis_change_stays_in_tile_select() {
|
fn tile_axis_change_stays_in_tile_select() {
|
||||||
let mut app = two_col_model();
|
let mut app = two_col_model();
|
||||||
app.mode = AppMode::TileSelect;
|
app.view_state.mode = AppMode::TileSelect;
|
||||||
app.tile_cat_idx = 0;
|
app.view_state.tile_cat_idx = 0;
|
||||||
|
|
||||||
// Press 'r' to set axis to Row — should stay in TileSelect
|
// Press 'r' to set axis to Row — should stay in TileSelect
|
||||||
app.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE))
|
app.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::TileSelect),
|
matches!(app.view_state.mode, AppMode::TileSelect),
|
||||||
"should stay in TileSelect after axis change, got {:?}",
|
"should stay in TileSelect after axis change, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!app.status_msg.is_empty(),
|
!app.view_state.status_msg.is_empty(),
|
||||||
"should show status feedback after axis change"
|
"should show status feedback after axis change"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1369,42 +1406,42 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn category_panel_colon_enters_command_mode() {
|
fn category_panel_colon_enters_command_mode() {
|
||||||
let mut app = two_col_model();
|
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))
|
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||||
"colon in CategoryPanel should enter CommandMode, got {:?}",
|
"colon in CategoryPanel should enter CommandMode, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn view_panel_colon_enters_command_mode() {
|
fn view_panel_colon_enters_command_mode() {
|
||||||
let mut app = two_col_model();
|
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))
|
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||||
"colon in ViewPanel should enter CommandMode, got {:?}",
|
"colon in ViewPanel should enter CommandMode, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tile_select_colon_enters_command_mode() {
|
fn tile_select_colon_enters_command_mode() {
|
||||||
let mut app = two_col_model();
|
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))
|
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::CommandMode { .. }),
|
matches!(app.view_state.mode, AppMode::CommandMode { .. }),
|
||||||
"colon in TileSelect should enter CommandMode, got {:?}",
|
"colon in TileSelect should enter CommandMode, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-135
@@ -38,7 +38,7 @@ impl Effect for AddItem {
|
|||||||
if let Some(cat) = app.model_state.workbook.model.category_mut(&self.category) {
|
if let Some(cat) = app.model_state.workbook.model.category_mut(&self.category) {
|
||||||
cat.add_item(&self.item);
|
cat.add_item(&self.item);
|
||||||
} else {
|
} else {
|
||||||
app.status_msg = format!("Unknown category '{}'", self.category);
|
app.view_state.status_msg = format!("Unknown category '{}'", self.category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ impl Effect for AddItemInGroup {
|
|||||||
if let Some(cat) = app.model_state.workbook.model.category_mut(&self.category) {
|
if let Some(cat) = app.model_state.workbook.model.category_mut(&self.category) {
|
||||||
cat.add_item_in_group(&self.item, &self.group);
|
cat.add_item_in_group(&self.item, &self.group);
|
||||||
} else {
|
} else {
|
||||||
app.status_msg = format!("Unknown category '{}'", self.category);
|
app.view_state.status_msg = format!("Unknown category '{}'", self.category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ impl Effect for AddFormula {
|
|||||||
app.model_state.workbook.model.add_formula(formula);
|
app.model_state.workbook.model.add_formula(formula);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Formula error: {e}");
|
app.view_state.status_msg = format!("Formula error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ impl Effect for RemoveFormula {
|
|||||||
///
|
///
|
||||||
/// `target_mode` is supplied by the caller (keymap binding via
|
/// `target_mode` is supplied by the caller (keymap binding via
|
||||||
/// `EnterEditAtCursorCmd`, or `CommitAndAdvance` from its own `edit_mode`
|
/// `EnterEditAtCursorCmd`, or `CommitAndAdvance` from its own `edit_mode`
|
||||||
/// field). The effect itself never inspects `app.mode` — the mode is decided
|
/// field). The effect itself never inspects `app.view_state.mode` — the mode is decided
|
||||||
/// statically by whoever invoked us.
|
/// statically by whoever invoked us.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EnterEditAtCursor {
|
pub struct EnterEditAtCursor {
|
||||||
@@ -144,8 +144,8 @@ impl Effect for EnterEditAtCursor {
|
|||||||
);
|
);
|
||||||
let value = ctx.display_value.clone();
|
let value = ctx.display_value.clone();
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
app.buffers.insert("edit".to_string(), value);
|
app.view_state.buffers.insert("edit".to_string(), value);
|
||||||
app.mode = self.target_mode.clone();
|
app.view_state.mode = self.target_mode.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,8 +162,8 @@ impl Effect for TogglePruneEmpty {
|
|||||||
pub struct ToggleCatExpand(pub String);
|
pub struct ToggleCatExpand(pub String);
|
||||||
impl Effect for ToggleCatExpand {
|
impl Effect for ToggleCatExpand {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if !app.expanded_cats.remove(&self.0) {
|
if !app.view_state.expanded_cats.remove(&self.0) {
|
||||||
app.expanded_cats.insert(self.0.clone());
|
app.view_state.expanded_cats.insert(self.0.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,11 +211,11 @@ impl Effect for SwitchView {
|
|||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let current = app.model_state.workbook.active_view.clone();
|
let current = app.model_state.workbook.active_view.clone();
|
||||||
if current != self.0 {
|
if current != self.0 {
|
||||||
app.view_back_stack.push(ViewFrame {
|
app.view_state.view_back_stack.push(ViewFrame {
|
||||||
view_name: current,
|
view_name: current,
|
||||||
mode: app.mode.clone(),
|
mode: app.view_state.mode.clone(),
|
||||||
});
|
});
|
||||||
app.view_forward_stack.clear();
|
app.view_state.view_forward_stack.clear();
|
||||||
}
|
}
|
||||||
let _ = app.model_state.workbook.switch_view(&self.0);
|
let _ = app.model_state.workbook.switch_view(&self.0);
|
||||||
}
|
}
|
||||||
@@ -226,14 +226,14 @@ impl Effect for SwitchView {
|
|||||||
pub struct ViewBack;
|
pub struct ViewBack;
|
||||||
impl Effect for ViewBack {
|
impl Effect for ViewBack {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(frame) = app.view_back_stack.pop() {
|
if let Some(frame) = app.view_state.view_back_stack.pop() {
|
||||||
let current = app.model_state.workbook.active_view.clone();
|
let current = app.model_state.workbook.active_view.clone();
|
||||||
app.view_forward_stack.push(ViewFrame {
|
app.view_state.view_forward_stack.push(ViewFrame {
|
||||||
view_name: current,
|
view_name: current,
|
||||||
mode: app.mode.clone(),
|
mode: app.view_state.mode.clone(),
|
||||||
});
|
});
|
||||||
let _ = app.model_state.workbook.switch_view(&frame.view_name);
|
let _ = app.model_state.workbook.switch_view(&frame.view_name);
|
||||||
app.mode = frame.mode;
|
app.view_state.mode = frame.mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,14 +243,14 @@ impl Effect for ViewBack {
|
|||||||
pub struct ViewForward;
|
pub struct ViewForward;
|
||||||
impl Effect for ViewForward {
|
impl Effect for ViewForward {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(frame) = app.view_forward_stack.pop() {
|
if let Some(frame) = app.view_state.view_forward_stack.pop() {
|
||||||
let current = app.model_state.workbook.active_view.clone();
|
let current = app.model_state.workbook.active_view.clone();
|
||||||
app.view_back_stack.push(ViewFrame {
|
app.view_state.view_back_stack.push(ViewFrame {
|
||||||
view_name: current,
|
view_name: current,
|
||||||
mode: app.mode.clone(),
|
mode: app.view_state.mode.clone(),
|
||||||
});
|
});
|
||||||
let _ = app.model_state.workbook.switch_view(&frame.view_name);
|
let _ = app.model_state.workbook.switch_view(&frame.view_name);
|
||||||
app.mode = frame.mode;
|
app.view_state.mode = frame.mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +376,7 @@ impl Effect for SetColOffset {
|
|||||||
pub struct ChangeMode(pub AppMode);
|
pub struct ChangeMode(pub AppMode);
|
||||||
impl Effect for ChangeMode {
|
impl Effect for ChangeMode {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.mode = self.0.clone();
|
app.view_state.mode = self.0.clone();
|
||||||
}
|
}
|
||||||
fn changes_mode(&self) -> bool {
|
fn changes_mode(&self) -> bool {
|
||||||
true
|
true
|
||||||
@@ -387,7 +387,7 @@ impl Effect for ChangeMode {
|
|||||||
pub struct SetStatus(pub String);
|
pub struct SetStatus(pub String);
|
||||||
impl Effect for SetStatus {
|
impl Effect for SetStatus {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.status_msg = self.0.clone();
|
app.view_state.status_msg = self.0.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ impl Effect for MarkDirty {
|
|||||||
pub struct SetYanked(pub Option<CellValue>);
|
pub struct SetYanked(pub Option<CellValue>);
|
||||||
impl Effect for SetYanked {
|
impl Effect for SetYanked {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.yanked = self.0.clone();
|
app.view_state.yanked = self.0.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ impl Effect for SetYanked {
|
|||||||
pub struct SetSearchQuery(pub String);
|
pub struct SetSearchQuery(pub String);
|
||||||
impl Effect for SetSearchQuery {
|
impl Effect for SetSearchQuery {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.search_query = self.0.clone();
|
app.view_state.search_query = self.0.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ impl Effect for SetSearchQuery {
|
|||||||
pub struct SetSearchMode(pub bool);
|
pub struct SetSearchMode(pub bool);
|
||||||
impl Effect for SetSearchMode {
|
impl Effect for SetSearchMode {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.search_mode = self.0;
|
app.view_state.search_mode = self.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,9 +433,9 @@ impl Effect for SetBuffer {
|
|||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
// "search" is special — it writes to search_query for backward compat
|
// "search" is special — it writes to search_query for backward compat
|
||||||
if self.name == "search" {
|
if self.name == "search" {
|
||||||
app.search_query = self.value.clone();
|
app.view_state.search_query = self.value.clone();
|
||||||
} else {
|
} else {
|
||||||
app.buffers.insert(self.name.clone(), self.value.clone());
|
app.view_state.buffers.insert(self.name.clone(), self.value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,7 +444,7 @@ impl Effect for SetBuffer {
|
|||||||
pub struct SetTileCatIdx(pub usize);
|
pub struct SetTileCatIdx(pub usize);
|
||||||
impl Effect for SetTileCatIdx {
|
impl Effect for SetTileCatIdx {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.tile_cat_idx = self.0;
|
app.view_state.tile_cat_idx = self.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ impl Effect for SetTileCatIdx {
|
|||||||
pub struct StartDrill(pub Vec<(CellKey, CellValue)>);
|
pub struct StartDrill(pub Vec<(CellKey, CellValue)>);
|
||||||
impl Effect for StartDrill {
|
impl Effect for StartDrill {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.drill_state = Some(super::app::DrillState {
|
app.view_state.drill_state = Some(super::app::DrillState {
|
||||||
records: std::rc::Rc::new(self.0.clone()),
|
records: std::rc::Rc::new(self.0.clone()),
|
||||||
pending_edits: std::collections::HashMap::new(),
|
pending_edits: std::collections::HashMap::new(),
|
||||||
});
|
});
|
||||||
@@ -466,7 +466,7 @@ impl Effect for StartDrill {
|
|||||||
pub struct ApplyAndClearDrill;
|
pub struct ApplyAndClearDrill;
|
||||||
impl Effect for ApplyAndClearDrill {
|
impl Effect for ApplyAndClearDrill {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let Some(drill) = app.drill_state.take() else {
|
let Some(drill) = app.view_state.drill_state.take() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if drill.pending_edits.is_empty() {
|
if drill.pending_edits.is_empty() {
|
||||||
@@ -490,7 +490,7 @@ impl Effect for ApplyAndClearDrill {
|
|||||||
app.model_state.workbook.model.set_cell(orig_key.clone(), value);
|
app.model_state.workbook.model.set_cell(orig_key.clone(), value);
|
||||||
} else {
|
} else {
|
||||||
if new_value.is_empty() {
|
if new_value.is_empty() {
|
||||||
app.status_msg = RECORD_COORDS_CANNOT_BE_EMPTY.to_string();
|
app.view_state.status_msg = RECORD_COORDS_CANNOT_BE_EMPTY.to_string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Rename a coordinate: remove old cell, insert new with updated coord
|
// Rename a coordinate: remove old cell, insert new with updated coord
|
||||||
@@ -532,7 +532,7 @@ pub struct SetDrillPendingEdit {
|
|||||||
}
|
}
|
||||||
impl Effect for SetDrillPendingEdit {
|
impl Effect for SetDrillPendingEdit {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
if let Some(drill) = &mut app.drill_state {
|
if let Some(drill) = &mut app.view_state.drill_state {
|
||||||
drill.pending_edits.insert(
|
drill.pending_edits.insert(
|
||||||
(self.record_idx, self.col_name.clone()),
|
(self.record_idx, self.col_name.clone()),
|
||||||
self.new_value.clone(),
|
self.new_value.clone(),
|
||||||
@@ -551,14 +551,14 @@ impl Effect for Save {
|
|||||||
match crate::persistence::save(&app.model_state.workbook, path) {
|
match crate::persistence::save(&app.model_state.workbook, path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
app.model_state.dirty = false;
|
app.model_state.dirty = false;
|
||||||
app.status_msg = format!("Saved to {}", path.display());
|
app.view_state.status_msg = format!("Saved to {}", path.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Save error: {e}");
|
app.view_state.status_msg = format!("Save error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.status_msg = "No file path — use :w <path>".to_string();
|
app.view_state.status_msg = "No file path — use :w <path>".to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,10 +571,10 @@ impl Effect for SaveAs {
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
app.model_state.file_path = Some(self.0.clone());
|
app.model_state.file_path = Some(self.0.clone());
|
||||||
app.model_state.dirty = false;
|
app.model_state.dirty = false;
|
||||||
app.status_msg = format!("Saved to {}", self.0.display());
|
app.view_state.status_msg = format!("Saved to {}", self.0.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Save error: {e}");
|
app.view_state.status_msg = format!("Save error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -591,7 +591,7 @@ impl Effect for WizardKey {
|
|||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
use crate::import::wizard::WizardStep;
|
use crate::import::wizard::WizardStep;
|
||||||
|
|
||||||
let Some(wizard) = &mut app.wizard else {
|
let Some(wizard) = &mut app.view_state.wizard else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -601,8 +601,8 @@ impl Effect for WizardKey {
|
|||||||
wizard.advance()
|
wizard.advance()
|
||||||
}
|
}
|
||||||
crossterm::event::KeyCode::Esc => {
|
crossterm::event::KeyCode::Esc => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
@@ -615,8 +615,8 @@ impl Effect for WizardKey {
|
|||||||
}
|
}
|
||||||
crossterm::event::KeyCode::Enter => wizard.confirm_path(),
|
crossterm::event::KeyCode::Enter => wizard.confirm_path(),
|
||||||
crossterm::event::KeyCode::Esc => {
|
crossterm::event::KeyCode::Esc => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
@@ -631,8 +631,8 @@ impl Effect for WizardKey {
|
|||||||
crossterm::event::KeyCode::Char('c') => wizard.cycle_proposal_kind(),
|
crossterm::event::KeyCode::Char('c') => wizard.cycle_proposal_kind(),
|
||||||
crossterm::event::KeyCode::Enter => wizard.advance(),
|
crossterm::event::KeyCode::Enter => wizard.advance(),
|
||||||
crossterm::event::KeyCode::Esc => {
|
crossterm::event::KeyCode::Esc => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
@@ -646,8 +646,8 @@ impl Effect for WizardKey {
|
|||||||
crossterm::event::KeyCode::Char(' ') => wizard.toggle_date_component(),
|
crossterm::event::KeyCode::Char(' ') => wizard.toggle_date_component(),
|
||||||
crossterm::event::KeyCode::Enter => wizard.advance(),
|
crossterm::event::KeyCode::Enter => wizard.advance(),
|
||||||
crossterm::event::KeyCode::Esc => {
|
crossterm::event::KeyCode::Esc => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
@@ -672,8 +672,8 @@ impl Effect for WizardKey {
|
|||||||
}
|
}
|
||||||
crossterm::event::KeyCode::Enter => wizard.advance(),
|
crossterm::event::KeyCode::Enter => wizard.advance(),
|
||||||
crossterm::event::KeyCode::Esc => {
|
crossterm::event::KeyCode::Esc => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -686,27 +686,27 @@ impl Effect for WizardKey {
|
|||||||
Ok(mut workbook) => {
|
Ok(mut workbook) => {
|
||||||
workbook.normalize_view_state();
|
workbook.normalize_view_state();
|
||||||
app.model_state.workbook = workbook;
|
app.model_state.workbook = workbook;
|
||||||
app.formula_cursor = 0;
|
app.view_state.formula_cursor = 0;
|
||||||
app.model_state.dirty = true;
|
app.model_state.dirty = true;
|
||||||
app.status_msg = "Import successful! Press :w <path> to save.".to_string();
|
app.view_state.status_msg = "Import successful! Press :w <path> to save.".to_string();
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Some(w) = &mut app.wizard {
|
if let Some(w) = &mut app.view_state.wizard {
|
||||||
w.message = Some(format!("Error: {e}"));
|
w.message = Some(format!("Error: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
crossterm::event::KeyCode::Esc => {
|
crossterm::event::KeyCode::Esc => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
WizardStep::Done => {
|
WizardStep::Done => {
|
||||||
app.mode = AppMode::Normal;
|
app.view_state.mode = AppMode::Normal;
|
||||||
app.wizard = None;
|
app.view_state.wizard = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -720,15 +720,15 @@ impl Effect for StartImportWizard {
|
|||||||
match std::fs::read_to_string(&self.0) {
|
match std::fs::read_to_string(&self.0) {
|
||||||
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
|
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
app.wizard = Some(crate::import::wizard::ImportWizard::new(json));
|
app.view_state.wizard = Some(crate::import::wizard::ImportWizard::new(json));
|
||||||
app.mode = AppMode::ImportWizard;
|
app.view_state.mode = AppMode::ImportWizard;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("JSON parse error: {e}");
|
app.view_state.status_msg = format!("JSON parse error: {e}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Cannot read file: {e}");
|
app.view_state.status_msg = format!("Cannot read file: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,10 +741,10 @@ impl Effect for ExportCsv {
|
|||||||
let view_name = app.model_state.workbook.active_view.clone();
|
let view_name = app.model_state.workbook.active_view.clone();
|
||||||
match crate::persistence::export_csv(&app.model_state.workbook, &view_name, &self.0) {
|
match crate::persistence::export_csv(&app.model_state.workbook, &view_name, &self.0) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
app.status_msg = format!("Exported to {}", self.0.display());
|
app.view_state.status_msg = format!("Exported to {}", self.0.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Export error: {e}");
|
app.view_state.status_msg = format!("Export error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -759,10 +759,10 @@ impl Effect for LoadModel {
|
|||||||
Ok(mut loaded) => {
|
Ok(mut loaded) => {
|
||||||
loaded.normalize_view_state();
|
loaded.normalize_view_state();
|
||||||
app.model_state.workbook = loaded;
|
app.model_state.workbook = loaded;
|
||||||
app.status_msg = format!("Loaded from {}", self.0.display());
|
app.view_state.status_msg = format!("Loaded from {}", self.0.display());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Load error: {e}");
|
app.view_state.status_msg = format!("Load error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -791,7 +791,7 @@ impl Effect for ImportJsonHeadless {
|
|||||||
match crate::import::csv_parser::parse_csv(&self.path) {
|
match crate::import::csv_parser::parse_csv(&self.path) {
|
||||||
Ok(recs) => recs,
|
Ok(recs) => recs,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("CSV error: {e}");
|
app.view_state.status_msg = format!("CSV error: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -799,14 +799,14 @@ impl Effect for ImportJsonHeadless {
|
|||||||
let content = match std::fs::read_to_string(&self.path) {
|
let content = match std::fs::read_to_string(&self.path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Cannot read '{}': {e}", self.path.display());
|
app.view_state.status_msg = format!("Cannot read '{}': {e}", self.path.display());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let value: serde_json::Value = match serde_json::from_str(&content) {
|
let value: serde_json::Value = match serde_json::from_str(&content) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("JSON parse error: {e}");
|
app.view_state.status_msg = format!("JSON parse error: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -815,7 +815,7 @@ impl Effect for ImportJsonHeadless {
|
|||||||
match extract_array_at_path(&value, ap) {
|
match extract_array_at_path(&value, ap) {
|
||||||
Some(arr) => arr.clone(),
|
Some(arr) => arr.clone(),
|
||||||
None => {
|
None => {
|
||||||
app.status_msg = format!("No array at path '{ap}'");
|
app.view_state.status_msg = format!("No array at path '{ap}'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,12 +827,12 @@ impl Effect for ImportJsonHeadless {
|
|||||||
match extract_array_at_path(&value, first) {
|
match extract_array_at_path(&value, first) {
|
||||||
Some(arr) => arr.clone(),
|
Some(arr) => arr.clone(),
|
||||||
None => {
|
None => {
|
||||||
app.status_msg = "Could not extract records array".to_string();
|
app.view_state.status_msg = "Could not extract records array".to_string();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.status_msg = "No array found in JSON".to_string();
|
app.view_state.status_msg = "No array found in JSON".to_string();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -870,10 +870,10 @@ impl Effect for ImportJsonHeadless {
|
|||||||
match pipeline.build_model() {
|
match pipeline.build_model() {
|
||||||
Ok(new_workbook) => {
|
Ok(new_workbook) => {
|
||||||
app.model_state.workbook = new_workbook;
|
app.model_state.workbook = new_workbook;
|
||||||
app.status_msg = "Imported successfully".to_string();
|
app.view_state.status_msg = "Imported successfully".to_string();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_msg = format!("Import error: {e}");
|
app.view_state.status_msg = format!("Import error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -905,9 +905,9 @@ impl Panel {
|
|||||||
impl Effect for SetPanelOpen {
|
impl Effect for SetPanelOpen {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
match self.panel {
|
match self.panel {
|
||||||
Panel::Formula => app.formula_panel_open = self.open,
|
Panel::Formula => app.view_state.formula_panel_open = self.open,
|
||||||
Panel::Category => app.category_panel_open = self.open,
|
Panel::Category => app.view_state.category_panel_open = self.open,
|
||||||
Panel::View => app.view_panel_open = self.open,
|
Panel::View => app.view_state.view_panel_open = self.open,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -920,9 +920,9 @@ pub struct SetPanelCursor {
|
|||||||
impl Effect for SetPanelCursor {
|
impl Effect for SetPanelCursor {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
match self.panel {
|
match self.panel {
|
||||||
Panel::Formula => app.formula_cursor = self.cursor,
|
Panel::Formula => app.view_state.formula_cursor = self.cursor,
|
||||||
Panel::Category => app.cat_panel_cursor = self.cursor,
|
Panel::Category => app.view_state.cat_panel_cursor = self.cursor,
|
||||||
Panel::View => app.view_panel_cursor = self.cursor,
|
Panel::View => app.view_state.view_panel_cursor = self.cursor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -992,7 +992,7 @@ pub struct HelpPageNext;
|
|||||||
impl Effect for HelpPageNext {
|
impl Effect for HelpPageNext {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
let max = crate::ui::help::HELP_PAGE_COUNT.saturating_sub(1);
|
let max = crate::ui::help::HELP_PAGE_COUNT.saturating_sub(1);
|
||||||
app.help_page = app.help_page.saturating_add(1).min(max);
|
app.view_state.help_page = app.view_state.help_page.saturating_add(1).min(max);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1000,7 +1000,7 @@ impl Effect for HelpPageNext {
|
|||||||
pub struct HelpPagePrev;
|
pub struct HelpPagePrev;
|
||||||
impl Effect for HelpPagePrev {
|
impl Effect for HelpPagePrev {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.help_page = app.help_page.saturating_sub(1);
|
app.view_state.help_page = app.view_state.help_page.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1008,7 +1008,7 @@ impl Effect for HelpPagePrev {
|
|||||||
pub struct HelpPageSet(pub usize);
|
pub struct HelpPageSet(pub usize);
|
||||||
impl Effect for HelpPageSet {
|
impl Effect for HelpPageSet {
|
||||||
fn apply(&self, app: &mut App) {
|
fn apply(&self, app: &mut App) {
|
||||||
app.help_page = self.0;
|
app.view_state.help_page = self.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,7 +1079,7 @@ mod tests {
|
|||||||
item: "X".to_string(),
|
item: "X".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.status_msg.contains("Unknown category"));
|
assert!(app.view_state.status_msg.contains("Unknown category"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1184,7 +1184,7 @@ mod tests {
|
|||||||
target_category: "Type".to_string(),
|
target_category: "Type".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.status_msg.contains("Formula error"));
|
assert!(app.view_state.status_msg.contains("Formula error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1210,21 +1210,21 @@ mod tests {
|
|||||||
fn switch_view_pushes_to_back_stack() {
|
fn switch_view_pushes_to_back_stack() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
app.model_state.workbook.create_view("View 2");
|
app.model_state.workbook.create_view("View 2");
|
||||||
assert!(app.view_back_stack.is_empty());
|
assert!(app.view_state.view_back_stack.is_empty());
|
||||||
|
|
||||||
SwitchView("View 2".to_string()).apply(&mut app);
|
SwitchView("View 2".to_string()).apply(&mut app);
|
||||||
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
||||||
assert_eq!(app.view_back_stack.len(), 1);
|
assert_eq!(app.view_state.view_back_stack.len(), 1);
|
||||||
assert_eq!(app.view_back_stack[0].view_name, "Default");
|
assert_eq!(app.view_state.view_back_stack[0].view_name, "Default");
|
||||||
// Forward stack should be cleared
|
// Forward stack should be cleared
|
||||||
assert!(app.view_forward_stack.is_empty());
|
assert!(app.view_state.view_forward_stack.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn switch_view_to_same_does_not_push_stack() {
|
fn switch_view_to_same_does_not_push_stack() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SwitchView("Default".to_string()).apply(&mut app);
|
SwitchView("Default".to_string()).apply(&mut app);
|
||||||
assert!(app.view_back_stack.is_empty());
|
assert!(app.view_state.view_back_stack.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1237,16 +1237,16 @@ mod tests {
|
|||||||
// Go back
|
// Go back
|
||||||
ViewBack.apply(&mut app);
|
ViewBack.apply(&mut app);
|
||||||
assert_eq!(app.model_state.workbook.active_view.as_str(), "Default");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "Default");
|
||||||
assert_eq!(app.view_forward_stack.len(), 1);
|
assert_eq!(app.view_state.view_forward_stack.len(), 1);
|
||||||
assert_eq!(app.view_forward_stack[0].view_name, "View 2");
|
assert_eq!(app.view_state.view_forward_stack[0].view_name, "View 2");
|
||||||
assert!(app.view_back_stack.is_empty());
|
assert!(app.view_state.view_back_stack.is_empty());
|
||||||
|
|
||||||
// Go forward
|
// Go forward
|
||||||
ViewForward.apply(&mut app);
|
ViewForward.apply(&mut app);
|
||||||
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
assert_eq!(app.model_state.workbook.active_view.as_str(), "View 2");
|
||||||
assert_eq!(app.view_back_stack.len(), 1);
|
assert_eq!(app.view_state.view_back_stack.len(), 1);
|
||||||
assert_eq!(app.view_back_stack[0].view_name, "Default");
|
assert_eq!(app.view_state.view_back_stack[0].view_name, "Default");
|
||||||
assert!(app.view_forward_stack.is_empty());
|
assert!(app.view_state.view_forward_stack.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1347,7 +1347,7 @@ mod tests {
|
|||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
assert!(ChangeMode(AppMode::Help).changes_mode());
|
assert!(ChangeMode(AppMode::Help).changes_mode());
|
||||||
ChangeMode(AppMode::Help).apply(&mut app);
|
ChangeMode(AppMode::Help).apply(&mut app);
|
||||||
assert_eq!(app.mode, AppMode::Help);
|
assert_eq!(app.view_state.mode, AppMode::Help);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `AbortChain` must cause subsequent effects in the same
|
/// `AbortChain` must cause subsequent effects in the same
|
||||||
@@ -1356,7 +1356,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn abort_chain_short_circuits_apply_effects() {
|
fn abort_chain_short_circuits_apply_effects() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
app.status_msg = String::new();
|
app.view_state.status_msg = String::new();
|
||||||
let effects: Vec<Box<dyn Effect>> = vec![
|
let effects: Vec<Box<dyn Effect>> = vec![
|
||||||
Box::new(SetStatus("before".into())),
|
Box::new(SetStatus("before".into())),
|
||||||
Box::new(AbortChain),
|
Box::new(AbortChain),
|
||||||
@@ -1364,7 +1364,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
app.apply_effects(effects);
|
app.apply_effects(effects);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.status_msg, "before",
|
app.view_state.status_msg, "before",
|
||||||
"effects after AbortChain must not apply"
|
"effects after AbortChain must not apply"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1373,7 +1373,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
// A subsequent batch must not be affected by the prior abort.
|
// A subsequent batch must not be affected by the prior abort.
|
||||||
app.apply_effects(vec![Box::new(SetStatus("next-batch".into()))]);
|
app.apply_effects(vec![Box::new(SetStatus("next-batch".into()))]);
|
||||||
assert_eq!(app.status_msg, "next-batch");
|
assert_eq!(app.view_state.status_msg, "next-batch");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `CleanEmptyRecords` removes cells whose `CellKey` has no
|
/// `CleanEmptyRecords` removes cells whose `CellKey` has no
|
||||||
@@ -1414,37 +1414,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `EnterEditAtCursor` must use its `target_mode` field, *not* whatever
|
/// `EnterEditAtCursor` must use its `target_mode` field, *not* whatever
|
||||||
/// `app.mode` happens to be when applied. Previous implementation
|
/// `app.view_state.mode` happens to be when applied. Previous implementation
|
||||||
/// branched on `app.mode.is_records()` — the parameterized version
|
/// branched on `app.view_state.mode.is_records()` — the parameterized version
|
||||||
/// trusts the caller (keymap or composing command).
|
/// trusts the caller (keymap or composing command).
|
||||||
#[test]
|
#[test]
|
||||||
fn enter_edit_at_cursor_uses_target_mode_not_app_mode() {
|
fn enter_edit_at_cursor_uses_target_mode_not_app_mode() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
// App starts in Normal mode — but caller has decided we want
|
// App starts in Normal mode — but caller has decided we want
|
||||||
// RecordsEditing (e.g. records-mode `o` sequence).
|
// RecordsEditing (e.g. records-mode `o` sequence).
|
||||||
assert_eq!(app.mode, AppMode::Normal);
|
assert_eq!(app.view_state.mode, AppMode::Normal);
|
||||||
EnterEditAtCursor {
|
EnterEditAtCursor {
|
||||||
target_mode: AppMode::records_editing(),
|
target_mode: AppMode::records_editing(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app.mode, AppMode::RecordsEditing { .. }),
|
matches!(app.view_state.mode, AppMode::RecordsEditing { .. }),
|
||||||
"Expected RecordsEditing, got {:?}",
|
"Expected RecordsEditing, got {:?}",
|
||||||
app.mode
|
app.view_state.mode
|
||||||
);
|
);
|
||||||
|
|
||||||
// Same effect with editing target — should land in plain Editing
|
// Same effect with editing target — should land in plain Editing
|
||||||
// even if app.mode was something else.
|
// even if app.view_state.mode was something else.
|
||||||
let mut app2 = test_app();
|
let mut app2 = test_app();
|
||||||
app2.mode = AppMode::RecordsNormal;
|
app2.view_state.mode = AppMode::RecordsNormal;
|
||||||
EnterEditAtCursor {
|
EnterEditAtCursor {
|
||||||
target_mode: AppMode::editing(),
|
target_mode: AppMode::editing(),
|
||||||
}
|
}
|
||||||
.apply(&mut app2);
|
.apply(&mut app2);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(app2.mode, AppMode::Editing { .. }),
|
matches!(app2.view_state.mode, AppMode::Editing { .. }),
|
||||||
"Expected Editing, got {:?}",
|
"Expected Editing, got {:?}",
|
||||||
app2.mode
|
app2.view_state.mode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1453,21 +1453,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn set_buffer_empty_clears() {
|
fn set_buffer_empty_clears() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
app.buffers
|
app.view_state.buffers
|
||||||
.insert("formula".to_string(), "old text".to_string());
|
.insert("formula".to_string(), "old text".to_string());
|
||||||
SetBuffer {
|
SetBuffer {
|
||||||
name: "formula".to_string(),
|
name: "formula".to_string(),
|
||||||
value: String::new(),
|
value: String::new(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert_eq!(app.buffers.get("formula").map(|s| s.as_str()), Some(""),);
|
assert_eq!(app.view_state.buffers.get("formula").map(|s| s.as_str()), Some(""),);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_status_effect() {
|
fn set_status_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetStatus("hello".to_string()).apply(&mut app);
|
SetStatus("hello".to_string()).apply(&mut app);
|
||||||
assert_eq!(app.status_msg, "hello");
|
assert_eq!(app.view_state.status_msg, "hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1482,18 +1482,18 @@ mod tests {
|
|||||||
fn set_yanked_effect() {
|
fn set_yanked_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetYanked(Some(CellValue::Number(42.0))).apply(&mut app);
|
SetYanked(Some(CellValue::Number(42.0))).apply(&mut app);
|
||||||
assert_eq!(app.yanked, Some(CellValue::Number(42.0)));
|
assert_eq!(app.view_state.yanked, Some(CellValue::Number(42.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_search_query_and_mode() {
|
fn set_search_query_and_mode() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetSearchQuery("foo".to_string()).apply(&mut app);
|
SetSearchQuery("foo".to_string()).apply(&mut app);
|
||||||
assert_eq!(app.search_query, "foo");
|
assert_eq!(app.view_state.search_query, "foo");
|
||||||
SetSearchMode(true).apply(&mut app);
|
SetSearchMode(true).apply(&mut app);
|
||||||
assert!(app.search_mode);
|
assert!(app.view_state.search_mode);
|
||||||
SetSearchMode(false).apply(&mut app);
|
SetSearchMode(false).apply(&mut app);
|
||||||
assert!(!app.search_mode);
|
assert!(!app.view_state.search_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SetBuffer special behavior ──────────────────────────────────────
|
// ── SetBuffer special behavior ──────────────────────────────────────
|
||||||
@@ -1506,7 +1506,7 @@ mod tests {
|
|||||||
value: "hello".to_string(),
|
value: "hello".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert_eq!(app.buffers.get("edit").unwrap(), "hello");
|
assert_eq!(app.view_state.buffers.get("edit").unwrap(), "hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1517,8 +1517,8 @@ mod tests {
|
|||||||
value: "query".to_string(),
|
value: "query".to_string(),
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
// "search" buffer is special — writes to app.search_query
|
// "search" buffer is special — writes to app.view_state.search_query
|
||||||
assert_eq!(app.search_query, "query");
|
assert_eq!(app.view_state.search_query, "query");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Panel effects ───────────────────────────────────────────────────
|
// ── Panel effects ───────────────────────────────────────────────────
|
||||||
@@ -1531,35 +1531,35 @@ mod tests {
|
|||||||
open: true,
|
open: true,
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.formula_panel_open);
|
assert!(app.view_state.formula_panel_open);
|
||||||
|
|
||||||
SetPanelCursor {
|
SetPanelCursor {
|
||||||
panel: Panel::Formula,
|
panel: Panel::Formula,
|
||||||
cursor: 3,
|
cursor: 3,
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert_eq!(app.formula_cursor, 3);
|
assert_eq!(app.view_state.formula_cursor, 3);
|
||||||
|
|
||||||
SetPanelOpen {
|
SetPanelOpen {
|
||||||
panel: Panel::Category,
|
panel: Panel::Category,
|
||||||
open: true,
|
open: true,
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.category_panel_open);
|
assert!(app.view_state.category_panel_open);
|
||||||
|
|
||||||
SetPanelOpen {
|
SetPanelOpen {
|
||||||
panel: Panel::View,
|
panel: Panel::View,
|
||||||
open: true,
|
open: true,
|
||||||
}
|
}
|
||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
assert!(app.view_panel_open);
|
assert!(app.view_state.view_panel_open);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_tile_cat_idx_effect() {
|
fn set_tile_cat_idx_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
SetTileCatIdx(2).apply(&mut app);
|
SetTileCatIdx(2).apply(&mut app);
|
||||||
assert_eq!(app.tile_cat_idx, 2);
|
assert_eq!(app.view_state.tile_cat_idx, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Help page effects ───────────────────────────────────────────────
|
// ── Help page effects ───────────────────────────────────────────────
|
||||||
@@ -1567,22 +1567,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn help_page_navigation() {
|
fn help_page_navigation() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
assert_eq!(app.help_page, 0);
|
assert_eq!(app.view_state.help_page, 0);
|
||||||
HelpPageNext.apply(&mut app);
|
HelpPageNext.apply(&mut app);
|
||||||
assert_eq!(app.help_page, 1);
|
assert_eq!(app.view_state.help_page, 1);
|
||||||
HelpPageNext.apply(&mut app);
|
HelpPageNext.apply(&mut app);
|
||||||
assert_eq!(app.help_page, 2);
|
assert_eq!(app.view_state.help_page, 2);
|
||||||
HelpPagePrev.apply(&mut app);
|
HelpPagePrev.apply(&mut app);
|
||||||
assert_eq!(app.help_page, 1);
|
assert_eq!(app.view_state.help_page, 1);
|
||||||
HelpPageSet(0).apply(&mut app);
|
HelpPageSet(0).apply(&mut app);
|
||||||
assert_eq!(app.help_page, 0);
|
assert_eq!(app.view_state.help_page, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn help_page_prev_clamps_at_zero() {
|
fn help_page_prev_clamps_at_zero() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
HelpPagePrev.apply(&mut app);
|
HelpPagePrev.apply(&mut app);
|
||||||
assert_eq!(app.help_page, 0);
|
assert_eq!(app.view_state.help_page, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Drill effects ───────────────────────────────────────────────────
|
// ── Drill effects ───────────────────────────────────────────────────
|
||||||
@@ -1596,11 +1596,11 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
let records = vec![(key, CellValue::Number(42.0))];
|
let records = vec![(key, CellValue::Number(42.0))];
|
||||||
StartDrill(records).apply(&mut app);
|
StartDrill(records).apply(&mut app);
|
||||||
assert!(app.drill_state.is_some());
|
assert!(app.view_state.drill_state.is_some());
|
||||||
|
|
||||||
// Apply with no pending edits — should just clear state
|
// Apply with no pending edits — should just clear state
|
||||||
ApplyAndClearDrill.apply(&mut app);
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
assert!(app.drill_state.is_none());
|
assert!(app.view_state.drill_state.is_none());
|
||||||
assert!(!app.model_state.dirty); // no edits → not dirty
|
assert!(!app.model_state.dirty); // no edits → not dirty
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,7 +1628,7 @@ mod tests {
|
|||||||
.apply(&mut app);
|
.apply(&mut app);
|
||||||
|
|
||||||
ApplyAndClearDrill.apply(&mut app);
|
ApplyAndClearDrill.apply(&mut app);
|
||||||
assert!(app.drill_state.is_none());
|
assert!(app.view_state.drill_state.is_none());
|
||||||
assert!(app.model_state.dirty);
|
assert!(app.model_state.dirty);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.model_state.workbook.model.get_cell(&key),
|
app.model_state.workbook.model.get_cell(&key),
|
||||||
@@ -1726,11 +1726,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn toggle_cat_expand_effect() {
|
fn toggle_cat_expand_effect() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
assert!(!app.expanded_cats.contains("Type"));
|
assert!(!app.view_state.expanded_cats.contains("Type"));
|
||||||
ToggleCatExpand("Type".to_string()).apply(&mut app);
|
ToggleCatExpand("Type".to_string()).apply(&mut app);
|
||||||
assert!(app.expanded_cats.contains("Type"));
|
assert!(app.view_state.expanded_cats.contains("Type"));
|
||||||
ToggleCatExpand("Type".to_string()).apply(&mut app);
|
ToggleCatExpand("Type".to_string()).apply(&mut app);
|
||||||
assert!(!app.expanded_cats.contains("Type"));
|
assert!(!app.view_state.expanded_cats.contains("Type"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1846,7 +1846,7 @@ mod tests {
|
|||||||
fn save_without_file_path_shows_status() {
|
fn save_without_file_path_shows_status() {
|
||||||
let mut app = test_app();
|
let mut app = test_app();
|
||||||
Save.apply(&mut app);
|
Save.apply(&mut app);
|
||||||
assert!(app.status_msg.contains("No file path"));
|
assert!(app.view_state.status_msg.contains("No file path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Panel mode helper ───────────────────────────────────────────────
|
// ── Panel mode helper ───────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user