diff --git a/src/app/cred.rs b/src/app/cred.rs index 4f24ad8..3fc532e 100644 --- a/src/app/cred.rs +++ b/src/app/cred.rs @@ -1,4 +1,4 @@ -use super::{TextEditing, char_len}; +use super::{FormNav, TextEditing, char_len}; use crate::app::AuthKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -82,6 +82,13 @@ impl CredFormState { } } +impl FormNav for CredFormState { + fn nav_next(&mut self) { self.next_field(); } + fn nav_prev(&mut self) { self.prev_field(); } + fn active_is_toggle(&self) -> bool { self.active.is_toggle() } + fn active_is_text(&self) -> bool { self.active.is_text() } +} + impl TextEditing for CredFormState { fn active_text(&self) -> &str { match self.active { diff --git a/src/app/form.rs b/src/app/form.rs index c932379..cee814a 100644 --- a/src/app/form.rs +++ b/src/app/form.rs @@ -1,4 +1,4 @@ -use super::{TextEditing, char_len}; +use super::{FormNav, TextEditing, char_len}; use crate::app::AuthKind; use crate::config::{ConnectionProfile, ConnectionType, CredentialEntry, SshellConfig}; @@ -211,6 +211,13 @@ impl FormState { } } +impl FormNav for FormState { + fn nav_next(&mut self) { self.next_field(); } + fn nav_prev(&mut self) { self.prev_field(); } + fn active_is_toggle(&self) -> bool { self.active.is_toggle() } + fn active_is_text(&self) -> bool { self.active.is_text() } +} + impl TextEditing for FormState { fn active_text(&self) -> &str { match self.active { diff --git a/src/app/settings.rs b/src/app/settings.rs index 370c889..9e40e77 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -1,4 +1,4 @@ -use super::TextEditing; +use super::{FormNav, TextEditing}; use crate::config::SyncBackend; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -128,6 +128,13 @@ impl SettingsState { } } +impl FormNav for SettingsState { + fn nav_next(&mut self) { self.next_field(); } + fn nav_prev(&mut self) { self.prev_field(); } + fn active_is_toggle(&self) -> bool { self.active.is_toggle() } + fn active_is_text(&self) -> bool { self.active.is_text() } +} + impl Default for SettingsState { fn default() -> Self { Self { diff --git a/src/app/types.rs b/src/app/types.rs index db9da3a..1007316 100644 --- a/src/app/types.rs +++ b/src/app/types.rs @@ -76,6 +76,22 @@ pub trait TextEditing { } } +// ── Form navigation trait ──────────────────────────────────── + +pub trait FormNav: TextEditing { + fn nav_next(&mut self); + fn nav_prev(&mut self); + fn active_is_toggle(&self) -> bool; + fn active_is_text(&self) -> bool; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormAction { + Toggle, + Save, + Cancel, +} + pub fn char_len(value: &str) -> usize { value.chars().count() } diff --git a/src/ui.rs b/src/ui.rs index ec11e5b..6772edf 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -69,13 +69,27 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut crate::app::App) { ]) .split(area); - draw_header(frame, app, shell[0]); let content = shell[1].inner(Margin { horizontal: 1, vertical: 1, }); use view::View; + let (title, hints): (&str, Vec<_>) = match app.session.mode { + Mode::Home => (view::HomeListView.title(), view::HomeListView.hints()), + Mode::ActionMenu => (view::ActionMenuView.title(), view::ActionMenuView.hints()), + Mode::Search => (view::SearchView.title(), view::SearchView.hints()), + Mode::QuickSelect => (view::QuickSelectView.title(), view::QuickSelectView.hints()), + Mode::DeleteConfirm => (view::DeleteConfirmView.title(), view::DeleteConfirmView.hints()), + Mode::Form => (view::FormView.title(), view::FormView.hints()), + Mode::ImportSelector => (view::ImportView.title(), view::ImportView.hints()), + Mode::Credentials => (view::CredListView.title(), view::CredListView.hints()), + Mode::CredForm => (view::CredFormView.title(), view::CredFormView.hints()), + Mode::Settings => (view::SettingsView.title(), view::SettingsView.hints()), + }; + + draw_header(frame, app, title, shell[0]); + match app.session.mode { Mode::Home => view::HomeListView.draw(frame, app, content), Mode::ActionMenu => view::HomeListView.draw(frame, app, content), @@ -89,7 +103,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut crate::app::App) { Mode::Settings => view::SettingsView.draw(frame, app, content), } - draw_help(frame, app, shell[2]); + draw_help(frame, &hints, shell[2]); if app.session.mode == Mode::DeleteConfirm { draw_delete_confirm(frame, app); } diff --git a/src/ui/component/common/form_list.rs b/src/ui/component/common/form_list.rs index d5d8be6..8a02c72 100644 --- a/src/ui/component/common/form_list.rs +++ b/src/ui/component/common/form_list.rs @@ -58,7 +58,7 @@ pub fn draw_form_list( format!("{marker} {label:<13} "), Style::default().fg(if active { ACCENT } else { MUTED }), ); - let hint = Span::styled(" Enter toggles", Style::default().fg(BLUE)); + let hint = Span::styled(" Tab toggles", Style::default().fg(BLUE)); let line = if active { Line::from(vec![label_span, badge, hint]).style(Style::default().bg(ACTIVE_BG)) } else { diff --git a/src/ui/component/custom/header.rs b/src/ui/component/custom/header.rs index 5259d6f..76ea6d2 100644 --- a/src/ui/component/custom/header.rs +++ b/src/ui/component/custom/header.rs @@ -1,4 +1,4 @@ -use crate::app::{App, Mode}; +use crate::app::App; use crate::config::SyncBackend; use crate::ui::{ACCENT, BG, BLUE, DIM_BORDER, GREEN, MUTED, ORANGE, PANEL, PURPLE, TEXT}; use ratatui::{ @@ -8,19 +8,7 @@ use ratatui::{ widgets::{Block, Borders, Paragraph, Widget}, }; -pub fn draw_header(frame: &mut ratatui::Frame<'_>, app: &App, area: Rect) { - let mode = match app.session.mode { - Mode::Home => "Home", - Mode::ActionMenu => "Actions", - Mode::Search => "Search", - Mode::QuickSelect => "Quick Select", - Mode::Form => "Editor", - Mode::DeleteConfirm => "Delete", - Mode::ImportSelector => "Import", - Mode::Credentials => "Credentials", - Mode::CredForm => "Cred Editor", - Mode::Settings => "Settings", - }; +pub fn draw_header(frame: &mut ratatui::Frame<'_>, app: &App, title: &str, area: Rect) { let synced = app.config.connections.values().filter(|p| p.sync()).count(); let (sync_text, sync_color) = if synced > 0 { (format!("synced {synced}"), GREEN) @@ -58,7 +46,7 @@ pub fn draw_header(frame: &mut ratatui::Frame<'_>, app: &App, area: Rect) { Style::default().fg(Color::Black).bg(ACCENT).bold(), ), Span::raw(" "), - Span::styled(mode.to_string(), Style::default().fg(TEXT).bold()), + Span::styled(title.to_string(), Style::default().fg(TEXT).bold()), ]; let conn_pill = pill( diff --git a/src/ui/component/custom/help_bar.rs b/src/ui/component/custom/help_bar.rs index 0ae0ed5..edd2225 100644 --- a/src/ui/component/custom/help_bar.rs +++ b/src/ui/component/custom/help_bar.rs @@ -1,4 +1,3 @@ -use crate::app::{App, Mode}; use crate::ui::{ACCENT, DIM_BORDER, MUTED, PANEL}; use ratatui::{ layout::{Alignment, Rect}, @@ -7,124 +6,14 @@ use ratatui::{ widgets::{Paragraph, Widget}, }; -struct Hint { - key: &'static str, - desc: &'static str, -} - -fn home_hints() -> Vec { - vec![ - Hint { key: "j/k", desc: "move" }, - Hint { key: "Tab", desc: "group" }, - Hint { key: "Enter", desc: "connect" }, - Hint { key: "Ctrl+Q", desc: "quick" }, - Hint { key: "/", desc: "search" }, - Hint { key: "a", desc: "add" }, - Hint { key: "e", desc: "edit" }, - Hint { key: "d", desc: "delete" }, - Hint { key: ":", desc: "actions" }, - Hint { key: "q", desc: "quit" }, - ] -} - -fn search_hints() -> Vec { - vec![ - Hint { key: "type", desc: "filter" }, - Hint { key: "j/k", desc: "move" }, - Hint { key: "Esc", desc: "close" }, - ] -} - -fn quick_select_hints() -> Vec { - vec![ - Hint { key: "1-9", desc: "connect" }, - Hint { key: "Tab", desc: "sort" }, - Hint { key: "Esc", desc: "cancel" }, - ] -} - -fn form_hints() -> Vec { - vec![ - Hint { key: "Tab", desc: "next" }, - Hint { key: "Enter", desc: "save" }, - Hint { key: "Esc", desc: "cancel" }, - Hint { key: "Ctrl+U", desc: "clear" }, - ] -} - -fn delete_hints() -> Vec { - vec![ - Hint { key: "Y", desc: "yes" }, - Hint { key: "N", desc: "no" }, - ] -} - -fn credentials_hints() -> Vec { - vec![ - Hint { key: "j/k", desc: "move" }, - Hint { key: "Enter", desc: "edit" }, - Hint { key: "a", desc: "add" }, - Hint { key: "d", desc: "delete" }, - Hint { key: "Esc", desc: "back" }, - ] -} - -fn cred_form_hints() -> Vec { - vec![ - Hint { key: "Tab", desc: "next" }, - Hint { key: "Enter", desc: "save" }, - Hint { key: "Esc", desc: "back" }, - Hint { key: "Ctrl+U", desc: "clear" }, - ] -} - -fn import_hints() -> Vec { - vec![ - Hint { key: "j/k", desc: "move" }, - Hint { key: "Space", desc: "toggle" }, - Hint { key: "a/A", desc: "all/none" }, - Hint { key: "Enter", desc: "import" }, - Hint { key: "Esc", desc: "cancel" }, - ] -} - -fn settings_hints() -> Vec { - vec![ - Hint { key: "type", desc: "edit" }, - Hint { key: "Enter", desc: "save" }, - Hint { key: "Esc", desc: "cancel" }, - ] -} - -fn action_menu_hints() -> Vec { - vec![ - Hint { key: "j/k", desc: "move" }, - Hint { key: "Enter", desc: "select" }, - Hint { key: "Esc", desc: "cancel" }, - ] -} - -pub fn draw_help(frame: &mut ratatui::Frame<'_>, app: &App, area: Rect) { - let hints = match app.session.mode { - Mode::Home => home_hints(), - Mode::ActionMenu => action_menu_hints(), - Mode::Search => search_hints(), - Mode::QuickSelect => quick_select_hints(), - Mode::Form => form_hints(), - Mode::DeleteConfirm => delete_hints(), - Mode::Credentials => credentials_hints(), - Mode::CredForm => cred_form_hints(), - Mode::ImportSelector => import_hints(), - Mode::Settings => settings_hints(), - }; - +pub fn draw_help(frame: &mut ratatui::Frame<'_>, hints: &[(&str, &str)], area: Rect) { let sep_width: usize = 5; // " | " let mut spans: Vec> = Vec::new(); let mut used: usize = 0; let max_w = area.width as usize; for (i, hint) in hints.iter().enumerate() { - let hint_span = key_hint(hint.key, hint.desc); + let hint_span = key_hint(hint.0, hint.1); let needed = hint_span.width() + if i > 0 { sep_width } else { 0 }; if used + needed > max_w { break; diff --git a/src/ui/view.rs b/src/ui/view.rs index 76bf140..37c6cc6 100644 --- a/src/ui/view.rs +++ b/src/ui/view.rs @@ -6,9 +6,9 @@ mod home_list; mod import; mod settings; -use crate::app::App; +use crate::app::{App, FormAction, FormNav}; use anyhow::Result; -use crossterm::event::KeyEvent; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::{ Frame, layout::Rect, @@ -19,6 +19,12 @@ use ratatui::{ pub trait View { fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect); fn handle_key(&self, app: &mut App, key: KeyEvent) -> Result<()>; + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![] + } + fn title(&self) -> &'static str { + "" + } } pub use action_menu::ActionMenuView; @@ -46,3 +52,30 @@ pub fn scroll_rows<'a>(rows: Vec>, selected: usize, area_height: u16) -> }; rows.into_iter().skip(scroll).take(visible).collect() } + +/// Common form key handler. Returns Some(action) for Toggle/Save/Cancel, +/// None for text-editing and navigation keys that are fully handled here. +pub fn handle_form_nav(form: &mut F, key: KeyEvent) -> Option { + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + + match key.code { + KeyCode::Down => { form.nav_next(); None } + KeyCode::Up => { form.nav_prev(); None } + KeyCode::Tab if form.active_is_toggle() => Some(FormAction::Toggle), + KeyCode::Enter => Some(FormAction::Save), + KeyCode::Esc => Some(FormAction::Cancel), + + KeyCode::Backspace if form.active_is_text() => { form.delete_char(); None } + KeyCode::Delete if form.active_is_text() => { form.delete_next_char(); None } + KeyCode::Left if form.active_is_text() => { form.move_cursor_left(); None } + KeyCode::Right if form.active_is_text() => { form.move_cursor_right(); None } + KeyCode::Home if form.active_is_text() => { form.cursor_home(); None } + KeyCode::End if form.active_is_text() => { form.cursor_end(); None } + KeyCode::Char('a') if ctrl && form.active_is_text() => { form.cursor_home(); None } + KeyCode::Char('e') if ctrl && form.active_is_text() => { form.cursor_end(); None } + KeyCode::Char('u') if ctrl && form.active_is_text() => { form.clear_field(); None } + KeyCode::Char(' ') if form.active_is_text() => { form.insert_char(' '); None } + KeyCode::Char(c) if !ctrl && form.active_is_text() => { form.insert_char(c); None } + _ => None, + } +} diff --git a/src/ui/view/action_menu.rs b/src/ui/view/action_menu.rs index 0e6d586..df6eb71 100644 --- a/src/ui/view/action_menu.rs +++ b/src/ui/view/action_menu.rs @@ -24,6 +24,15 @@ const ACTIONS: &[(&str, &str)] = &[ pub struct ActionMenuView; impl View for ActionMenuView { + fn title(&self) -> &'static str { "Actions" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("j/k", "move"), + ("Enter", "select"), + ("Esc", "cancel"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, _app: &App, area: Rect) { let list_width = 52u16.min(area.width.saturating_sub(4)); let list_height = (ACTIONS.len() as u16 + 4).min(area.height); diff --git a/src/ui/view/cred_form.rs b/src/ui/view/cred_form.rs index 03ba787..9ef2433 100644 --- a/src/ui/view/cred_form.rs +++ b/src/ui/view/cred_form.rs @@ -1,16 +1,27 @@ -use crate::app::{App, AuthKind, CredFormField, CredFormState, Mode, TextEditing, char_len}; +use crate::app::{App, AuthKind, CredFormField, CredFormState, FormAction, Mode, char_len}; use crate::ui::component::{FormRow, badge_span}; use crate::ui::{GREEN, ORANGE}; -use super::View; +use super::{View, handle_form_nav}; use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::KeyEvent; use ratatui::{Frame, layout::Rect}; pub struct CredFormView; impl View for CredFormView { + fn title(&self) -> &'static str { "Cred Editor" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("↑/↓", "move"), + ("Tab", "toggle"), + ("Enter", "save"), + ("Esc", "back"), + ("Ctrl+U", "clear"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { let form = &app.session.credentials.form; let is_new = form.edit_name.is_none(); @@ -71,7 +82,7 @@ impl View for CredFormView { } } - let subtitle = "Tab ▽ ↑ △ Enter save/toggle Esc back Ctrl+U clear"; + let subtitle = "↓ ▽ ↑ △ Tab toggle Enter save Esc back Ctrl+U clear"; crate::ui::component::draw_form_list(frame, area, title, subtitle, rows); } @@ -83,55 +94,15 @@ impl View for CredFormView { // ── Key handling ─────────────────────────────────────────────── fn handle_cred_form(app: &mut App, key: KeyEvent) -> Result<()> { - let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - - match key.code { - KeyCode::Esc => { - app.session.mode = Mode::Credentials; + if let Some(action) = handle_form_nav(&mut app.session.credentials.form, key) { + match action { + FormAction::Toggle => toggle_cred_field(&mut app.session.credentials.form), + FormAction::Save => match app.save_cred_form() { + Ok(()) => app.toast("saved", true), + Err(err) => app.toast(err.to_string(), false), + }, + FormAction::Cancel => app.session.mode = Mode::Credentials, } - - KeyCode::Tab | KeyCode::Down => { - app.session.credentials.form.next_field(); - } - KeyCode::BackTab | KeyCode::Up => { - app.session.credentials.form.prev_field(); - } - - KeyCode::Enter => { - if app.session.credentials.form.active.is_toggle() { - toggle_cred_field(&mut app.session.credentials.form); - } else { - match app.save_cred_form() { - Ok(()) => app.toast("saved", true), - Err(err) => app.toast(err.to_string(), false), - } - } - } - - KeyCode::Backspace => { - app.session.credentials.form.delete_char(); - } - KeyCode::Left => app.session.credentials.form.move_cursor_left(), - KeyCode::Right => app.session.credentials.form.move_cursor_right(), - KeyCode::Home => app.session.credentials.form.cursor_home(), - KeyCode::End => app.session.credentials.form.cursor_end(), - KeyCode::Char('a') if ctrl => app.session.credentials.form.cursor_home(), - KeyCode::Char('e') if ctrl => app.session.credentials.form.cursor_end(), - KeyCode::Char('u') if ctrl => app.session.credentials.form.clear_field(), - - KeyCode::Char(' ') => { - if app.session.credentials.form.active.is_toggle() { - toggle_cred_field(&mut app.session.credentials.form); - } else { - app.session.credentials.form.insert_char(' '); - } - } - - KeyCode::Char(c) if !ctrl && !app.session.credentials.form.active.is_toggle() => { - app.session.credentials.form.insert_char(c); - } - - _ => {} } Ok(()) } diff --git a/src/ui/view/cred_list.rs b/src/ui/view/cred_list.rs index d595c1b..af8c27d 100644 --- a/src/ui/view/cred_list.rs +++ b/src/ui/view/cred_list.rs @@ -17,6 +17,17 @@ use ratatui::{ pub struct CredListView; impl View for CredListView { + fn title(&self) -> &'static str { "Credentials" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("j/k", "move"), + ("Enter", "edit"), + ("a", "add"), + ("d", "delete"), + ("Esc", "back"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { draw_credentials(frame, app, area); } diff --git a/src/ui/view/form.rs b/src/ui/view/form.rs index 599fcce..971c6ea 100644 --- a/src/ui/view/form.rs +++ b/src/ui/view/form.rs @@ -1,16 +1,27 @@ -use crate::app::{App, AuthKind, FormField, FormState, Mode, TextEditing, char_len}; +use crate::app::{App, AuthKind, FormAction, FormField, FormState, Mode, char_len}; use crate::ui::component::{FormRow, badge_span}; use crate::ui::{ACCENT, GREEN, ORANGE, PURPLE, RED}; -use super::View; +use super::{View, handle_form_nav}; use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::KeyEvent; use ratatui::{Frame, layout::Rect}; pub struct FormView; impl View for FormView { + fn title(&self) -> &'static str { "Editor" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("↑/↓", "move"), + ("Tab", "toggle"), + ("Enter", "save"), + ("Esc", "cancel"), + ("Ctrl+U", "clear"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { let form = &app.session.form; let fields = form.visible_fields(); @@ -88,7 +99,7 @@ impl View for FormView { } } - let subtitle = "Tab ▽ ↑ △ Enter save/toggle Esc cancel Ctrl+U clear"; + let subtitle = "↓ ▽ ↑ △ Tab toggle Enter save Esc cancel Ctrl+U clear"; crate::ui::component::draw_form_list(frame, area, title, subtitle, rows); } @@ -100,61 +111,15 @@ impl View for FormView { // ── Key handling ─────────────────────────────────────────────── fn handle_form(app: &mut App, key: KeyEvent) -> Result<()> { - let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - - match key.code { - KeyCode::Esc => app.session.mode = Mode::Home, - - KeyCode::Tab | KeyCode::Down => { - app.session.form.next_field(); + if let Some(action) = handle_form_nav(&mut app.session.form, key) { + match action { + FormAction::Toggle => toggle_field(&mut app.session.form), + FormAction::Save => match app.save_form() { + Ok(()) => app.toast("saved", true), + Err(err) => app.toast(err.to_string(), false), + }, + FormAction::Cancel => app.session.mode = Mode::Home, } - KeyCode::BackTab | KeyCode::Up => { - app.session.form.prev_field(); - } - - KeyCode::Enter => { - if app.session.form.active.is_toggle() { - toggle_field(&mut app.session.form); - } else { - match app.save_form() { - Ok(()) => app.toast("saved", true), - Err(err) => app.toast(err.to_string(), false), - } - } - } - - KeyCode::Backspace => { - app.session.form.delete_char(); - } - KeyCode::Delete => { - app.session.form.delete_next_char(); - } - KeyCode::Left => { - app.session.form.move_cursor_left(); - } - KeyCode::Right => { - app.session.form.move_cursor_right(); - } - KeyCode::Home => app.session.form.cursor_home(), - KeyCode::End => app.session.form.cursor_end(), - - KeyCode::Char('a') if ctrl => app.session.form.cursor_home(), - KeyCode::Char('e') if ctrl => app.session.form.cursor_end(), - KeyCode::Char('u') if ctrl => app.session.form.clear_field(), - - KeyCode::Char(' ') => { - if app.session.form.active.is_toggle() { - toggle_field(&mut app.session.form); - } else { - app.session.form.insert_char(' '); - } - } - - KeyCode::Char(c) if !ctrl && !app.session.form.active.is_toggle() => { - app.session.form.insert_char(c); - } - - _ => {} } Ok(()) } diff --git a/src/ui/view/home_list.rs b/src/ui/view/home_list.rs index cee4e16..c49c245 100644 --- a/src/ui/view/home_list.rs +++ b/src/ui/view/home_list.rs @@ -19,6 +19,22 @@ use ratatui::{ pub struct HomeListView; impl View for HomeListView { + fn title(&self) -> &'static str { "Home" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("j/k", "move"), + ("Tab", "group"), + ("Enter", "connect"), + ("Ctrl+Q", "quick"), + ("/", "search"), + ("a", "add"), + ("e", "edit"), + ("d", "delete"), + (":", "actions"), + ("q", "quit"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { let has_search = app.session.mode == Mode::Search; let top_height = if has_search { 3 } else { 0 }; @@ -471,6 +487,15 @@ fn handle_home(app: &mut App, key: KeyEvent) -> Result<()> { pub struct SearchView; impl View for SearchView { + fn title(&self) -> &'static str { "Search" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("type", "filter"), + ("j/k", "move"), + ("Esc", "close"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { HomeListView.draw(frame, app, area); } @@ -505,6 +530,15 @@ impl View for SearchView { pub struct QuickSelectView; impl View for QuickSelectView { + fn title(&self) -> &'static str { "Quick Select" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("1-9", "connect"), + ("Tab", "sort"), + ("Esc", "cancel"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { HomeListView.draw(frame, app, area); } @@ -554,6 +588,14 @@ impl View for QuickSelectView { pub struct DeleteConfirmView; impl View for DeleteConfirmView { + fn title(&self) -> &'static str { "Delete" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("Y", "yes"), + ("N", "no"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { HomeListView.draw(frame, app, area); } diff --git a/src/ui/view/import.rs b/src/ui/view/import.rs index 6dbc643..e0dc225 100644 --- a/src/ui/view/import.rs +++ b/src/ui/view/import.rs @@ -16,6 +16,17 @@ use ratatui::{ pub struct ImportView; impl View for ImportView { + fn title(&self) -> &'static str { "Import" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("j/k", "move"), + ("Space", "toggle"), + ("a/A", "all/none"), + ("Enter", "import"), + ("Esc", "cancel"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { draw_import(frame, app, area); } diff --git a/src/ui/view/settings.rs b/src/ui/view/settings.rs index 0902882..00be4e2 100644 --- a/src/ui/view/settings.rs +++ b/src/ui/view/settings.rs @@ -1,17 +1,27 @@ -use crate::app::{App, Mode, SettingsField, SettingsState, TextEditing, char_len}; +use crate::app::{App, FormAction, Mode, SettingsField, SettingsState, char_len}; use crate::config::SyncBackend; use crate::ui::component::{FormRow, badge_span}; use crate::ui::{ACCENT, GREEN, ORANGE, PURPLE, RED}; -use super::View; +use super::{View, handle_form_nav}; use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::KeyEvent; use ratatui::{Frame, layout::Rect}; pub struct SettingsView; impl View for SettingsView { + fn title(&self) -> &'static str { "Settings" } + fn hints(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("↑/↓", "move"), + ("Tab", "toggle"), + ("Enter", "save"), + ("Esc", "cancel"), + ] + } + fn draw(&self, frame: &mut Frame<'_>, app: &App, area: Rect) { let settings = &app.session.settings; let fields = settings.visible_fields(); @@ -86,7 +96,7 @@ impl View for SettingsView { } } - let subtitle = "Tab ▽ ↑ △ Enter save/toggle Esc cancel Ctrl+U clear"; + let subtitle = "↓ ▽ ↑ △ Tab toggle Enter save Esc cancel Ctrl+U clear"; crate::ui::component::draw_form_list(frame, area, "Settings", subtitle, rows); } @@ -98,73 +108,15 @@ impl View for SettingsView { // ── Key handling ─────────────────────────────────────────────── fn handle_settings(app: &mut App, key: KeyEvent) -> Result<()> { - let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - let settings = &mut app.session.settings; - - match key.code { - KeyCode::Esc => { - app.session.mode = Mode::Home; + if let Some(action) = handle_form_nav(&mut app.session.settings, key) { + match action { + FormAction::Toggle => settings_toggle(&mut app.session.settings), + FormAction::Save => match app.save_settings() { + Ok(()) => app.toast("settings saved", true), + Err(err) => app.toast(err.to_string(), false), + }, + FormAction::Cancel => app.session.mode = Mode::Home, } - - KeyCode::Tab | KeyCode::Down => { - settings.next_field(); - } - KeyCode::BackTab | KeyCode::Up => { - settings.prev_field(); - } - - KeyCode::Enter => { - if settings.active.is_toggle() { - settings_toggle(settings); - } else { - match app.save_settings() { - Ok(()) => app.toast("settings saved", true), - Err(err) => app.toast(err.to_string(), false), - } - } - } - - KeyCode::Backspace if settings.active.is_text() => { - settings.delete_char(); - } - KeyCode::Delete if settings.active.is_text() => { - settings.delete_next_char(); - } - KeyCode::Left if settings.active.is_text() => { - settings.move_cursor_left(); - } - KeyCode::Right if settings.active.is_text() => { - settings.move_cursor_right(); - } - KeyCode::Home if settings.active.is_text() => { - settings.cursor_home(); - } - KeyCode::End if settings.active.is_text() => { - settings.cursor_end(); - } - KeyCode::Char('a') if ctrl && settings.active.is_text() => { - settings.cursor_home(); - } - KeyCode::Char('e') if ctrl && settings.active.is_text() => { - settings.cursor_end(); - } - KeyCode::Char('u') if ctrl && settings.active.is_text() => { - settings.clear_field(); - } - - KeyCode::Char(' ') => { - if settings.active.is_toggle() { - settings_toggle(settings); - } else if settings.active.is_text() { - settings.insert_char(' '); - } - } - - KeyCode::Char(c) if !ctrl && settings.active.is_text() => { - settings.insert_char(c); - } - - _ => {} } Ok(()) }