W51: Appearance dialog (MC CK_OptionsAppearance)
The F9 audit found TLC had no Appearance dialog — the Skins... dialog was the only appearance option. MC's CK_OptionsAppearance offers five checkbox toggles plus a skin selector. Create AppearanceDialog with 6 rows: - Menubar (show/hide F9 menu bar) - Keybar (show/hide F1-F10 key hint bar) - Hint bar (show/hide status bar) - Command prompt (show/hide command-line prompt) - Mini status (show/hide mini-status above keybar) - Skins... (opens the existing SkinDialog) All five toggles write back to RuntimeConfig's show_* fields. Enter toggles the checkbox; Esc dismisses; Skins... opens the skin selection dialog transparently. Added to F9 → Options menu as 'Appearance...' between Confirmation and Skins, matching MC's menu ordering. Added 4 unit tests: toggles default true, enter toggles off/on, down cycles cursor, skin button opens skin outcome. Tests: 1411 pass (was 1407), zero warnings.
This commit is contained in:
@@ -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<Constraint> = (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));
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -146,6 +146,7 @@ fn build_menus() -> Vec<Menu> {
|
||||
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),
|
||||
|
||||
@@ -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<ConnectionDialog>),
|
||||
/// Alt-Shift-T — sort order dialog.
|
||||
Sort(Box<sort_dialog::SortDialog>),
|
||||
/// F9 → Options → Appearance — show/hide UI bars.
|
||||
Appearance(Box<appearance_dialog::AppearanceDialog>),
|
||||
/// F9 → Options → Confirmation — per-operation confirm toggles.
|
||||
Confirm(Box<confirm_dialog::ConfirmDialog>),
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user