diff --git a/src/ui/component/common.rs b/src/ui/component/common.rs index 8af25ff..9df1393 100644 --- a/src/ui/component/common.rs +++ b/src/ui/component/common.rs @@ -3,6 +3,7 @@ pub mod dialog; pub mod form_list; pub mod input; pub mod layout; +pub mod list_nav; pub mod panel; pub mod toast; @@ -11,5 +12,6 @@ pub use dialog::*; pub use form_list::{FormRow, draw_form_list}; pub use input::draw_input; pub use layout::*; +pub use list_nav::{ListAction, handle_list_nav}; pub use panel::*; pub use toast::draw_toast; diff --git a/src/ui/component/common/list_nav.rs b/src/ui/component/common/list_nav.rs new file mode 100644 index 0000000..f40004e --- /dev/null +++ b/src/ui/component/common/list_nav.rs @@ -0,0 +1,23 @@ +use crossterm::event::{KeyCode, KeyEvent}; + +pub enum ListAction { + None, + Cancel, + Select, +} + +pub fn handle_list_nav(cursor: &mut usize, len: usize, key: KeyEvent) -> ListAction { + match key.code { + KeyCode::Down | KeyCode::Char('j') if len > 0 => { + *cursor = (*cursor + 1) % len; + ListAction::None + } + KeyCode::Up | KeyCode::Char('k') if len > 0 => { + *cursor = (*cursor + len - 1) % len; + ListAction::None + } + KeyCode::Esc => ListAction::Cancel, + KeyCode::Enter => ListAction::Select, + _ => ListAction::None, + } +} diff --git a/src/ui/view/action_menu.rs b/src/ui/view/action_menu.rs index df6eb71..f0a5006 100644 --- a/src/ui/view/action_menu.rs +++ b/src/ui/view/action_menu.rs @@ -1,4 +1,5 @@ use crate::app::{App, Mode}; +use crate::ui::component::{ListAction, handle_list_nav}; use crate::ui::{ACCENT, MUTED, PANEL, SELECTED_BG, TEXT}; use super::View; @@ -82,20 +83,15 @@ impl View for ActionMenuView { } fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()> { - let len = ACTIONS.len(); - match key.code { - KeyCode::Char(':') | KeyCode::Esc => { + if key.code == KeyCode::Char(':') { + app.session.mode = Mode::Home; + return Ok(()); + } + match handle_list_nav(&mut app.session.action_menu.cursor, ACTIONS.len(), key) { + ListAction::Cancel => { app.session.mode = Mode::Home; } - KeyCode::Down | KeyCode::Char('j') => { - app.session.action_menu.cursor = - (app.session.action_menu.cursor + 1) % len; - } - KeyCode::Up | KeyCode::Char('k') => { - app.session.action_menu.cursor = - (app.session.action_menu.cursor + len - 1) % len; - } - KeyCode::Enter => { + ListAction::Select => { app.session.mode = Mode::Home; match app.session.action_menu.cursor { 0 => app.enter_combined_import()?, diff --git a/src/ui/view/cred_list.rs b/src/ui/view/cred_list.rs index af8c27d..5c0b490 100644 --- a/src/ui/view/cred_list.rs +++ b/src/ui/view/cred_list.rs @@ -1,6 +1,6 @@ use crate::app::{App, Mode}; use crate::config::CredentialEntry; -use crate::ui::component::panel; +use crate::ui::component::{ListAction, handle_list_nav, panel}; use crate::ui::{BLUE, GREEN, MUTED, RED, SELECTED_BG, TEXT}; use super::{View, scroll_rows}; @@ -118,35 +118,18 @@ fn draw_credentials(frame: &mut Frame<'_>, app: &App, area: Rect) { // ── Key handling ─────────────────────────────────────────────── fn handle_credentials(app: &mut App, key: KeyEvent) -> Result<()> { - match key.code { - KeyCode::Esc => app.session.mode = Mode::Home, - KeyCode::Down | KeyCode::Char('j') => { - let len = app.config.credentials.entries.len(); - if len > 0 { - app.session.credentials.selected = (app.session.credentials.selected as isize + 1) - .rem_euclid(len as isize) - as usize; - } - } - KeyCode::Up | KeyCode::Char('k') => { - let len = app.config.credentials.entries.len(); - if len > 0 { - app.session.credentials.selected = (app.session.credentials.selected as isize - 1) - .rem_euclid(len as isize) - as usize; - } - } - KeyCode::Enter => { - app.edit_cred_form(); - } - KeyCode::Char('a') => { - app.new_cred_form(); - } - KeyCode::Char('d') => match app.delete_cred() { - Ok(()) => app.toast("deleted", true), - Err(err) => app.toast(err.to_string(), false), + let len = app.config.credentials.entries.len(); + match handle_list_nav(&mut app.session.credentials.selected, len, key) { + ListAction::Cancel => app.session.mode = Mode::Home, + ListAction::Select => app.edit_cred_form(), + ListAction::None => match key.code { + KeyCode::Char('a') => app.new_cred_form(), + KeyCode::Char('d') => match app.delete_cred() { + Ok(()) => app.toast("deleted", true), + Err(err) => app.toast(err.to_string(), false), + }, + _ => {} }, - _ => {} } Ok(()) } diff --git a/src/ui/view/import.rs b/src/ui/view/import.rs index e0dc225..c6ed3bf 100644 --- a/src/ui/view/import.rs +++ b/src/ui/view/import.rs @@ -1,5 +1,5 @@ use crate::app::App; -use crate::ui::component::{panel, panel_with_subtitle}; +use crate::ui::component::{ListAction, handle_list_nav, panel, panel_with_subtitle}; use crate::ui::{BLUE, GREEN, MUTED, SELECTED_BG, TEXT}; use super::View; @@ -189,48 +189,9 @@ fn handle_import(app: &mut App, key: KeyEvent) -> Result<()> { let shell_len = app.session.import.shell_candidates.len(); let total = shell_len + app.session.import.candidates.len(); - match key.code { - KeyCode::Esc => app.session.mode = crate::app::Mode::Home, - KeyCode::Down | KeyCode::Char('j') if total > 0 => { - app.session.import.cursor = (app.session.import.cursor + 1) % total; - } - KeyCode::Up | KeyCode::Char('k') if total > 0 => { - app.session.import.cursor = (app.session.import.cursor + total - 1) % total; - } - KeyCode::Char(' ') => { - if app.session.import.cursor < shell_len { - let can_toggle = app - .session - .import - .shell_candidates - .get(app.session.import.cursor) - .is_some_and(|c| c.conflict.is_none()); - if can_toggle - && let Some(v) = app.session.import.shell_selected.get_mut(app.session.import.cursor) - { - *v = !*v; - } - } else { - let ssh_idx = app.session.import.cursor - shell_len; - if let Some(v) = app.session.import.selected.get_mut(ssh_idx) { - *v = !*v; - } - } - } - KeyCode::Char('a') => { - app.session - .import - .shell_selected - .iter_mut() - .zip(&app.session.import.shell_candidates) - .for_each(|(sel, c)| *sel = c.conflict.is_none()); - app.session.import.selected.fill(true); - } - KeyCode::Char('A') => { - app.session.import.shell_selected.fill(false); - app.session.import.selected.fill(false); - } - KeyCode::Enter => { + match handle_list_nav(&mut app.session.import.cursor, total, key) { + ListAction::Cancel => app.session.mode = crate::app::Mode::Home, + ListAction::Select => { let mut imported = 0; let picked_shells: Vec<_> = app .session @@ -272,7 +233,43 @@ fn handle_import(app: &mut App, key: KeyEvent) -> Result<()> { } app.session.mode = crate::app::Mode::Home; } - _ => {} + ListAction::None => match key.code { + KeyCode::Char(' ') => { + if app.session.import.cursor < shell_len { + let can_toggle = app + .session + .import + .shell_candidates + .get(app.session.import.cursor) + .is_some_and(|c| c.conflict.is_none()); + if can_toggle + && let Some(v) = + app.session.import.shell_selected.get_mut(app.session.import.cursor) + { + *v = !*v; + } + } else { + let ssh_idx = app.session.import.cursor - shell_len; + if let Some(v) = app.session.import.selected.get_mut(ssh_idx) { + *v = !*v; + } + } + } + KeyCode::Char('a') => { + app.session + .import + .shell_selected + .iter_mut() + .zip(&app.session.import.shell_candidates) + .for_each(|(sel, c)| *sel = c.conflict.is_none()); + app.session.import.selected.fill(true); + } + KeyCode::Char('A') => { + app.session.import.shell_selected.fill(false); + app.session.import.selected.fill(false); + } + _ => {} + }, } Ok(()) }