refactor: extract FormNav trait, unify form key handling, decentralize view titles and hints

This commit is contained in:
2026-05-27 02:09:44 +08:00
parent d3bd4af634
commit 0d65be65d3
16 changed files with 238 additions and 316 deletions
+8 -1
View File
@@ -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 {
+8 -1
View File
@@ -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 {
+8 -1
View File
@@ -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 {
+16
View File
@@ -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()
}
+16 -2
View File
@@ -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);
}
+1 -1
View File
@@ -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 {
+3 -15
View File
@@ -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(
+2 -113
View File
@@ -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<Hint> {
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<Hint> {
vec![
Hint { key: "type", desc: "filter" },
Hint { key: "j/k", desc: "move" },
Hint { key: "Esc", desc: "close" },
]
}
fn quick_select_hints() -> Vec<Hint> {
vec![
Hint { key: "1-9", desc: "connect" },
Hint { key: "Tab", desc: "sort" },
Hint { key: "Esc", desc: "cancel" },
]
}
fn form_hints() -> Vec<Hint> {
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<Hint> {
vec![
Hint { key: "Y", desc: "yes" },
Hint { key: "N", desc: "no" },
]
}
fn credentials_hints() -> Vec<Hint> {
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<Hint> {
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<Hint> {
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<Hint> {
vec![
Hint { key: "type", desc: "edit" },
Hint { key: "Enter", desc: "save" },
Hint { key: "Esc", desc: "cancel" },
]
}
fn action_menu_hints() -> Vec<Hint> {
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<Span<'static>> = 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;
+35 -2
View File
@@ -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<Row<'a>>, 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<F: FormNav>(form: &mut F, key: KeyEvent) -> Option<FormAction> {
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,
}
}
+9
View File
@@ -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);
+21 -50
View File
@@ -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,56 +94,16 @@ 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;
}
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() {
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::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(())
}
+11
View File
@@ -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);
}
+21 -56
View File
@@ -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,62 +111,16 @@ 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();
}
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() {
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::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(())
}
+42
View File
@@ -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);
}
+11
View File
@@ -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);
}
+20 -68
View File
@@ -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,74 +108,16 @@ 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;
}
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() {
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::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(())
}