From b8cff2488ca949067f14c5ea9cac8c54d866cc07 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Sat, 4 Apr 2026 10:42:25 -0700 Subject: [PATCH] feat(effect): add WizardKey and StartImportWizard effects Add WizardKey effect to handle key bindings for navigating wizard steps: Preview, SelectArrayPath, ReviewProposals, ConfigureDates, DefineFormulas, and NameModel. Add StartImportWizard effect to initialize the wizard by reading and parsing a JSON file. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M) --- src/ui/effect.rs | 154 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/ui/effect.rs b/src/ui/effect.rs index 0a7e435..5eb1153 100644 --- a/src/ui/effect.rs +++ b/src/ui/effect.rs @@ -340,6 +340,160 @@ impl Effect for SaveAs { } } +/// Dispatch a key event to the import wizard. +/// The wizard has its own internal state machine; this effect handles +/// all wizard key interactions and App-level side effects. +#[derive(Debug)] +pub struct WizardKey { + pub key_code: crossterm::event::KeyCode, +} +impl Effect for WizardKey { + fn apply(&self, app: &mut App) { + use crate::import::wizard::WizardStep; + + let Some(wizard) = &mut app.wizard else { + return; + }; + + match &wizard.step.clone() { + WizardStep::Preview => match self.key_code { + crossterm::event::KeyCode::Enter | crossterm::event::KeyCode::Char(' ') => { + wizard.advance() + } + crossterm::event::KeyCode::Esc => { + app.mode = AppMode::Normal; + app.wizard = None; + } + _ => {} + }, + WizardStep::SelectArrayPath => match self.key_code { + crossterm::event::KeyCode::Up | crossterm::event::KeyCode::Char('k') => { + wizard.move_cursor(-1) + } + crossterm::event::KeyCode::Down | crossterm::event::KeyCode::Char('j') => { + wizard.move_cursor(1) + } + crossterm::event::KeyCode::Enter => wizard.confirm_path(), + crossterm::event::KeyCode::Esc => { + app.mode = AppMode::Normal; + app.wizard = None; + } + _ => {} + }, + WizardStep::ReviewProposals => match self.key_code { + crossterm::event::KeyCode::Up | crossterm::event::KeyCode::Char('k') => { + wizard.move_cursor(-1) + } + crossterm::event::KeyCode::Down | crossterm::event::KeyCode::Char('j') => { + wizard.move_cursor(1) + } + crossterm::event::KeyCode::Char(' ') => wizard.toggle_proposal(), + crossterm::event::KeyCode::Char('c') => wizard.cycle_proposal_kind(), + crossterm::event::KeyCode::Enter => wizard.advance(), + crossterm::event::KeyCode::Esc => { + app.mode = AppMode::Normal; + app.wizard = None; + } + _ => {} + }, + WizardStep::ConfigureDates => match self.key_code { + crossterm::event::KeyCode::Up | crossterm::event::KeyCode::Char('k') => { + wizard.move_cursor(-1) + } + crossterm::event::KeyCode::Down | crossterm::event::KeyCode::Char('j') => { + wizard.move_cursor(1) + } + crossterm::event::KeyCode::Char(' ') => wizard.toggle_date_component(), + crossterm::event::KeyCode::Enter => wizard.advance(), + crossterm::event::KeyCode::Esc => { + app.mode = AppMode::Normal; + app.wizard = None; + } + _ => {} + }, + WizardStep::DefineFormulas => { + if wizard.formula_editing { + match self.key_code { + crossterm::event::KeyCode::Enter => wizard.confirm_formula(), + crossterm::event::KeyCode::Esc => wizard.cancel_formula_edit(), + crossterm::event::KeyCode::Backspace => wizard.pop_formula_char(), + crossterm::event::KeyCode::Char(c) => wizard.push_formula_char(c), + _ => {} + } + } else { + match self.key_code { + crossterm::event::KeyCode::Char('n') => wizard.start_formula_edit(), + crossterm::event::KeyCode::Char('d') => wizard.delete_formula(), + crossterm::event::KeyCode::Up | crossterm::event::KeyCode::Char('k') => { + wizard.move_cursor(-1) + } + crossterm::event::KeyCode::Down | crossterm::event::KeyCode::Char('j') => { + wizard.move_cursor(1) + } + crossterm::event::KeyCode::Enter => wizard.advance(), + crossterm::event::KeyCode::Esc => { + app.mode = AppMode::Normal; + app.wizard = None; + } + _ => {} + } + } + } + WizardStep::NameModel => match self.key_code { + crossterm::event::KeyCode::Char(c) => wizard.push_name_char(c), + crossterm::event::KeyCode::Backspace => wizard.pop_name_char(), + crossterm::event::KeyCode::Enter => match wizard.build_model() { + Ok(mut model) => { + model.normalize_view_state(); + app.model = model; + app.formula_cursor = 0; + app.dirty = true; + app.status_msg = "Import successful! Press :w to save.".to_string(); + app.mode = AppMode::Normal; + app.wizard = None; + } + Err(e) => { + if let Some(w) = &mut app.wizard { + w.message = Some(format!("Error: {e}")); + } + } + }, + crossterm::event::KeyCode::Esc => { + app.mode = AppMode::Normal; + app.wizard = None; + } + _ => {} + }, + WizardStep::Done => { + app.mode = AppMode::Normal; + app.wizard = None; + } + } + } +} + +/// Start the import wizard from a JSON file path. +#[derive(Debug)] +pub struct StartImportWizard(pub String); +impl Effect for StartImportWizard { + fn apply(&self, app: &mut App) { + match std::fs::read_to_string(&self.0) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(json) => { + app.wizard = Some(crate::import::wizard::ImportWizard::new(json)); + app.mode = AppMode::ImportWizard; + } + Err(e) => { + app.status_msg = format!("JSON parse error: {e}"); + } + }, + Err(e) => { + app.status_msg = format!("Cannot read file: {e}"); + } + } + } +} + #[derive(Debug)] pub struct ExportCsv(pub PathBuf); impl Effect for ExportCsv {