refactor: extract ListNav component, unify list key handling across views

This commit is contained in:
2026-05-27 10:41:58 +08:00
parent 0d65be65d3
commit 01c8f7f8fc
5 changed files with 86 additions and 85 deletions
+2
View File
@@ -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;
+23
View File
@@ -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,
}
}
+8 -12
View File
@@ -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()?,
+7 -24
View File
@@ -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();
}
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(())
}
+40 -43
View File
@@ -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(())
}