tlc: Phase C.2/C.4/C.5/C.8 — Layout/Pattern/Confirm/Appearance dialogs MC parity

Phase C.2 — Layout dialog (layout_dialog.rs):
- New LayoutSettings fields: split_horizontal, output_lines
- New 'Split Horizontal' checkbox (Horizontal vs Vertical panel split)
- New 'Output lines' field (numeric toggle 100 <-> 200)
- from_runtime_config takes both RuntimeConfig + FilemanagerConfig
  to read the layout string

Phase C.4 — Pattern dialog (pattern_dialog.rs):
- New PatternOptions struct: files_only, shell_patterns, case_sensitive
- New FocusField enum: Pattern/FilesOnly/ShellPatterns/CaseSensitive
- 3 new checkboxes matching MC's 'Select group' dialog
- PatternOutcome::Confirm now carries PatternOptions
- Tab cycles between pattern input and checkboxes

Phase C.5 — Confirmation dialog (confirm_dialog.rs):
- New ConfirmSettings fields: confirm_hotlist_delete, confirm_history_cleanup
- 2 new toggles matching MC's 'Confirmation' dialog

Phase C.8 — Appearance dialog (appearance_dialog.rs):
- New AppearanceSettings fields: shadows, console_paste, double_click_speed_ms
- 3 new toggles matching MC's 'Appearance' dialog
- Row 7 displays 'Output lines: 250 ms' style numeric field

Tests: 1486 passing (was 1486). Tests updated for the expanded
state models.

Refs: MC-PARITY-AUDIT.md §5.10, §5.16, §5.13, §5.21 (GAP-LD-1..2,
GAP-PD-1..3, GAP-XD-1..2, GAP-AD-1..3)
This commit is contained in:
Sisyphus
2026-07-26 02:25:45 +09:00
parent e15590bc74
commit 66ec2b0c24
5 changed files with 275 additions and 117 deletions
@@ -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 F1F10 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<Constraint> = (0..ROWS).map(|_| Constraint::Length(1)).collect();
let constraints: Vec<Constraint> = (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));
}
@@ -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]
@@ -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),
@@ -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);
@@ -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);