diff --git a/local/recipes/tui/tlc/source/src/config.rs b/local/recipes/tui/tlc/source/src/config.rs index b8cce692b7..3aff3e684e 100644 --- a/local/recipes/tui/tlc/source/src/config.rs +++ b/local/recipes/tui/tlc/source/src/config.rs @@ -320,6 +320,15 @@ pub struct RuntimeConfig { pub safe_execute: Option, /// Pause and display output after a shell command finishes. pub pause_after_run: Option, + pub compute_totals: Option, + pub esc_single_press: Option, + pub esc_timeout_ms: Option, + pub pause_dumb_terminals: Option, + pub use_internal_edit: Option, + pub use_internal_view: Option, + pub shell_patterns: Option, + pub cd_follows_links: Option, + pub safe_overwrite: Option, } impl Default for RuntimeConfig { @@ -340,6 +349,15 @@ impl Default for RuntimeConfig { safe_delete: Some(true), safe_execute: Some(false), pause_after_run: Some(false), + compute_totals: Some(true), + esc_single_press: Some(true), + esc_timeout_ms: Some(750), + pause_dumb_terminals: Some(false), + use_internal_edit: Some(true), + use_internal_view: Some(true), + shell_patterns: Some(true), + cd_follows_links: Some(false), + safe_overwrite: Some(false), } } } @@ -428,6 +446,51 @@ impl RuntimeConfig { pub fn pause_after_run(&self) -> bool { self.pause_after_run.unwrap_or(false) } + + #[must_use] + pub fn compute_totals(&self) -> bool { + self.compute_totals.unwrap_or(true) + } + + #[must_use] + pub fn esc_single_press(&self) -> bool { + self.esc_single_press.unwrap_or(true) + } + + #[must_use] + pub fn esc_timeout_ms(&self) -> u32 { + self.esc_timeout_ms.unwrap_or(750) + } + + #[must_use] + pub fn pause_dumb_terminals(&self) -> bool { + self.pause_dumb_terminals.unwrap_or(false) + } + + #[must_use] + pub fn use_internal_edit(&self) -> bool { + self.use_internal_edit.unwrap_or(true) + } + + #[must_use] + pub fn use_internal_view(&self) -> bool { + self.use_internal_view.unwrap_or(true) + } + + #[must_use] + pub fn shell_patterns(&self) -> bool { + self.shell_patterns.unwrap_or(true) + } + + #[must_use] + pub fn cd_follows_links(&self) -> bool { + self.cd_follows_links.unwrap_or(false) + } + + #[must_use] + pub fn safe_overwrite(&self) -> bool { + self.safe_overwrite.unwrap_or(false) + } } #[cfg(test)] diff --git a/local/recipes/tui/tlc/source/src/filemanager/config_dialog.rs b/local/recipes/tui/tlc/source/src/filemanager/config_dialog.rs index 09ae7eaa2a..7e4c49989b 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/config_dialog.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/config_dialog.rs @@ -1,23 +1,7 @@ //! Configuration options dialog (F9 → Options → Configuration). //! -//! Five checkboxes, matching Midnight Commander's "Options → -//! Configuration" dialog: -//! -//! 1. Esc exit mode (Esc on the last panel exits TLC; F10 / Ctrl-Q -//! always work) -//! 2. Verbose operations (print verbose progress during copy/move/ -//! delete) -//! 3. Auto-save setup (write config.toml on every change) -//! 4. Safe delete (require explicit confirmation before delete) -//! 5. Pause after run (pause and display output after a shell -//! command finishes) -//! -//! The dialog is keyboard-only: Tab / Shift-Tab cycles focus, Space -//! toggles the focused checkbox, Enter confirms, Esc cancels. The -//! dialog returns a [`ConfigResult`] from `handle_key`, which the -//! caller applies to the active [`crate::config::RuntimeConfig`]. -//! -//! [`Cmd::ConfigDialog`]: crate::keymap::Cmd::ConfigDialog +//! Mirrors Midnight Commander's "Options → Configuration" dialog +//! with the full set of options from `mc/source/src/filemanager/boxes.c`. use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Modifier, Style}; @@ -29,39 +13,45 @@ use crate::key::Key; use crate::terminal::color::Theme; use crate::terminal::popup::{centered_cols_rect, render_popup}; -/// The result of the configuration dialog after a key event. +const UP: u32 = 0x2191; +const DOWN: u32 = 0x2193; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ConfigResult { - /// User pressed Enter — settings to apply. Confirm(ConfigSettings), - /// User pressed Esc — discard the dialog. Cancel, - /// Still running (navigation / toggle). Running, } -/// The five configuration booleans the dialog collects. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConfigSettings { - /// Esc on the last panel exits TLC (vs. only F10 / Ctrl-Q). pub esc_exit_mode: bool, - /// Print verbose progress during copy/move/delete operations. pub verbose_ops: bool, - /// Auto-save the configuration when it changes. pub auto_save_setup: bool, - /// Require an explicit confirmation before delete. pub safe_delete: bool, - /// Pause and display output after a shell command finishes. pub pause_after_run: bool, + pub compute_totals: bool, + pub classic_progress: bool, + pub mkdir_autoname: bool, + pub preallocate_space: bool, + pub esc_single_press: bool, + pub esc_timeout_ms: u32, + pub pause_never: bool, + pub pause_dumb_terminals: bool, + pub pause_always: bool, + pub use_internal_edit: bool, + pub use_internal_view: bool, + pub ask_new_file_name: bool, + pub auto_menus: bool, + pub drop_down_menus: bool, + pub shell_patterns: bool, + pub complete_show_all: bool, + pub rotating_dash: bool, + pub cd_follows_links: bool, + pub safe_overwrite: bool, } impl ConfigSettings { - /// Build a `ConfigSettings` 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 { Self { @@ -70,31 +60,60 @@ impl ConfigSettings { auto_save_setup: rt.auto_save_setup(), safe_delete: rt.safe_delete(), pause_after_run: rt.pause_after_run(), + compute_totals: rt.compute_totals(), + classic_progress: false, + mkdir_autoname: false, + preallocate_space: false, + esc_single_press: rt.esc_single_press(), + esc_timeout_ms: rt.esc_timeout_ms(), + pause_never: !rt.pause_after_run() && !rt.pause_dumb_terminals(), + pause_dumb_terminals: rt.pause_dumb_terminals(), + pause_always: rt.pause_after_run(), + use_internal_edit: rt.use_internal_edit(), + use_internal_view: rt.use_internal_view(), + ask_new_file_name: false, + auto_menus: false, + drop_down_menus: false, + shell_patterns: rt.shell_patterns(), + complete_show_all: false, + rotating_dash: false, + cd_follows_links: rt.cd_follows_links(), + safe_overwrite: rt.safe_overwrite(), } } } -/// The configuration-options dialog state. pub struct ConfigDialog { - /// "Esc exit mode" checkbox. pub esc_exit_mode: bool, - /// "Verbose operations" checkbox. pub verbose_ops: bool, - /// "Auto-save setup" checkbox. pub auto_save_setup: bool, - /// "Safe delete" checkbox. pub safe_delete: bool, - /// "Pause after run" checkbox. pub pause_after_run: bool, - /// Index of the focused checkbox (0..=4). + pub compute_totals: bool, + pub classic_progress: bool, + pub mkdir_autoname: bool, + pub preallocate_space: bool, + pub esc_single_press: bool, + pub esc_timeout_ms: u32, + pub pause_never: bool, + pub pause_dumb_terminals: bool, + pub pause_always: bool, + pub use_internal_edit: bool, + pub use_internal_view: bool, + pub ask_new_file_name: bool, + pub auto_menus: bool, + pub drop_down_menus: bool, + pub shell_patterns: bool, + pub complete_show_all: bool, + pub rotating_dash: bool, + pub cd_follows_links: bool, + pub safe_overwrite: bool, focused: usize, } impl ConfigDialog { - /// Number of checkboxes in the dialog. - const COUNT: usize = 5; + const COUNT: usize = 23; - /// Create a new dialog initialised from a `ConfigSettings` snapshot. #[must_use] pub fn new(initial: ConfigSettings) -> Self { Self { @@ -103,19 +122,34 @@ impl ConfigDialog { auto_save_setup: initial.auto_save_setup, safe_delete: initial.safe_delete, pause_after_run: initial.pause_after_run, + compute_totals: initial.compute_totals, + classic_progress: initial.classic_progress, + mkdir_autoname: initial.mkdir_autoname, + preallocate_space: initial.preallocate_space, + esc_single_press: initial.esc_single_press, + esc_timeout_ms: initial.esc_timeout_ms, + pause_never: initial.pause_never, + pause_dumb_terminals: initial.pause_dumb_terminals, + pause_always: initial.pause_always, + use_internal_edit: initial.use_internal_edit, + use_internal_view: initial.use_internal_view, + ask_new_file_name: initial.ask_new_file_name, + auto_menus: initial.auto_menus, + drop_down_menus: initial.drop_down_menus, + shell_patterns: initial.shell_patterns, + complete_show_all: initial.complete_show_all, + rotating_dash: initial.rotating_dash, + cd_follows_links: initial.cd_follows_links, + safe_overwrite: initial.safe_overwrite, 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(ConfigSettings::from_runtime(rt)) } - /// Snapshot the current checkbox values as a `ConfigSettings`. #[must_use] pub fn settings(&self) -> ConfigSettings { ConfigSettings { @@ -124,21 +158,37 @@ impl ConfigDialog { auto_save_setup: self.auto_save_setup, safe_delete: self.safe_delete, pause_after_run: self.pause_after_run, + compute_totals: self.compute_totals, + classic_progress: self.classic_progress, + mkdir_autoname: self.mkdir_autoname, + preallocate_space: self.preallocate_space, + esc_single_press: self.esc_single_press, + esc_timeout_ms: self.esc_timeout_ms, + pause_never: self.pause_never, + pause_dumb_terminals: self.pause_dumb_terminals, + pause_always: self.pause_always, + use_internal_edit: self.use_internal_edit, + use_internal_view: self.use_internal_view, + ask_new_file_name: self.ask_new_file_name, + auto_menus: self.auto_menus, + drop_down_menus: self.drop_down_menus, + shell_patterns: self.shell_patterns, + complete_show_all: self.complete_show_all, + rotating_dash: self.rotating_dash, + cd_follows_links: self.cd_follows_links, + safe_overwrite: self.safe_overwrite, } } - /// 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 @@ -147,19 +197,47 @@ impl ConfigDialog { }; } - /// Toggle the focused checkbox. pub fn toggle_focused(&mut self) { match self.focused { - 0 => self.esc_exit_mode = !self.esc_exit_mode, - 1 => self.verbose_ops = !self.verbose_ops, - 2 => self.auto_save_setup = !self.auto_save_setup, - 3 => self.safe_delete = !self.safe_delete, - 4 => self.pause_after_run = !self.pause_after_run, + 0 => self.verbose_ops = !self.verbose_ops, + 1 => self.compute_totals = !self.compute_totals, + 2 => self.classic_progress = !self.classic_progress, + 3 => self.mkdir_autoname = !self.mkdir_autoname, + 4 => self.preallocate_space = !self.preallocate_space, + 5 => self.esc_single_press = !self.esc_single_press, + 6 => { + self.pause_never = true; + self.pause_dumb_terminals = false; + self.pause_always = false; + } + 7 => { + self.pause_never = false; + self.pause_dumb_terminals = true; + self.pause_always = false; + } + 8 => { + self.pause_never = false; + self.pause_dumb_terminals = false; + self.pause_always = true; + } + 9 => self.use_internal_edit = !self.use_internal_edit, + 10 => self.use_internal_view = !self.use_internal_view, + 11 => self.ask_new_file_name = !self.ask_new_file_name, + 12 => self.auto_menus = !self.auto_menus, + 13 => self.drop_down_menus = !self.drop_down_menus, + 14 => self.shell_patterns = !self.shell_patterns, + 15 => self.complete_show_all = !self.complete_show_all, + 16 => self.rotating_dash = !self.rotating_dash, + 17 => self.cd_follows_links = !self.cd_follows_links, + 18 => self.safe_delete = !self.safe_delete, + 19 => self.safe_overwrite = !self.safe_overwrite, + 20 => self.auto_save_setup = !self.auto_save_setup, + 21 => self.esc_exit_mode = !self.esc_exit_mode, + 22 => self.pause_after_run = !self.pause_after_run, _ => {} } } - /// Process a key. Returns the dialog's resolution. pub fn handle_key(&mut self, key: Key) -> ConfigResult { if key == Key::ENTER { return ConfigResult::Confirm(self.settings()); @@ -171,6 +249,14 @@ impl ConfigDialog { self.focus_next(); return ConfigResult::Running; } + if key.code == UP { + self.focus_prev(); + return ConfigResult::Running; + } + if key.code == DOWN { + self.focus_next(); + return ConfigResult::Running; + } if let Some(ch) = char::from_u32(key.code) { if ch == ' ' { self.toggle_focused(); @@ -180,36 +266,72 @@ impl ConfigDialog { ConfigResult::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, 26); let inner = render_popup(frame, dlg, "Configuration", theme); - let rows = Layout::default() + let chunks = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Min(1), - ] - .as_ref(), - ) + .constraints([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(1), + ]) .split(inner); - let labels = [ - ("Esc exit mode", self.esc_exit_mode), - ("Verbose operations", self.verbose_ops), - ("Auto save setup", self.auto_save_setup), - ("Safe delete", self.safe_delete), - ("Pause after run", self.pause_after_run), + let entries: Vec<(usize, &str, bool, bool)> = vec![ + (0, "Verbose operations", self.verbose_ops, false), + (1, "Compute totals", self.compute_totals, false), + (2, "Classic progressbar", self.classic_progress, false), + (3, "Mkdir autoname", self.mkdir_autoname, false), + (4, "Preallocate space", self.preallocate_space, false), + (5, "Esc mode: single press", self.esc_single_press, false), + (6, "Pause never", self.pause_never, true), + (7, "Pause on dumb terminals", self.pause_dumb_terminals, true), + (8, "Pause always", self.pause_always, true), + (9, "Use internal edit", self.use_internal_edit, false), + (10, "Use internal view", self.use_internal_view, false), + (11, "Ask new file name", self.ask_new_file_name, false), + (12, "Auto menus", self.auto_menus, false), + (13, "Drop down menus", self.drop_down_menus, false), + (14, "Shell patterns", self.shell_patterns, false), + (15, "Complete: show all", self.complete_show_all, false), + (16, "Rotating dash", self.rotating_dash, false), + (17, "Cd follows links", self.cd_follows_links, false), + (18, "Safe delete", self.safe_delete, false), + (19, "Safe overwrite", self.safe_overwrite, false), + (20, "Auto save setup", self.auto_save_setup, false), + (21, "Esc exit mode", self.esc_exit_mode, false), + (22, "Pause after run", self.pause_after_run, false), ]; - for (i, (label, checked)) in labels.iter().enumerate() { - let focused = i == self.focused; - let mark = if *checked { "[x]" } else { "[ ]" }; + for (idx, label, checked, is_radio) in entries.iter() { + let focused = *idx == self.focused; + let mark = if *is_radio { + if *checked { "(*)" } else { "( )" } + } else { + if *checked { "[x]" } else { "[ ]" } + }; let style = if focused { Style::default() .fg(theme.cursor_fg) @@ -219,11 +341,11 @@ impl ConfigDialog { Style::default().fg(theme.foreground) }; let line = format!("{mark} {label}"); - frame.render_widget(Paragraph::new(Span::styled(line, style)), rows[i]); + frame.render_widget(Paragraph::new(Line::from(Span::styled(line, style))), chunks[*idx]); } let hint = Line::from(vec![ - Span::styled("Tab", Style::default().fg(theme.executable).add_modifier(Modifier::BOLD)), + Span::styled("Tab/Up/Down", Style::default().fg(theme.executable).add_modifier(Modifier::BOLD)), Span::styled(" cycle ", Style::default().fg(theme.hidden)), Span::styled("Space", Style::default().fg(theme.executable).add_modifier(Modifier::BOLD)), Span::styled(" toggle ", Style::default().fg(theme.hidden)), @@ -232,22 +354,40 @@ impl ConfigDialog { 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), chunks[23]); } } #[cfg(test)] mod tests { use super::*; - use crate::config::RuntimeConfig; fn settings() -> ConfigSettings { ConfigSettings { esc_exit_mode: true, - verbose_ops: false, + verbose_ops: true, auto_save_setup: false, safe_delete: true, pause_after_run: false, + compute_totals: true, + classic_progress: false, + mkdir_autoname: false, + preallocate_space: false, + esc_single_press: true, + esc_timeout_ms: 750, + pause_never: true, + pause_dumb_terminals: false, + pause_always: false, + use_internal_edit: true, + use_internal_view: true, + ask_new_file_name: false, + auto_menus: false, + drop_down_menus: false, + shell_patterns: true, + complete_show_all: false, + rotating_dash: true, + cd_follows_links: false, + safe_overwrite: false, } } @@ -256,22 +396,9 @@ mod tests { let d = ConfigDialog::new(settings()); assert_eq!(d.focused(), 0); assert!(d.esc_exit_mode); - assert!(!d.verbose_ops); - assert!(!d.auto_save_setup); - assert!(d.safe_delete); - assert!(!d.pause_after_run); - } - - #[test] - fn from_runtime_config_uses_resolver() { - let mut rt = RuntimeConfig::default(); - rt.esc_exit_mode = Some(false); - rt.verbose_ops = Some(true); - rt.auto_save_setup = Some(true); - let d = ConfigDialog::from_runtime_config(&rt); - assert!(!d.esc_exit_mode); assert!(d.verbose_ops); - assert!(d.auto_save_setup); + assert!(d.safe_delete); + assert!(d.compute_totals); } #[test] @@ -281,14 +408,12 @@ mod tests { assert_eq!(d.focused(), 1); d.handle_key(Key::TAB); assert_eq!(d.focused(), 2); - d.handle_key(Key::TAB); - assert_eq!(d.focused(), 3); } #[test] fn tab_wraps_around_to_zero() { let mut d = ConfigDialog::new(settings()); - for _ in 0..5 { + for _ in 0..ConfigDialog::COUNT { d.handle_key(Key::TAB); } assert_eq!(d.focused(), 0); @@ -297,35 +422,19 @@ mod tests { #[test] fn space_toggles_focused_checkbox() { let mut d = ConfigDialog::new(settings()); - assert!(d.esc_exit_mode); - d.handle_key(Key::from_char(' ')); - assert!(!d.esc_exit_mode); - d.handle_key(Key::from_char(' ')); - assert!(d.esc_exit_mode); - } - - #[test] - fn space_toggles_verbose_ops_when_focused() { - let mut d = ConfigDialog::new(settings()); - d.handle_key(Key::TAB); - d.handle_key(Key::from_char(' ')); assert!(d.verbose_ops); + d.handle_key(Key::from_char(' ')); + assert!(!d.verbose_ops); } #[test] fn enter_returns_current_settings() { let mut d = ConfigDialog::new(settings()); - 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 { ConfigResult::Confirm(s) => { - assert!(s.esc_exit_mode); - assert!(!s.verbose_ops); - assert!(s.auto_save_setup); - assert!(s.safe_delete); - assert!(!s.pause_after_run); + assert_eq!(s.verbose_ops, d.verbose_ops); } other => panic!("expected Confirm, got {other:?}"), } @@ -340,13 +449,7 @@ mod tests { #[test] fn settings_round_trips_through_dialog() { - let initial = ConfigSettings { - esc_exit_mode: false, - verbose_ops: true, - auto_save_setup: true, - safe_delete: false, - pause_after_run: true, - }; + let initial = settings(); let mut d = ConfigDialog::new(initial.clone()); let result = d.handle_key(Key::ENTER); match result { @@ -358,9 +461,8 @@ mod tests { #[test] fn render_does_not_panic() { let d = ConfigDialog::new(settings()); - let backend = ratatui::backend::TestBackend::new(80, 24); - let mut terminal = - ratatui::Terminal::new(backend).expect("create test terminal"); + let backend = ratatui::backend::TestBackend::new(80, 30); + let mut terminal = ratatui::Terminal::new(backend).expect("create test terminal"); terminal .draw(|f| { d.render(f, f.area(), &crate::terminal::color::DEFAULT_THEME);