use std::fmt::Debug; use std::path::PathBuf; use crate::model::cell::{CellKey, CellValue}; use crate::view::Axis; use super::app::{App, AppMode}; /// A discrete state change produced by a command. /// Effects know how to apply themselves to the App. pub trait Effect: Debug { fn apply(&self, app: &mut App); } // ── Model mutations ────────────────────────────────────────────────────────── #[derive(Debug)] pub struct AddCategory(pub String); impl Effect for AddCategory { fn apply(&self, app: &mut App) { let _ = app.model.add_category(&self.0); } } #[derive(Debug)] pub struct AddItem { pub category: String, pub item: String, } impl Effect for AddItem { fn apply(&self, app: &mut App) { if let Some(cat) = app.model.category_mut(&self.category) { cat.add_item(&self.item); } } } #[derive(Debug)] pub struct AddItemInGroup { pub category: String, pub item: String, pub group: String, } impl Effect for AddItemInGroup { fn apply(&self, app: &mut App) { if let Some(cat) = app.model.category_mut(&self.category) { cat.add_item_in_group(&self.item, &self.group); } } } #[derive(Debug)] pub struct SetCell(pub CellKey, pub CellValue); impl Effect for SetCell { fn apply(&self, app: &mut App) { app.model.set_cell(self.0.clone(), self.1.clone()); } } #[derive(Debug)] pub struct ClearCell(pub CellKey); impl Effect for ClearCell { fn apply(&self, app: &mut App) { app.model.clear_cell(&self.0); } } #[derive(Debug)] pub struct AddFormula { pub raw: String, pub target_category: String, } impl Effect for AddFormula { fn apply(&self, app: &mut App) { if let Ok(formula) = crate::formula::parse_formula(&self.raw, &self.target_category) { app.model.add_formula(formula); } } } #[derive(Debug)] pub struct RemoveFormula { pub target: String, pub target_category: String, } impl Effect for RemoveFormula { fn apply(&self, app: &mut App) { app.model .remove_formula(&self.target, &self.target_category); } } // ── View mutations ─────────────────────────────────────────────────────────── #[derive(Debug)] pub struct CreateView(pub String); impl Effect for CreateView { fn apply(&self, app: &mut App) { app.model.create_view(&self.0); } } #[derive(Debug)] pub struct DeleteView(pub String); impl Effect for DeleteView { fn apply(&self, app: &mut App) { let _ = app.model.delete_view(&self.0); } } #[derive(Debug)] pub struct SwitchView(pub String); impl Effect for SwitchView { fn apply(&self, app: &mut App) { let _ = app.model.switch_view(&self.0); } } #[derive(Debug)] pub struct SetAxis { pub category: String, pub axis: Axis, } impl Effect for SetAxis { fn apply(&self, app: &mut App) { app.model .active_view_mut() .set_axis(&self.category, self.axis); } } #[derive(Debug)] pub struct SetPageSelection { pub category: String, pub item: String, } impl Effect for SetPageSelection { fn apply(&self, app: &mut App) { app.model .active_view_mut() .set_page_selection(&self.category, &self.item); } } #[derive(Debug)] pub struct ToggleGroup { pub category: String, pub group: String, } impl Effect for ToggleGroup { fn apply(&self, app: &mut App) { app.model .active_view_mut() .toggle_group_collapse(&self.category, &self.group); } } #[derive(Debug)] pub struct HideItem { pub category: String, pub item: String, } impl Effect for HideItem { fn apply(&self, app: &mut App) { app.model .active_view_mut() .hide_item(&self.category, &self.item); } } #[derive(Debug)] pub struct ShowItem { pub category: String, pub item: String, } impl Effect for ShowItem { fn apply(&self, app: &mut App) { app.model .active_view_mut() .show_item(&self.category, &self.item); } } #[derive(Debug)] pub struct TransposeAxes; impl Effect for TransposeAxes { fn apply(&self, app: &mut App) { app.model.active_view_mut().transpose_axes(); } } #[derive(Debug)] pub struct CycleAxis(pub String); impl Effect for CycleAxis { fn apply(&self, app: &mut App) { app.model.active_view_mut().cycle_axis(&self.0); } } #[derive(Debug)] pub struct SetNumberFormat(pub String); impl Effect for SetNumberFormat { fn apply(&self, app: &mut App) { app.model.active_view_mut().number_format = self.0.clone(); } } // ── Navigation ─────────────────────────────────────────────────────────────── #[derive(Debug)] pub struct SetSelected(pub usize, pub usize); impl Effect for SetSelected { fn apply(&self, app: &mut App) { app.model.active_view_mut().selected = (self.0, self.1); } } #[derive(Debug)] pub struct SetRowOffset(pub usize); impl Effect for SetRowOffset { fn apply(&self, app: &mut App) { app.model.active_view_mut().row_offset = self.0; } } #[derive(Debug)] pub struct SetColOffset(pub usize); impl Effect for SetColOffset { fn apply(&self, app: &mut App) { app.model.active_view_mut().col_offset = self.0; } } // ── App state ──────────────────────────────────────────────────────────────── #[derive(Debug)] pub struct ChangeMode(pub AppMode); impl Effect for ChangeMode { fn apply(&self, app: &mut App) { app.mode = self.0.clone(); } } #[derive(Debug)] pub struct SetStatus(pub String); impl Effect for SetStatus { fn apply(&self, app: &mut App) { app.status_msg = self.0.clone(); } } #[derive(Debug)] pub struct MarkDirty; impl Effect for MarkDirty { fn apply(&self, app: &mut App) { app.dirty = true; } } #[derive(Debug)] pub struct SetYanked(pub Option); impl Effect for SetYanked { fn apply(&self, app: &mut App) { app.yanked = self.0.clone(); } } #[derive(Debug)] pub struct SetSearchQuery(pub String); impl Effect for SetSearchQuery { fn apply(&self, app: &mut App) { app.search_query = self.0.clone(); } } #[derive(Debug)] pub struct SetSearchMode(pub bool); impl Effect for SetSearchMode { fn apply(&self, app: &mut App) { app.search_mode = self.0; } } // ── Side effects ───────────────────────────────────────────────────────────── #[derive(Debug)] pub struct Save; impl Effect for Save { fn apply(&self, app: &mut App) { if let Some(ref path) = app.file_path { match crate::persistence::save(&app.model, path) { Ok(()) => { app.dirty = false; app.status_msg = format!("Saved to {}", path.display()); } Err(e) => { app.status_msg = format!("Save error: {e}"); } } } else { app.status_msg = "No file path — use :w ".to_string(); } } } #[derive(Debug)] pub struct SaveAs(pub PathBuf); impl Effect for SaveAs { fn apply(&self, app: &mut App) { match crate::persistence::save(&app.model, &self.0) { Ok(()) => { app.file_path = Some(self.0.clone()); app.dirty = false; app.status_msg = format!("Saved to {}", self.0.display()); } Err(e) => { app.status_msg = format!("Save error: {e}"); } } } } #[derive(Debug)] pub struct ExportCsv(pub PathBuf); impl Effect for ExportCsv { fn apply(&self, app: &mut App) { let view_name = app.model.active_view.clone(); match crate::persistence::export_csv(&app.model, &view_name, &self.0) { Ok(()) => { app.status_msg = format!("Exported to {}", self.0.display()); } Err(e) => { app.status_msg = format!("Export error: {e}"); } } } } #[derive(Debug)] pub struct SetPanelOpen { pub panel: Panel, pub open: bool, } #[derive(Debug, Clone, Copy)] pub enum Panel { Formula, Category, View, } impl Effect for SetPanelOpen { fn apply(&self, app: &mut App) { match self.panel { Panel::Formula => app.formula_panel_open = self.open, Panel::Category => app.category_panel_open = self.open, Panel::View => app.view_panel_open = self.open, } } } #[derive(Debug)] pub struct SetPanelCursor { pub panel: Panel, pub cursor: usize, } impl Effect for SetPanelCursor { fn apply(&self, app: &mut App) { match self.panel { Panel::Formula => app.formula_cursor = self.cursor, Panel::Category => app.cat_panel_cursor = self.cursor, Panel::View => app.view_panel_cursor = self.cursor, } } } // ── Convenience constructors ───────────────────────────────────────────────── pub fn mark_dirty() -> Box { Box::new(MarkDirty) } pub fn set_status(msg: impl Into) -> Box { Box::new(SetStatus(msg.into())) } pub fn change_mode(mode: AppMode) -> Box { Box::new(ChangeMode(mode)) } pub fn set_selected(row: usize, col: usize) -> Box { Box::new(SetSelected(row, col)) }