refactor: extract FormNav trait, unify form key handling, decentralize view titles and hints
This commit is contained in:
+8
-1
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user