diff --git a/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs b/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs new file mode 100644 index 0000000000..06aa352c76 --- /dev/null +++ b/local/recipes/tui/tlc/source/src/filemanager/appearance_dialog.rs @@ -0,0 +1,240 @@ +//! Appearance dialog (F9 → Options → Appearance). +//! +//! Mirrors MC's CK_OptionsAppearance — checkbox toggles for the +//! menubar, command prompt, keybar, hint bar, and mini-status bar +//! visibility, plus a "Skin..." button that opens the existing +//! [`SkinDialog`]. + +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::config::RuntimeConfig; +use crate::key::Key; +use crate::terminal::color::Theme; +use crate::terminal::popup::{centered_percent_rect, render_popup}; + +/// The appearance checkbox dialog. +pub struct AppearanceDialog { + /// Current toggle state for each option. + pub settings: AppearanceSettings, + /// Currently selected row. + cursor: usize, +} + +/// The five 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, +} + +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", + 1 => " Keybar", + 2 => " Hint bar", + 3 => " Command prompt", + 4 => " Mini status", + 5 => " Skins...", + _ => "", + } + } + + /// True when `idx` corresponds to the "Skin..." button (not a + /// checkbox toggle). + fn is_skin_button(idx: usize) -> bool { + idx == 5 + } +} + +impl From<&RuntimeConfig> for AppearanceSettings { + fn from(cfg: &RuntimeConfig) -> Self { + Self { + show_menubar: cfg.show_menubar(), + show_keybar: cfg.show_keybar(), + show_hintbar: cfg.show_hintbar(), + show_cmdline: cfg.show_cmdline(), + show_mini_status: cfg.show_mini_status(), + } + } +} + +/// Outcome of a key press in the appearance dialog. +#[derive(Debug, Clone)] +pub enum AppearanceOutcome { + /// The "Skin..." button was activated. + OpenSkin, + /// Settings were committed (Enter on the last row or + /// explicit Ok button). + Confirm(AppearanceSettings), + /// Dismissed without saving. + Cancel, + /// Dialog is still active (key consumed but no action taken). + Running, +} + +impl AppearanceDialog { + /// Create a new appearance dialog initialised from `cfg`. + #[must_use] + pub fn new(cfg: &RuntimeConfig) -> Self { + Self { + settings: AppearanceSettings::from(cfg), + cursor: 0, + } + } + + /// Handle a key event. Returns the outcome. + pub fn handle_key(&mut self, key: Key) -> AppearanceOutcome { + match key { + Key::ESCAPE => AppearanceOutcome::Cancel, + Key::UP => { + self.cursor = self + .cursor + .checked_sub(1) + .unwrap_or(5); // 5 = last row + AppearanceOutcome::Running + } + Key::DOWN => { + self.cursor = (self.cursor + 1) % 6; + AppearanceOutcome::Running + } + Key::ENTER => { + if AppearanceSettings::is_skin_button(self.cursor) { + AppearanceOutcome::OpenSkin + } else { + // Toggle the checkbox. + match self.cursor { + 0 => self.settings.show_menubar = !self.settings.show_menubar, + 1 => self.settings.show_keybar = !self.settings.show_keybar, + 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, + _ => {} + } + AppearanceOutcome::Running + } + } + _ => AppearanceOutcome::Running, + } + } + + /// Render the dialog. + pub fn render(&self, frame: &mut Frame, area: Rect, theme: &Theme) { + let popup = centered_percent_rect(area, 0.5, 0.55); + 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 chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(inner); + + let base = Style::default() + .fg(theme.foreground) + .bg(theme.background); + let selected = Style::default() + .fg(theme.cursor_fg) + .bg(theme.cursor_bg) + .add_modifier(Modifier::BOLD); + let accent = Style::default().fg(theme.accent).bg(theme.background); + + for i in 0..ROWS { + 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 { + let ch = if self.settings.check(i) { "✔" } else { "☐" }; + let ch_style = if self.settings.check(i) { + accent.add_modifier(Modifier::BOLD) + } else { + base + }; + Line::from(vec![ + Span::styled(format!(" [{}] ", ch), ch_style), + Span::styled(label, style), + ]) + }; + let p = Paragraph::new(line).style(style); + frame.render_widget(p, chunks[i]); + } + } +} + +impl AppearanceSettings { + fn check(&self, idx: usize) -> bool { + match idx { + 0 => self.show_menubar, + 1 => self.show_keybar, + 2 => self.show_hintbar, + 3 => self.show_cmdline, + 4 => self.show_mini_status, + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_dialog_has_all_toggles_default_true() { + let cfg = RuntimeConfig::default(); + let dlg = AppearanceDialog::new(&cfg); + assert!(dlg.settings.show_menubar); + assert!(dlg.settings.show_keybar); + assert!(dlg.settings.show_hintbar); + assert!(dlg.settings.show_cmdline); + assert!(dlg.settings.show_mini_status); + } + + #[test] + fn enter_toggles_checkbox() { + let cfg = RuntimeConfig::default(); + let mut dlg = AppearanceDialog::new(&cfg); + // Toggle menubar off. + dlg.cursor = 0; + let outcome = dlg.handle_key(Key::ENTER); + assert!(matches!(outcome, AppearanceOutcome::Running)); + assert!(!dlg.settings.show_menubar); + // Toggle it back on. + let _ = dlg.handle_key(Key::ENTER); + assert!(dlg.settings.show_menubar); + } + + #[test] + fn down_cycles_cursor() { + let cfg = RuntimeConfig::default(); + let mut dlg = AppearanceDialog::new(&cfg); + for _ in 0..7 { + let _ = dlg.handle_key(Key::DOWN); + } + assert_eq!(dlg.cursor, 1); + } + + #[test] + fn skin_button_returns_open_skin_outcome() { + let cfg = RuntimeConfig::default(); + let mut dlg = AppearanceDialog::new(&cfg); + dlg.cursor = 5; + let outcome = dlg.handle_key(Key::ENTER); + assert!(matches!(outcome, AppearanceOutcome::OpenSkin)); + } +} diff --git a/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs b/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs index bf0798d0ae..78bd545480 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs @@ -985,6 +985,7 @@ Err(e) => self.status.set_error(format!("view: {e}")), | Some(DialogState::Encoding(_)) | Some(DialogState::Connection(_)) | Some(DialogState::Sort(_)) + | Some(DialogState::Appearance(_)) | Some(DialogState::Confirm(_)) | Some(DialogState::Progress(_)) => { // These are closed by their own apply_*_outcome @@ -1421,6 +1422,7 @@ fn dialog_label(d: &DialogState) -> String { DialogState::Encoding(_) => "Display encoding".to_string(), DialogState::Connection(_) => "Network connection".to_string(), DialogState::Sort(_) => "Sort order".to_string(), + DialogState::Appearance(_) => "Appearance".to_string(), DialogState::Confirm(_) => "Confirmation".to_string(), DialogState::Progress(_) => "Progress".to_string(), } diff --git a/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs b/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs index 4fcbd90ff4..a7993365c6 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/dispatch.rs @@ -12,11 +12,12 @@ use std::sync::Arc; use anyhow::Result; use crate::filemanager::{ - chattr_dialog, compare, config_dialog, connection_dialog, display_bits_dialog, - edit_history, encoding_dialog, external_panelize, filter_dialog, filtered_view, - find, format_utils, hotlist, jobs, learn_keys_dialog, layout_dialog, link, - menubar, panel_options, quickcd_dialog, screen_list, skin_dialog, - tree, usermenu, vfs_list, vfs_settings_dialog, Cmd, DialogState, FileManager, + appearance_dialog, chattr_dialog, compare, config_dialog, confirm_dialog, + connection_dialog, display_bits_dialog, edit_history, encoding_dialog, + external_panelize, filter_dialog, filtered_view, find, format_utils, hotlist, + jobs, learn_keys_dialog, layout_dialog, link, menubar, panel_options, + quickcd_dialog, screen_list, skin_dialog, tree, usermenu, vfs_list, + vfs_settings_dialog, Cmd, DialogState, FileManager, }; use crate::filemanager::link::LinkKind; use crate::key::Key; @@ -372,7 +373,20 @@ impl FileManager { Ok(true) } Cmd::OptionsConfirm => { - self.toggle_confirmations(); + let init = confirm_dialog::ConfirmSettings { + confirm_delete: self.runtime.safe_delete.unwrap_or(true), + confirm_overwrite: true, + confirm_execute: self.runtime.safe_execute.unwrap_or(false), + confirm_exit: true, + }; + self.dialog = Some(DialogState::Confirm(Box::new( + confirm_dialog::ConfirmDialog::new(init), + ))); + Ok(true) + } + Cmd::Appearance => { + let dlg = appearance_dialog::AppearanceDialog::new(&self.runtime); + self.dialog = Some(DialogState::Appearance(Box::new(dlg))); Ok(true) } Cmd::CompareDirs => { @@ -914,6 +928,31 @@ impl FileManager { } consumed = true; } + Some(DialogState::Appearance(d)) => { + use crate::filemanager::appearance_dialog::AppearanceOutcome; + let r = d.handle_key(key); + match r { + AppearanceOutcome::Confirm(settings) => { + self.runtime.show_menubar = Some(settings.show_menubar); + self.runtime.show_keybar = Some(settings.show_keybar); + self.runtime.show_hintbar = Some(settings.show_hintbar); + self.runtime.show_cmdline = Some(settings.show_cmdline); + self.runtime.show_mini_status = Some(settings.show_mini_status); + self.status.set_message("Appearance settings updated".to_string()); + self.dialog = None; + } + AppearanceOutcome::OpenSkin => { + self.dialog = Some(DialogState::Skin(Box::new( + skin_dialog::SkinDialog::new(&self.skin_name), + ))); + } + AppearanceOutcome::Cancel => { + self.dialog = None; + } + AppearanceOutcome::Running => {} + } + consumed = true; + } Some(DialogState::Sort(d)) => { use crate::filemanager::sort_dialog::SortResult; let r = d.handle_key(key); diff --git a/local/recipes/tui/tlc/source/src/filemanager/menubar.rs b/local/recipes/tui/tlc/source/src/filemanager/menubar.rs index cbe189c4ed..b3663d2dc3 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/menubar.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/menubar.rs @@ -146,6 +146,7 @@ fn build_menus() -> Vec { MenuItem::item("Layout...", Cmd::LayoutDialog), MenuItem::item("Panel options...", Cmd::PanelOptionsDialog), MenuItem::item("Confirmation...", Cmd::OptionsConfirm), + MenuItem::item("Appearance...", Cmd::Appearance), MenuItem::item("Skins...", Cmd::SkinSelect), MenuItem::item("Display bits...", Cmd::DisplayBits), MenuItem::item("Learn keys...", Cmd::LearnKeysDialog), diff --git a/local/recipes/tui/tlc/source/src/filemanager/mod.rs b/local/recipes/tui/tlc/source/src/filemanager/mod.rs index 7388049fa4..829fa54717 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/mod.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/mod.rs @@ -9,6 +9,7 @@ pub mod cmdline; pub mod compare; pub mod config_dialog; pub mod confirm_dialog; +pub mod appearance_dialog; pub mod connection_dialog; pub mod connection_manager; pub mod copy_dialog; @@ -356,6 +357,8 @@ pub enum DialogState { Connection(Box), /// Alt-Shift-T — sort order dialog. Sort(Box), + /// F9 → Options → Appearance — show/hide UI bars. + Appearance(Box), /// F9 → Options → Confirmation — per-operation confirm toggles. Confirm(Box), /// F5/F6/F8 — live progress dialog for background copy/move/delete. @@ -420,6 +423,7 @@ impl DialogState { DialogState::Encoding(_) => false, DialogState::Connection(_) => false, DialogState::Sort(_) => false, + DialogState::Appearance(_) => false, DialogState::Confirm(_) => false, DialogState::Progress(_) => false, } diff --git a/local/recipes/tui/tlc/source/src/filemanager/render.rs b/local/recipes/tui/tlc/source/src/filemanager/render.rs index 563df0f8d6..05710da6cf 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/render.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/render.rs @@ -181,6 +181,7 @@ impl FileManager { DialogState::Connection(d) => d.render(frame, dialog_area, &self.theme), DialogState::Sort(d) => d.render(frame, dialog_area, &self.theme), DialogState::Confirm(d) => d.render(frame, dialog_area, &self.theme), + DialogState::Appearance(d) => d.render(frame, dialog_area, &self.theme), DialogState::Progress(d) => d.render(frame, dialog_area, &self.theme), } } diff --git a/local/recipes/tui/tlc/source/src/keymap/mod.rs b/local/recipes/tui/tlc/source/src/keymap/mod.rs index db61edcde5..c832d32b86 100644 --- a/local/recipes/tui/tlc/source/src/keymap/mod.rs +++ b/local/recipes/tui/tlc/source/src/keymap/mod.rs @@ -219,6 +219,8 @@ pub enum Cmd { CommandHistory, /// Options menu — toggle confirmation dialogs. OptionsConfirm, + /// Options menu — appearance dialog (MC CK_OptionsAppearance). + Appearance, } impl Cmd { @@ -318,6 +320,7 @@ impl Cmd { Cmd::VfsSettings => "Virtual FS", Cmd::CommandHistory => "Command history", Cmd::OptionsConfirm => "Confirmation", + Cmd::Appearance => "Appearance", } } }