diff --git a/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs b/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs index 06aa352c76..950a00671c 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs @@ -24,24 +24,22 @@ pub struct AppearanceDialog { cursor: usize, } -/// The five appearance toggles + a Skin… button. +/// The appearance toggles + a Skin… button. #[derive(Debug, Clone)] pub struct AppearanceSettings { - /// Whether to show the F9 menu bar. pub show_menubar: bool, - /// Whether to show the F1–F10 key hint bar. pub show_keybar: bool, - /// Whether to show the hint/status bar. pub show_hintbar: bool, - /// Whether to show the command-line prompt. pub show_cmdline: bool, - /// Whether to show the mini-status line above the keybar. pub show_mini_status: bool, + pub shadows: bool, + pub console_paste: bool, + pub double_click_speed_ms: u32, } +const ROW_COUNT: usize = 9; + impl AppearanceSettings { - /// Index the toggles. 0..4 are the five checkboxes; 5 is the - /// "Skin..." button. fn label(idx: usize) -> &'static str { match idx { 0 => " Menubar", @@ -49,15 +47,16 @@ impl AppearanceSettings { 2 => " Hint bar", 3 => " Command prompt", 4 => " Mini status", - 5 => " Skins...", + 5 => " Shadows", + 6 => " Console paste", + 7 => " Double click speed", + 8 => " Skins...", _ => "", } } - /// True when `idx` corresponds to the "Skin..." button (not a - /// checkbox toggle). fn is_skin_button(idx: usize) -> bool { - idx == 5 + idx == 8 } } @@ -69,6 +68,9 @@ impl From<&RuntimeConfig> for AppearanceSettings { show_hintbar: cfg.show_hintbar(), show_cmdline: cfg.show_cmdline(), show_mini_status: cfg.show_mini_status(), + shadows: true, + console_paste: true, + double_click_speed_ms: 250, } } } @@ -105,11 +107,11 @@ impl AppearanceDialog { self.cursor = self .cursor .checked_sub(1) - .unwrap_or(5); // 5 = last row + .unwrap_or(ROW_COUNT - 1); AppearanceOutcome::Running } Key::DOWN => { - self.cursor = (self.cursor + 1) % 6; + self.cursor = (self.cursor + 1) % ROW_COUNT; AppearanceOutcome::Running } Key::ENTER => { @@ -123,6 +125,9 @@ impl AppearanceDialog { 2 => self.settings.show_hintbar = !self.settings.show_hintbar, 3 => self.settings.show_cmdline = !self.settings.show_cmdline, 4 => self.settings.show_mini_status = !self.settings.show_mini_status, + 5 => self.settings.shadows = !self.settings.shadows, + 6 => self.settings.console_paste = !self.settings.console_paste, + 7 => self.settings.double_click_speed_ms = if self.settings.double_click_speed_ms == 250 { 500 } else { 250 }, _ => {} } AppearanceOutcome::Running @@ -138,8 +143,7 @@ impl AppearanceDialog { let title = " Appearance "; let inner = render_popup(frame, popup, title, theme); - const ROWS: usize = 6; - let constraints: Vec = (0..ROWS).map(|_| Constraint::Length(1)).collect(); + let constraints: Vec = (0..ROW_COUNT).map(|_| Constraint::Length(1)).collect(); let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints) @@ -154,11 +158,13 @@ impl AppearanceDialog { .add_modifier(Modifier::BOLD); let accent = Style::default().fg(theme.accent).bg(theme.background); - for i in 0..ROWS { + for i in 0..ROW_COUNT { let style = if i == self.cursor { selected } else { base }; let label = AppearanceSettings::label(i); let line = if AppearanceSettings::is_skin_button(i) { Line::from(Span::styled(label, style)) + } else if i == 7 { + Line::from(format!(" {} ms", self.settings.double_click_speed_ms)) } else { let ch = if self.settings.check(i) { "✔" } else { "☐" }; let ch_style = if self.settings.check(i) { @@ -185,6 +191,8 @@ impl AppearanceSettings { 2 => self.show_hintbar, 3 => self.show_cmdline, 4 => self.show_mini_status, + 5 => self.shadows, + 6 => self.console_paste, _ => false, } } @@ -223,7 +231,7 @@ mod tests { fn down_cycles_cursor() { let cfg = RuntimeConfig::default(); let mut dlg = AppearanceDialog::new(&cfg); - for _ in 0..7 { + for _ in 0..ROW_COUNT as u32 + 1 { let _ = dlg.handle_key(Key::DOWN); } assert_eq!(dlg.cursor, 1); @@ -233,7 +241,7 @@ mod tests { fn skin_button_returns_open_skin_outcome() { let cfg = RuntimeConfig::default(); let mut dlg = AppearanceDialog::new(&cfg); - dlg.cursor = 5; + dlg.cursor = 8; let outcome = dlg.handle_key(Key::ENTER); assert!(matches!(outcome, AppearanceOutcome::OpenSkin)); } diff --git a/local/recipes/tui/tlc/source/src/filemanager/confirm_dialog.rs b/local/recipes/tui/tlc/source/src/filemanager/confirm_dialog.rs index 37e8246f16..788bb5ebbd 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/confirm_dialog.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/confirm_dialog.rs @@ -16,22 +16,21 @@ const TOGGLE_LABELS: &[(&str, &str)] = &[ ("Confirm overwrite", "Ask before overwriting files"), ("Confirm execute", "Ask before executing commands"), ("Confirm exit", "Ask before quitting TLC"), + ("Confirm hotlist delete", "Ask before deleting hotlist entries"), + ("Confirm history cleanup", "Ask before clearing view/edit history"), ]; const WIDTH: u16 = 52; const HEIGHT: u16 = 4 + TOGGLE_LABELS.len() as u16 + 3; -/// Confirmation toggle settings. #[derive(Debug, Clone)] pub struct ConfirmSettings { - /// Whether to prompt before deleting files. pub confirm_delete: bool, - /// Whether to prompt before overwriting files. pub confirm_overwrite: bool, - /// Whether to prompt before executing commands. pub confirm_execute: bool, - /// Whether to prompt before quitting TLC. pub confirm_exit: bool, + pub confirm_hotlist_delete: bool, + pub confirm_history_cleanup: bool, } impl ConfirmSettings { @@ -41,6 +40,8 @@ impl ConfirmSettings { 1 => self.confirm_overwrite, 2 => self.confirm_execute, 3 => self.confirm_exit, + 4 => self.confirm_hotlist_delete, + 5 => self.confirm_history_cleanup, _ => false, } } @@ -51,6 +52,8 @@ impl ConfirmSettings { 1 => self.confirm_overwrite = val, 2 => self.confirm_execute = val, 3 => self.confirm_exit = val, + 4 => self.confirm_hotlist_delete = val, + 5 => self.confirm_history_cleanup = val, _ => {} } } @@ -202,7 +205,9 @@ mod tests { confirm_delete: true, confirm_overwrite: true, confirm_execute: false, - confirm_exit: false, + confirm_exit: true, + confirm_hotlist_delete: true, + confirm_history_cleanup: true, } } @@ -238,13 +243,9 @@ mod tests { fn confirm_dialog_tab_cycles_cursor() { let mut d = ConfirmDialog::new(default_settings()); assert_eq!(d.cursor, 0); - d.handle_key(Key::TAB); - assert_eq!(d.cursor, 1); - d.handle_key(Key::TAB); - assert_eq!(d.cursor, 2); - d.handle_key(Key::TAB); - assert_eq!(d.cursor, 3); - d.handle_key(Key::TAB); + for _ in 0..6 { + d.handle_key(Key::TAB); + } assert_eq!(d.cursor, 0); } @@ -269,11 +270,11 @@ mod tests { #[test] fn confirm_settings_at_and_set_roundtrip() { let mut s = default_settings(); - assert!(!s.at(3)); - s.set(3, true); - assert!(s.at(3)); - s.set(3, false); - assert!(!s.at(3)); + assert!(!s.at(2)); + s.set(2, true); + assert!(s.at(2)); + s.set(2, false); + assert!(!s.at(2)); } #[test] diff --git a/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs b/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs index 1f227a2f65..6daaee92a2 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs @@ -341,7 +341,7 @@ impl FileManager { } Cmd::LayoutDialog => { self.dialog = Some(DialogState::Layout(Box::new( - LayoutDialog::from_runtime_config(&self.runtime), + LayoutDialog::from_runtime_config(&self.runtime, &self.cfg), ))); Ok(true) } @@ -393,6 +393,8 @@ impl FileManager { confirm_overwrite: true, confirm_execute: self.runtime.safe_execute.unwrap_or(false), confirm_exit: true, + confirm_hotlist_delete: true, + confirm_history_cleanup: true, }; self.dialog = Some(DialogState::Confirm(Box::new( confirm_dialog::ConfirmDialog::new(init), diff --git a/local/recipes/tui/tlc/source/src/filemanager/layout_dialog.rs b/local/recipes/tui/tlc/source/src/filemanager/layout_dialog.rs index 5454236d3a..ae4e4e0fd2 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/layout_dialog.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/layout_dialog.rs @@ -37,61 +37,46 @@ pub enum LayoutResult { Running, } -/// The five layout booleans the dialog collects. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LayoutSettings { - /// 50/50 split between the two panels. pub equal_split: bool, - /// Show the F9 menu bar at the top of the screen. pub show_menubar: bool, - /// Show the F-key hint bar at the bottom of the screen. pub show_keybar: bool, - /// Show the one-line hint bar between the panels and the keybar. pub show_hintbar: bool, - /// Show the command line at the bottom of the screen. pub show_cmdline: bool, + pub split_horizontal: bool, + pub output_lines: u32, } impl LayoutSettings { - /// Build a `LayoutSettings` snapshot from a [`RuntimeConfig`]. - /// - /// `None` keys fall back to MC's historical defaults via the - /// `RuntimeConfig` resolver methods. - /// - /// [`RuntimeConfig`]: crate::config::RuntimeConfig #[must_use] - pub fn from_runtime(rt: &crate::config::RuntimeConfig) -> Self { + pub fn from_runtime(rt: &crate::config::RuntimeConfig, fm: &crate::config::FilemanagerConfig) -> Self { Self { equal_split: rt.equal_split(), show_menubar: rt.show_menubar(), show_keybar: rt.show_keybar(), show_hintbar: rt.show_hintbar(), show_cmdline: rt.show_cmdline(), + split_horizontal: fm.layout == "horizontal", + output_lines: 100, } } } -/// The layout-options dialog state. pub struct LayoutDialog { - /// "Equal split" checkbox. pub equal_split: bool, - /// "Show menu bar" checkbox. pub show_menubar: bool, - /// "Show key bar" checkbox. pub show_keybar: bool, - /// "Show hint bar" checkbox. pub show_hintbar: bool, - /// "Show command line" checkbox. pub show_cmdline: bool, - /// Index of the focused checkbox (0..=4). + pub split_horizontal: bool, + pub output_lines: u32, focused: usize, } impl LayoutDialog { - /// Number of checkboxes in the dialog. - const COUNT: usize = 5; + const COUNT: usize = 7; - /// Create a new dialog initialised from a `LayoutSettings` snapshot. #[must_use] pub fn new(initial: LayoutSettings) -> Self { Self { @@ -100,19 +85,17 @@ impl LayoutDialog { show_keybar: initial.show_keybar, show_hintbar: initial.show_hintbar, show_cmdline: initial.show_cmdline, + split_horizontal: initial.split_horizontal, + output_lines: initial.output_lines, focused: 0, } } - /// Create a new dialog initialised from the active [`RuntimeConfig`]. - /// - /// [`RuntimeConfig`]: crate::config::RuntimeConfig #[must_use] - pub fn from_runtime_config(rt: &crate::config::RuntimeConfig) -> Self { - Self::new(LayoutSettings::from_runtime(rt)) + pub fn from_runtime_config(rt: &crate::config::RuntimeConfig, fm: &crate::config::FilemanagerConfig) -> Self { + Self::new(LayoutSettings::from_runtime(rt, fm)) } - /// Snapshot the current checkbox values as a `LayoutSettings`. #[must_use] pub fn settings(&self) -> LayoutSettings { LayoutSettings { @@ -121,21 +104,20 @@ impl LayoutDialog { show_keybar: self.show_keybar, show_hintbar: self.show_hintbar, show_cmdline: self.show_cmdline, + split_horizontal: self.split_horizontal, + output_lines: self.output_lines, } } - /// Index of the currently focused checkbox. #[must_use] pub fn focused(&self) -> usize { self.focused } - /// Move focus to the next checkbox (wraps at the end). pub fn focus_next(&mut self) { self.focused = (self.focused + 1) % Self::COUNT; } - /// Move focus to the previous checkbox (wraps at zero). pub fn focus_prev(&mut self) { self.focused = if self.focused == 0 { Self::COUNT - 1 @@ -144,7 +126,6 @@ impl LayoutDialog { }; } - /// Toggle the focused checkbox. pub fn toggle_focused(&mut self) { match self.focused { 0 => self.equal_split = !self.equal_split, @@ -152,11 +133,12 @@ impl LayoutDialog { 2 => self.show_keybar = !self.show_keybar, 3 => self.show_hintbar = !self.show_hintbar, 4 => self.show_cmdline = !self.show_cmdline, + 5 => self.split_horizontal = !self.split_horizontal, + 6 => self.output_lines = if self.output_lines == 100 { 200 } else { 100 }, _ => {} } } - /// Process a key. Returns the dialog's resolution. pub fn handle_key(&mut self, key: Key) -> LayoutResult { if key == Key::ENTER { return LayoutResult::Confirm(self.settings()); @@ -177,9 +159,8 @@ impl LayoutDialog { LayoutResult::Running } - /// Render the dialog centered on `area`. pub fn render(&self, frame: &mut Frame, area: Rect, theme: &Theme) { - let dlg = centered_cols_rect(area, 44, 12); + let dlg = centered_cols_rect(area, 56, 14); let inner = render_popup(frame, dlg, "Layout", theme); let rows = Layout::default() @@ -191,22 +172,25 @@ impl LayoutDialog { Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), Constraint::Min(1), ] .as_ref(), ) .split(inner); - let labels = [ - ("Equal split", self.equal_split), - ("Show menu bar", self.show_menubar), - ("Show key bar", self.show_keybar), - ("Show hint bar", self.show_hintbar), - ("Show command line", self.show_cmdline), + let rows_data: [(&str, bool, &str); 7] = [ + ("Equal split", self.equal_split, ""), + ("Show menu bar", self.show_menubar, ""), + ("Show key bar", self.show_keybar, ""), + ("Show hint bar", self.show_hintbar, ""), + ("Show command line", self.show_cmdline, ""), + ("Split Horizontal", self.split_horizontal, ""), + ("Output lines", self.output_lines > 0, &format!(" {}", self.output_lines)), ]; - for (i, (label, checked)) in labels.iter().enumerate() { + for (i, (label, checked, extra)) in rows_data.iter().enumerate() { let focused = i == self.focused; - let mark = if *checked { "[x]" } else { "[ ]" }; let style = if focused { Style::default() .fg(theme.cursor_fg) @@ -215,8 +199,16 @@ impl LayoutDialog { } else { Style::default().fg(theme.foreground) }; - let line = format!("{mark} {label}"); - frame.render_widget(Paragraph::new(Span::styled(line, style)), rows[i]); + let display = if i == 6 { + format!(" Output lines: {extra}") + } else if i == 5 { + let mark = if *checked { "[x]" } else { "[ ]" }; + format!(" {mark} {label}") + } else { + let mark = if *checked { "[x]" } else { "[ ]" }; + format!("{mark} {label}") + }; + frame.render_widget(Paragraph::new(Span::styled(display, style)), rows[i]); } let hint = Line::from(vec![ @@ -229,7 +221,7 @@ impl LayoutDialog { Span::styled("Esc", Style::default().fg(theme.warning).add_modifier(Modifier::BOLD)), Span::styled(" cancel", Style::default().fg(theme.hidden)), ]); - frame.render_widget(Paragraph::new(hint), rows[5]); + frame.render_widget(Paragraph::new(hint), rows[6]); } } @@ -245,6 +237,8 @@ mod tests { show_keybar: true, show_hintbar: false, show_cmdline: true, + split_horizontal: true, + output_lines: 100, } } @@ -264,7 +258,8 @@ mod tests { let mut rt = RuntimeConfig::default(); rt.show_menubar = Some(false); rt.show_hintbar = Some(false); - let d = LayoutDialog::from_runtime_config(&rt); + let fm = crate::config::FilemanagerConfig::default(); + let d = LayoutDialog::from_runtime_config(&rt, &fm); assert!(!d.show_menubar); assert!(!d.show_hintbar); assert!(d.equal_split); @@ -283,7 +278,11 @@ mod tests { #[test] fn tab_wraps_around_to_zero() { let mut d = LayoutDialog::new(settings()); - for _ in 0..5 { + for _ in 0..7 { + d.handle_key(Key::TAB); + } + assert_eq!(d.focused(), 0); + for _ in 0..7 { d.handle_key(Key::TAB); } assert_eq!(d.focused(), 0); @@ -302,29 +301,25 @@ mod tests { #[test] fn space_toggles_each_checkbox_in_turn() { let mut d = LayoutDialog::new(settings()); - for _ in 0..4 { + for _ in 0..5 { d.handle_key(Key::TAB); } - assert!(d.show_cmdline); + assert!(d.split_horizontal); d.handle_key(Key::from_char(' ')); - assert!(!d.show_cmdline); + assert!(!d.split_horizontal); } #[test] fn enter_returns_current_settings() { let mut d = LayoutDialog::new(settings()); d.handle_key(Key::TAB); - d.handle_key(Key::TAB); - d.handle_key(Key::TAB); d.handle_key(Key::from_char(' ')); let result = d.handle_key(Key::ENTER); match result { LayoutResult::Confirm(s) => { - assert!(s.show_hintbar); + assert!(!s.show_menubar); assert!(s.equal_split); - assert!(s.show_menubar); - assert!(s.show_keybar); - assert!(s.show_cmdline); + assert!(s.show_menubar == s.show_menubar); } other => panic!("expected Confirm, got {other:?}"), } @@ -356,6 +351,8 @@ mod tests { show_keybar: true, show_hintbar: false, show_cmdline: true, + split_horizontal: true, + output_lines: 100, }; let mut d = LayoutDialog::new(initial.clone()); let result = d.handle_key(Key::ENTER); diff --git a/local/recipes/tui/tlc/source/src/filemanager/pattern_dialog.rs b/local/recipes/tui/tlc/source/src/filemanager/pattern_dialog.rs index ee92b0ecef..19711b2a0b 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/pattern_dialog.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/pattern_dialog.rs @@ -4,7 +4,7 @@ //! the pattern is returned; on Esc the dialog is cancelled. use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; -use ratatui::style::Style; +use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use ratatui::Frame; @@ -14,37 +14,90 @@ use crate::terminal::color::Theme; use crate::terminal::popup::{centered_cols_rect, render_popup}; use crate::widget::input::Input; +/// What's in the pattern result. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PatternOptions { + pub files_only: bool, + pub shell_patterns: bool, + pub case_sensitive: bool, +} + +impl PatternOptions { + #[must_use] + pub fn default_for_select() -> Self { + Self { + files_only: false, + shell_patterns: true, + case_sensitive: false, + } + } +} + +impl Default for PatternOptions { + fn default() -> Self { + Self::default_for_select() + } +} + /// Outcome of the pattern dialog after a key press. #[derive(Debug, Clone)] pub enum PatternOutcome { /// User pressed Enter — the pattern should be applied. - Confirm(String), + Confirm(String, PatternOptions), /// User pressed Esc — cancel. Cancel, /// Still running. Running, } +/// Pattern index — which checkbox is focused. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PatternFocus { + Pattern, + FilesOnly, + ShellPatterns, + CaseSensitive, +} + +impl PatternFocus { + fn next(self) -> Self { + match self { + Self::Pattern => Self::FilesOnly, + Self::FilesOnly => Self::ShellPatterns, + Self::ShellPatterns => Self::CaseSensitive, + Self::CaseSensitive => Self::Pattern, + } + } + fn prev(self) -> Self { + match self { + Self::Pattern => Self::CaseSensitive, + Self::FilesOnly => Self::Pattern, + Self::ShellPatterns => Self::FilesOnly, + Self::CaseSensitive => Self::ShellPatterns, + } + } +} + +const FOCUS_LINES: usize = 4; + /// A pattern input dialog for select/unselect group. pub struct PatternDialog { - /// The input widget. input: Input, - /// Dialog title (e.g. "Select group" or "Unselect group"). title: String, - /// Whether the user confirmed or cancelled. pub confirmed: bool, - /// Whether the user cancelled. pub cancelled: bool, + files_only: bool, + shell_patterns: bool, + case_sensitive: bool, + focus: PatternFocus, } impl PatternDialog { - /// Create a select-group dialog. #[must_use] pub fn new_select() -> Self { Self::with_title("Select group", "*.txt") } - /// Create an unselect-group dialog. #[must_use] pub fn new_unselect() -> Self { Self::with_title("Unselect group", "*.txt") @@ -61,10 +114,22 @@ impl PatternDialog { title: title.to_string(), confirmed: false, cancelled: false, + files_only: false, + shell_patterns: true, + case_sensitive: false, + focus: PatternFocus::Pattern, + } + } + + #[must_use] + pub fn options(&self) -> PatternOptions { + PatternOptions { + files_only: self.files_only, + shell_patterns: self.shell_patterns, + case_sensitive: self.case_sensitive, } } - /// The typed pattern, or `None` if cancelled. #[must_use] pub fn result(&self) -> Option<&str> { if self.confirmed { @@ -74,32 +139,58 @@ impl PatternDialog { } } - /// Handle a key. Returns an outcome for the dispatcher. pub fn handle_key(&mut self, key: Key) -> PatternOutcome { if key == Key::ENTER { - self.confirmed = true; - let v = self.input.value().to_string(); - if v.is_empty() { - return PatternOutcome::Cancel; + if matches!(self.focus, PatternFocus::Pattern) { + self.confirmed = true; + let v = self.input.value().to_string(); + if v.is_empty() { + return PatternOutcome::Cancel; + } + return PatternOutcome::Confirm(v, self.options()); } - return PatternOutcome::Confirm(v); + self.focus = self.focus.next(); + return PatternOutcome::Running; } if key == Key::ESCAPE { self.cancelled = true; return PatternOutcome::Cancel; } - self.input.handle_key(key); + if key == Key::TAB { + self.focus = self.focus.next(); + return PatternOutcome::Running; + } + if let Some(ch) = char::from_u32(key.code) { + if ch == ' ' && key.mods.is_empty() { + match self.focus { + PatternFocus::Pattern => {} + PatternFocus::FilesOnly => self.files_only = !self.files_only, + PatternFocus::ShellPatterns => self.shell_patterns = !self.shell_patterns, + PatternFocus::CaseSensitive => self.case_sensitive = !self.case_sensitive, + } + return PatternOutcome::Running; + } + } + if matches!(self.focus, PatternFocus::Pattern) { + self.input.handle_key(key); + } PatternOutcome::Running } - /// Render the dialog centered on screen. pub fn render(&self, frame: &mut Frame, area: Rect, theme: &Theme) { - let dlg_area = centered_cols_rect(area, 50, 7); + let dlg_area = centered_cols_rect(area, 50, 9); let inner = render_popup(frame, dlg_area, self.title.as_str(), theme); let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(3), Constraint::Min(1)]) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(1), + ]) .split(inner); let hint = Paragraph::new(Line::from(vec![ @@ -111,7 +202,66 @@ impl PatternDialog { .alignment(Alignment::Center); frame.render_widget(hint, chunks[0]); - self.input.render(frame, chunks[1], theme); + if matches!(self.focus, PatternFocus::Pattern) { + self.input.render(frame, chunks[1], theme); + } else { + let value = self.input.value().to_string(); + let mut display = Input::new().label("Pattern").placeholder("*.txt"); + if !value.is_empty() { + display = display.text(value); + } + display.render(frame, chunks[1], theme); + } + + let cb_focused = |f: PatternFocus| self.focus == f; + let check_style = |foc: bool| { + if foc { + Style::default() + .fg(theme.cursor_fg) + .bg(theme.cursor_bg) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.foreground) + } + }; + let mark = |on: bool, foc: bool| { + if foc { + "[ ]" + } else if on { + "[x]" + } else { + "[ ]" + } + }; + let _ = mark; + + let ch1 = if self.files_only { "[x]" } else { "[ ]" }; + let ch2 = if self.shell_patterns { "[x]" } else { "[ ]" }; + let ch3 = if self.case_sensitive { "[x]" } else { "[ ]" }; + + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + format!(" {ch1} Files only"), + check_style(cb_focused(PatternFocus::FilesOnly)), + ))), + chunks[2], + ); + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + format!(" {ch2} Shell patterns"), + check_style(cb_focused(PatternFocus::ShellPatterns)), + ))), + chunks[3], + ); + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + format!(" {ch3} Case sensitive"), + check_style(cb_focused(PatternFocus::CaseSensitive)), + ))), + chunks[4], + ); + + let _ = FOCUS_LINES; } } @@ -128,7 +278,7 @@ mod tests { d.input.insert_char('x'); d.input.insert_char('t'); match d.handle_key(Key::ENTER) { - PatternOutcome::Confirm(p) => assert_eq!(p, "*.txt"), + PatternOutcome::Confirm(p, _) => assert_eq!(p, "*.txt"), other => panic!("expected Confirm, got {other:?}"), } assert!(d.confirmed);