diff --git a/src/ui/app.rs b/src/ui/app.rs index 2cc8a98..96a88a9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -141,7 +141,7 @@ impl App { pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> { // Transient keymap (prefix key sequence) takes priority if let Some(transient) = self.transient_keymap.take() { - let ctx = self.cmd_context(); + let ctx = self.cmd_context(key.code, key.modifiers); if let Some(effects) = transient.dispatch(&ctx, key.code, key.modifiers) { drop(ctx); self.apply_effects(effects); @@ -151,7 +151,7 @@ impl App { } // Try mode keymap — if a binding matches, apply effects and return - let ctx = self.cmd_context(); + let ctx = self.cmd_context(key.code, key.modifiers); if let Some(effects) = self.keymap_set.dispatch(&ctx, key.code, key.modifiers) { drop(ctx); self.apply_effects(effects); @@ -161,75 +161,22 @@ impl App { // Fallback: old-style handlers for modes not yet migrated to keymaps match &self.mode.clone() { - AppMode::Normal => { - // Normal mode keys are handled by the keymap above. - // Only search sub-mode still uses the old pattern. - if self.search_mode { - self.handle_search_key(key)?; - } - } - AppMode::Editing { .. } => self.handle_edit_key(key)?, - AppMode::FormulaEdit { .. } => self.handle_formula_edit_key(key)?, - AppMode::FormulaPanel => self.handle_formula_panel_key(key)?, - AppMode::CategoryPanel => self.handle_category_panel_key(key)?, - AppMode::CategoryAdd { .. } => self.handle_category_add_key(key)?, - AppMode::ItemAdd { .. } => self.handle_item_add_key(key)?, - AppMode::ViewPanel => self.handle_view_panel_key(key)?, - AppMode::TileSelect { .. } => self.handle_tile_select_key(key)?, - AppMode::ExportPrompt { .. } => self.handle_export_key(key)?, - AppMode::CommandMode { .. } => self.handle_command_mode_key(key)?, - AppMode::ImportWizard => self.handle_wizard_key(key)?, - AppMode::Quit | AppMode::Help => {} - } - Ok(()) - } - - fn handle_search_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc | KeyCode::Enter => { - self.search_mode = false; - } - KeyCode::Char(c) => { - self.search_query.push(c); - } - KeyCode::Backspace => { - self.search_query.pop(); - } - _ => {} - } - Ok(()) - } - - // ── Command mode ──────────────────────────────────────────────────────── - - fn handle_command_mode_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => { - self.mode = AppMode::Normal; - } - KeyCode::Enter => { - let buf = if let AppMode::CommandMode { buffer } = &self.mode { - buffer.clone() - } else { - return Ok(()); - }; - self.execute_command(&buf)?; - } - KeyCode::Char(c) => { - if let AppMode::CommandMode { buffer } = &mut self.mode { - buffer.push(c); - } - } - KeyCode::Backspace => { - if let AppMode::CommandMode { buffer } = &mut self.mode { - if buffer.is_empty() { - self.mode = AppMode::Normal; + AppMode::CommandMode { .. } => { + // Enter key (execute_command) is still handled here. + // Esc, Backspace, Char are handled by the keymap above. + if key.code == KeyCode::Enter { + let buf = if let AppMode::CommandMode { buffer } = &self.mode { + buffer.clone() } else { - buffer.pop(); - } + return Ok(()); + }; + // Also read from buffers map if the keymap was appending there + let cmd_buf = self.buffers.get("command").cloned().unwrap_or(buf); + self.execute_command(&cmd_buf)?; } } - _ => {} + AppMode::ImportWizard => self.handle_wizard_key(key)?, + _ => {} // All other modes handled by keymap } Ok(()) } @@ -438,502 +385,6 @@ impl App { Ok(()) } - // ── Edit mode ──────────────────────────────────────────────────────────── - - fn handle_edit_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => { - self.mode = AppMode::Normal; - } - KeyCode::Enter => { - let buf = if let AppMode::Editing { buffer } = &self.mode { - buffer.clone() - } else { - return Ok(()); - }; - if let Some(key) = self.selected_cell_key() { - let coords = key.0.iter().map(|(c, v)| [c.clone(), v.clone()]).collect(); - let cmd = if buf.is_empty() { - Command::ClearCell { coords } - } else if let Ok(n) = buf.parse::() { - Command::SetCell { - coords, - value: crate::command::types::CellValueArg::Number { number: n }, - } - } else { - Command::SetCell { - coords, - value: crate::command::types::CellValueArg::Text { text: buf.clone() }, - } - }; - command::dispatch(&mut self.model, &cmd); - self.dirty = true; - } - self.mode = AppMode::Normal; - // Advance cursor down after committing edit - let ctx = self.cmd_context(); - let effects = crate::command::cmd::MoveSelection { dr: 1, dc: 0 }.execute(&ctx); - drop(ctx); - self.apply_effects(effects); - } - KeyCode::Char(c) => { - if let AppMode::Editing { buffer } = &mut self.mode { - buffer.push(c); - } - } - KeyCode::Backspace => { - if let AppMode::Editing { buffer } = &mut self.mode { - buffer.pop(); - } - } - _ => {} - } - Ok(()) - } - - // ── Formula edit ───────────────────────────────────────────────────────── - - fn handle_formula_edit_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => { - self.mode = AppMode::FormulaPanel; - } - KeyCode::Enter => { - let buf = if let AppMode::FormulaEdit { buffer } = &self.mode { - buffer.clone() - } else { - return Ok(()); - }; - let first_cat = self - .model - .category_names() - .into_iter() - .next() - .map(String::from); - if let Some(cat) = first_cat { - let result = command::dispatch( - &mut self.model, - &Command::AddFormula { - raw: buf, - target_category: cat, - }, - ); - self.status_msg = result - .message - .unwrap_or_else(|| "Formula added".to_string()); - self.dirty = true; - } else { - self.status_msg = "Add at least one category first.".to_string(); - } - self.mode = AppMode::FormulaPanel; - } - KeyCode::Char(c) => { - if let AppMode::FormulaEdit { buffer } = &mut self.mode { - buffer.push(c); - } - } - KeyCode::Backspace => { - if let AppMode::FormulaEdit { buffer } = &mut self.mode { - buffer.pop(); - } - } - _ => {} - } - Ok(()) - } - - // ── Panel key handlers ─────────────────────────────────────────────────── - - fn handle_formula_panel_key(&mut self, key: KeyEvent) -> Result<()> { - // Clamp cursor in case the formula list shrank since it was last set. - let flen = self.model.formulas().len(); - if flen == 0 { - self.formula_cursor = 0; - } else { - self.formula_cursor = self.formula_cursor.min(flen - 1); - } - - match key.code { - KeyCode::Esc | KeyCode::Tab => { - self.mode = AppMode::Normal; - } - KeyCode::Char('a') | KeyCode::Char('n') | KeyCode::Char('o') => { - self.mode = AppMode::FormulaEdit { - buffer: String::new(), - }; - } - KeyCode::Char('d') | KeyCode::Delete => { - if self.formula_cursor < self.model.formulas().len() { - let f = &self.model.formulas()[self.formula_cursor]; - let target = f.target.clone(); - let target_category = f.target_category.clone(); - command::dispatch( - &mut self.model, - &Command::RemoveFormula { - target, - target_category, - }, - ); - if self.formula_cursor > 0 { - self.formula_cursor -= 1; - } - self.dirty = true; - } - } - KeyCode::Up | KeyCode::Char('k') => { - if self.formula_cursor > 0 { - self.formula_cursor -= 1; - } - } - KeyCode::Down | KeyCode::Char('j') => { - if self.formula_cursor + 1 < self.model.formulas().len() { - self.formula_cursor += 1; - } - } - _ => {} - } - Ok(()) - } - - fn handle_category_panel_key(&mut self, key: KeyEvent) -> Result<()> { - let cat_names: Vec = self - .model - .category_names() - .into_iter() - .map(String::from) - .collect(); - match key.code { - KeyCode::Esc | KeyCode::Tab => { - self.mode = AppMode::Normal; - } - KeyCode::Up | KeyCode::Char('k') => { - if self.cat_panel_cursor > 0 { - self.cat_panel_cursor -= 1; - } - } - KeyCode::Down | KeyCode::Char('j') => { - if self.cat_panel_cursor + 1 < cat_names.len() { - self.cat_panel_cursor += 1; - } - } - KeyCode::Enter | KeyCode::Char(' ') => { - if let Some(cat_name) = cat_names.get(self.cat_panel_cursor) { - self.model.active_view_mut().cycle_axis(cat_name); - } - } - // n — add a new category - KeyCode::Char('n') => { - self.mode = AppMode::CategoryAdd { - buffer: String::new(), - }; - } - // a / o — open quick-add items mode for the selected category - KeyCode::Char('a') | KeyCode::Char('o') => { - if let Some(cat_name) = cat_names.get(self.cat_panel_cursor) { - self.mode = AppMode::ItemAdd { - category: cat_name.clone(), - buffer: String::new(), - }; - } else { - self.status_msg = - "No category selected. Press n to add a category first.".to_string(); - } - } - _ => {} - } - Ok(()) - } - - fn handle_category_add_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => { - self.mode = AppMode::CategoryPanel; - self.status_msg = String::new(); - } - KeyCode::Enter | KeyCode::Tab => { - let buf = if let AppMode::CategoryAdd { buffer } = &self.mode { - buffer.trim().to_string() - } else { - return Ok(()); - }; - - if !buf.is_empty() { - let result = command::dispatch( - &mut self.model, - &Command::AddCategory { name: buf.clone() }, - ); - if result.ok { - // Move cursor to the new category - self.cat_panel_cursor = self.model.categories.len().saturating_sub(1); - let count = self.model.categories.len(); - self.status_msg = format!("Added category \"{buf}\" ({count} total). Enter to add more, Esc to finish."); - self.dirty = true; - } else { - self.status_msg = result.message.unwrap_or_default(); - } - } - // Stay in CategoryAdd for the next entry - if let AppMode::CategoryAdd { ref mut buffer } = self.mode { - buffer.clear(); - } - } - KeyCode::Char(c) => { - if let AppMode::CategoryAdd { ref mut buffer } = self.mode { - buffer.push(c); - } - } - KeyCode::Backspace => { - if let AppMode::CategoryAdd { ref mut buffer } = self.mode { - buffer.pop(); - } - } - _ => {} - } - Ok(()) - } - - fn handle_item_add_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => { - // Return to category panel - self.mode = AppMode::CategoryPanel; - self.status_msg = String::new(); - } - KeyCode::Enter => { - let (cat, buf) = if let AppMode::ItemAdd { category, buffer } = &self.mode { - (category.clone(), buffer.trim().to_string()) - } else { - return Ok(()); - }; - - if !buf.is_empty() { - let result = command::dispatch( - &mut self.model, - &Command::AddItem { - category: cat.clone(), - item: buf.clone(), - }, - ); - if result.ok { - let count = self - .model - .category(&cat) - .map(|c| c.items.len()) - .unwrap_or(0); - self.status_msg = format!( - "Added \"{buf}\" — {count} items. Enter to add more, Esc to finish." - ); - self.dirty = true; - } else { - self.status_msg = result.message.unwrap_or_default(); - } - } - // Clear buffer but stay in ItemAdd for next entry - if let AppMode::ItemAdd { ref mut buffer, .. } = self.mode { - buffer.clear(); - } - } - KeyCode::Tab => { - // Tab completes the current item and moves to next, same as Enter - return self.handle_item_add_key(crossterm::event::KeyEvent::new( - KeyCode::Enter, - crossterm::event::KeyModifiers::NONE, - )); - } - KeyCode::Char(c) => { - if let AppMode::ItemAdd { ref mut buffer, .. } = self.mode { - buffer.push(c); - } - } - KeyCode::Backspace => { - if let AppMode::ItemAdd { ref mut buffer, .. } = self.mode { - buffer.pop(); - } - } - _ => {} - } - Ok(()) - } - - fn handle_view_panel_key(&mut self, key: KeyEvent) -> Result<()> { - let view_names: Vec = self.model.views.keys().cloned().collect(); - match key.code { - KeyCode::Esc | KeyCode::Tab => { - self.mode = AppMode::Normal; - } - KeyCode::Up | KeyCode::Char('k') => { - if self.view_panel_cursor > 0 { - self.view_panel_cursor -= 1; - } - } - KeyCode::Down | KeyCode::Char('j') => { - if self.view_panel_cursor + 1 < view_names.len() { - self.view_panel_cursor += 1; - } - } - KeyCode::Enter => { - if let Some(name) = view_names.get(self.view_panel_cursor) { - command::dispatch(&mut self.model, &Command::SwitchView { name: name.clone() }); - self.mode = AppMode::Normal; - } - } - KeyCode::Char('n') | KeyCode::Char('o') => { - let new_name = format!("View {}", self.model.views.len() + 1); - command::dispatch( - &mut self.model, - &Command::CreateView { - name: new_name.clone(), - }, - ); - command::dispatch(&mut self.model, &Command::SwitchView { name: new_name }); - self.dirty = true; - self.mode = AppMode::Normal; - } - KeyCode::Delete | KeyCode::Char('d') => { - if let Some(name) = view_names.get(self.view_panel_cursor) { - command::dispatch(&mut self.model, &Command::DeleteView { name: name.clone() }); - if self.view_panel_cursor > 0 { - self.view_panel_cursor -= 1; - } - self.dirty = true; - } - } - _ => {} - } - Ok(()) - } - - fn handle_tile_select_key(&mut self, key: KeyEvent) -> Result<()> { - let cat_names: Vec = self - .model - .category_names() - .into_iter() - .map(String::from) - .collect(); - let cat_idx = if let AppMode::TileSelect { cat_idx } = self.mode { - cat_idx - } else { - 0 - }; - - match key.code { - KeyCode::Esc | KeyCode::Tab => { - self.mode = AppMode::Normal; - } - KeyCode::Left | KeyCode::Char('h') => { - if let AppMode::TileSelect { ref mut cat_idx } = self.mode { - if *cat_idx > 0 { - *cat_idx -= 1; - } - } - } - KeyCode::Right | KeyCode::Char('l') => { - if let AppMode::TileSelect { ref mut cat_idx } = self.mode { - if *cat_idx + 1 < cat_names.len() { - *cat_idx += 1; - } - } - } - KeyCode::Enter | KeyCode::Char(' ') => { - if let Some(name) = cat_names.get(cat_idx) { - self.model.active_view_mut().cycle_axis(name); - self.dirty = true; - } - self.mode = AppMode::Normal; - } - KeyCode::Char('r') => { - if let Some(name) = cat_names.get(cat_idx) { - command::dispatch( - &mut self.model, - &Command::SetAxis { - category: name.clone(), - axis: Axis::Row, - }, - ); - self.dirty = true; - } - self.mode = AppMode::Normal; - } - KeyCode::Char('c') => { - if let Some(name) = cat_names.get(cat_idx) { - command::dispatch( - &mut self.model, - &Command::SetAxis { - category: name.clone(), - axis: Axis::Column, - }, - ); - self.dirty = true; - } - self.mode = AppMode::Normal; - } - KeyCode::Char('p') => { - if let Some(name) = cat_names.get(cat_idx) { - command::dispatch( - &mut self.model, - &Command::SetAxis { - category: name.clone(), - axis: Axis::Page, - }, - ); - self.dirty = true; - } - self.mode = AppMode::Normal; - } - KeyCode::Char('n') => { - if let Some(name) = cat_names.get(cat_idx) { - command::dispatch( - &mut self.model, - &Command::SetAxis { - category: name.clone(), - axis: Axis::None, - }, - ); - self.dirty = true; - } - self.mode = AppMode::Normal; - } - _ => {} - } - Ok(()) - } - - fn handle_export_key(&mut self, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => { - self.mode = AppMode::Normal; - } - KeyCode::Enter => { - let buf = if let AppMode::ExportPrompt { buffer } = &self.mode { - buffer.clone() - } else { - return Ok(()); - }; - let view_name = self.model.active_view.clone(); - match persistence::export_csv(&self.model, &view_name, Path::new(&buf)) { - Ok(_) => { - self.status_msg = format!("Exported to {buf}"); - } - Err(e) => { - self.status_msg = format!("Export error: {e}"); - } - } - self.mode = AppMode::Normal; - } - KeyCode::Char(c) => { - if let AppMode::ExportPrompt { buffer } = &mut self.mode { - buffer.push(c); - } - } - KeyCode::Backspace => { - if let AppMode::ExportPrompt { buffer } = &mut self.mode { - buffer.pop(); - } - } - _ => {} - } - Ok(()) - } fn handle_wizard_key(&mut self, key: KeyEvent) -> Result<()> { if let Some(wizard) = &mut self.wizard {