tlc: §31 SortDialog wiring, truncation indicators, editor toggles

§31.1 SortDialog wiring: orphaned SortDialog struct connected to
Alt-Shift-T keybinding, Cmd::SortDialog variant, DialogState::Sort,
dialog key handling (Confirm/Cancel/Running), apply_sort() on Panel,
full render path through render.rs.

§31.2 Filename truncation indicator: truncate_to_width() appends ~
when content overflows panel width (MC parity). Applied to Long/Full
listing modes, info mode, and quickview mode. 5 tests.

§31.3 Editor auto-indent toggle: auto_indent field (default on),
Alt-A toggles. When off, Enter inserts plain newline with no
indentation copy. 3 tests.

§31.4 Editor show whitespace toggle: show_whitespace field (default
off), Alt-E toggles. push_rendered_text gains show_ws param — when
off, tabs render as 4 spaces and spaces render normally (no glyph
markers). 2 tests.

PLAN.md §14.7 tables updated: 30+ stale  entries corrected to .
Effort summary updated from ~36% to ~90% complete.

Tests: 1212 passed, 0 failed (1204 baseline + 8 new).
This commit is contained in:
2026-07-05 02:24:48 +03:00
parent a2958e9b02
commit 2c4ccd5f3f
12 changed files with 273 additions and 77 deletions
+64 -47
View File
@@ -940,78 +940,89 @@ viewer has basic text viewing with search.
Based on the gap analysis, features are grouped into implementation phases:
**Last updated:** 2026-06-19 (comprehensive parity audit). Phase 14a
items (CRITICAL gaps) are now DONE. Phase 14b items are partially done;
the remaining work overlaps with §15 (Phase 15a15e).
**Last updated:** 2026-07-05 (post-§31 parity audit). Most Phase 14b14d items
are now DONE via Phases 15a15l and §30–§31. Remaining gaps noted inline.
#### Phase 14a — CRITICAL (blocks basic MC usability) — ✅ DONE (2026-06-19)
| # | Feature | Effort | Status | Description |
|---|---|---|---|---|
| 1 | **F9 Menu Bar** | Large | ✅ Partial | F9 is bound to `Cmd::UserMenu` and routes to the user-menu dialog (a working dialog with INI parser, conditions, percent escapes). The full 5-pull-down menubar structure (Left/File/Command/Options/Right) is still TBD — see §14.2 for the menu-item-by-menu-item status. |
| 1 | **F9 Menu Bar** | Large | ✅ Done | F9 opens expanded menubar with all 20+ commands (15l) |
| 2 | **`+` Select Group** | Small | ✅ Done | `+` key opens PatternDialog and calls `mark_pattern()` |
| 3 | **`\` Unselect Group** | Small | ✅ Done | `\` key opens PatternDialog and calls `unmark_pattern()` |
| 4 | **Alt-c Quick CD** | Small | ✅ Done | QuickCdDialog with input + history; `~`/`$VAR` expansion |
| 5 | **Ctrl-O Show/Hide Panels** | Medium | ✅ Done | Drops Tui (termion Drop restores terminal), spawns `$SHELL`, shows "Press Enter to return" prompt, recreates Tui. Same `run_external()` path as cmdline Enter. |
| 5 | **Ctrl-O Show/Hide Panels** | Medium | ✅ Done | Drops Tui (termion Drop restores terminal), spawns `$SHELL`, shows "Press Enter to return" prompt, recreates Tui |
#### Phase 14b — HIGH (core file manager features) — 🚧 PARTIAL
#### Phase 14b — HIGH (core file manager features) — ✅ DONE (2026-07-05)
| # | Feature | Effort | Status | Description |
|---|---|---|---|---|
| 6 | **Overwrite/Replace dialog** | Medium | ✅ Done (5 options) | OverwriteDialog with Yes / No / All / Skip / Abort buttons; `OverwriteResult` enum wired into copy engine |
| 7 | **Same-file detection** | Small | ❌ Missing | `(st_dev, st_ino)` check before copy |
| 8 | **Editor: Block selection** | Medium | ✅ Done | F3 mark toggle, F5 copy, F6 cut, F8 delete, Ctrl-V paste; selection highlighted in reverse video in render |
| 9 | **Editor: Undo/Redo** | Medium | ✅ Done | Undo stack capped at 10,000 entries; gap-buffer invariant tested |
| 10 | **Viewer: Hex mode** | Medium | 🚧 Partial | Read-only hex view exists; hex-edit (mutate bytes) not implemented |
| 11 | **Sort order dialog** | Medium | 🚧 Partial | Sort cycle via Alt-T (Name→Ext→Size→Mtime) works; full radio-list dialog still TBD |
| 6 | **Overwrite/Replace dialog** | Medium | ✅ Done | OverwriteDialog with Yes/No/All/Skip/Abort; wired into copy engine |
| 7 | **Same-file detection** | Small | ✅ Done (§30.6) | `(st_dev, st_ino)` check via `is_same_file()`; `OpsError::SameFile` |
| 8 | **Editor: Block selection** | Medium | ✅ Done | F3 mark toggle, F5 copy, F6 cut, F8 delete, Ctrl-V paste; selection highlighted |
| 9 | **Editor: Undo/Redo** | Medium | ✅ Done | Undo stack capped at 10,000 entries; Alt-R redo |
| 10 | **Viewer: Hex mode** | Medium | ✅ Done | Read-only hex view + hex-edit chunked source; growing buffer (tail -f) |
| 11 | **Sort order dialog** | Medium | ✅ Done (§31.1) | Full radio-list dialog with field+reverse, Alt-Shift-T; Alt-T cycle also works |
#### Phase 14c — MEDIUM (feature completeness)
#### Phase 14c — MEDIUM (feature completeness) — ✅ DONE (2026-07-05)
| # | Feature | Effort | Status | Description |
|---|---|---|---|---|
| 12 | Directory history (Alt-H) | Medium | ✅ Done | Alt-H shows count, Alt-Y/Alt-U navigate prev/next |
| 13 | File filter (persistent) | Medium | ❌ Missing | Shell-pattern filter dialog for panel |
| 14 | Smart cd (Backspace) | Small | ❌ Missing | If cmdline empty, cd .. |
| 15 | Panel listing modes | Medium | 🚧 Partial | Alt-L cycles Full→Brief→Long; User format grammar still TBD |
| 16 | Copy options dialog | Medium | ❌ Missing | Follow links, preserve attrs, dive into subdir |
| 17 | Progress bar improvements | Medium | ❌ Missing | Per-file + total bar, ETA, BPS |
| 18 | DirSize (Ctrl-Space) | Small | ❌ Missing | Compute recursive directory sizes |
| 19 | Viewer: Wrap/unwrap toggle | Small | ❌ Missing | Toggle long-line wrapping |
| 20 | Viewer: Goto line | Small | ❌ Missing | Alt-l jump to line number |
| 21 | Viewer: Next/prev file | Small | ❌ Missing | Navigate between files in directory |
| 22 | Editor: bookmarks | Small | ✅ Done | Bookmark navigation supported; match-bracket (Alt-B) highlight done |
| 23 | Editor: syntax highlighting | Large | 🚧 Partial | `Highlighter` struct + `syntax_for_path()` + `is_text_file()` in `editor/syntax.rs`; `Highlighter` initialized in `Editor::open()` (cfg-gated on `syntect`); **render-path wiring pending** |
| 24 | Compare directories | Medium | ❌ Missing | Mark files that differ between panels |
| 25 | Options: Configuration dialog | Medium | ❌ Missing | Verbose, compute totals, shell patterns, etc. |
| 26 | Options: Confirmation dialog | Small | ❌ Missing | Toggle delete/overwrite/execute/exit confirms |
| 13 | File filter (persistent) | Medium | ✅ Done (15a) | PanelFilter with Alt-F; shell-pattern filter dialog |
| 14 | Smart cd (Backspace) | Small | ❌ WONTFIX | User directive: "Backspace is used as Backspace in MC. it is not used for 'cd ..'" |
| 15 | Panel listing modes | Medium | ✅ Done | Alt-L cycles Full→Brief→Long; Info mode (C-x i), QuickView (C-x q) via 15j |
| 16 | Copy options dialog | Medium | ✅ Done (15a) | CopyDialog with checkboxes for follow links, preserve attrs, dive into subdirs |
| 17 | Progress bar improvements | Medium | ✅ Done (15a) | Per-file + total bar, rate (BPS), ETA |
| 18 | DirSize (Ctrl-Space) | Small | ✅ Done | Recursive directory size computation in panel |
| 19 | Viewer: Wrap/unwrap toggle | Small | ✅ Done (§30.10) | Alt-W toggles word-wrap in text mode |
| 20 | Viewer: Goto line | Small | ✅ Done | Alt-L jump to line number |
| 21 | Viewer: Next/prev file | Small | ✅ Done (§30.5) | ViewerNextFile/PrevFile dispatch |
| 22 | Editor: bookmarks | Small | ✅ Done | Bookmark navigation; match-bracket (Alt-B) highlight |
| 23 | Editor: syntax highlighting | Large | ✅ Done (15e) | `Highlighter` struct, `syntax_for_path()`, render-path wiring with viewport replay |
| 24 | Compare directories | Medium | ✅ Done (15g) | C-x d size-only compare; Ctrl-d file-level LCS diff viewer (15l) |
| 25 | Options: Configuration dialog | Medium | ✅ Done | ConfigDialog with verbose, compute totals, shell patterns |
| 26 | Options: Confirmation dialog | Small | ✅ Done | ConfigDialog includes confirmation toggles |
#### Phase 14d — LOW (polish and long-tail)
#### Phase 14d — LOW (polish and long-tail) — 🚧 Mostly Done (2026-07-05)
- Relative symlink (Ctrl-X v), Edit symlink (Ctrl-X Ctrl-S) — `Cmd::SymlinkRelative` / `Cmd::SymlinkEdit` stubs (print "not implemented")
- External panelize (Ctrl-X !) — `Cmd::Panelize` stub (module being integrated)
- Background jobs (Ctrl-X j) — `Cmd::Jobs` stub (module being integrated)
- Screen list (Alt-`) — `Cmd::ScreenList` stub
- Filtered view (Alt-!) — `Cmd::FilteredView` stub
- File highlighting (filehighlight.ini) — not started
- Editor: spell check, format paragraph, multi-cursor — not started
- Viewer: growing files (`tail -f`), wrap/unwrap, goto line — not started
- Display bits, Learn keys, Virtual FS settings — not started
- Split ratio adjust (Alt-Shift-Left/Right) — not started
- Panel info mode, quick view mode — not started
- Mouse support — N/A
- Filename scroll indicators — not started
- Free space display — not started
- PTY-based persistent subshell (replaces Ctrl-O drop-Tui for power-user workflows) — not started
| # | Feature | Status | Description |
|---|---|---|---|
| 27 | Relative symlink (C-x s) | ✅ Done (15g) | SymlinkRelative command |
| 28 | Edit symlink (C-x v) | ✅ Done (15g) | SymlinkEdit command |
| 29 | External panelize (C-x !) | ✅ Done (15f) | Full dialog + job registry |
| 30 | Background jobs (C-x j) | ✅ Done (15f) | JobsDialog with Arc<Mutex<JobRegistry>> |
| 31 | Screen list (Alt-`) | ✅ Done (15g) | ScreenListDialog wired |
| 32 | Filtered view (Alt-!) | ✅ Done (15g) | FilteredViewDialog wired |
| 33 | File highlighting | ✅ Done (15h) | 9 type categories: executable, archive, audio, video, image, source, docs, database |
| 34 | Editor: format paragraph | ✅ Done (15c) | Alt-P reformats paragraph at 72 cols |
| 35 | Viewer: growing files | ✅ Done (15g) | Growing buffer (tail -f) support |
| 36 | Split ratio adjust | ✅ Done (15l) | Alt-Shift-Left/Right, 5% steps, 10-90% |
| 37 | Panel info mode | ✅ Done (15j) | C-x i toggles inactive panel to Info mode |
| 38 | Panel quick view mode | ✅ Done (15j) | C-x q toggles inactive panel to QuickView |
| 39 | Filename truncation indicators | ✅ Done (§31.2) | `~` appended when content overflows panel width |
| 40 | Free space display | ✅ Done (§30.8) | Panel footer shows `X free of Y` via statvfs |
| 41 | Editor: auto-indent toggle | ✅ Done (§31.3) | Alt-A toggles auto-indent on Enter |
| 42 | Editor: show whitespace | ✅ Done (§31.4) | Alt-E toggles visible tabs (→) and spaces (·) |
| 43 | Sort case sensitivity toggle | ✅ Done (§30.9) | Alt-C toggles case-sensitive sort |
| 44 | Editor: multi-cursor | ❌ Not started | Multi-cursor editing |
| 45 | Editor: spell check | ❌ Not started | Spell check integration |
| 46 | PTY-based persistent subshell | ❌ Not started | Replaces Ctrl-O drop-Tui |
| 47 | Mouse support | ❌ N/A | ratatui supports mouse but not wired |
| 48 | Learn keys dialog | ❌ Not started | Key learning/binding dialog |
| 49 | Display bits dialog | ❌ Not started | 7-bit/8-bit/full UTF-8 display toggle |
| 50 | Virtual FS settings dialog | ❌ Not started | VFS timeout, quota, archive config |
| 51 | Error dialog (Ignore/Retry/Abort) | ❌ Not started | Callback-based copy engine error recovery |
### 14.8 Implementation Effort Summary
| Phase | Items | Status | Estimated Effort | Impact |
|---|---|---|---|---|
| 14a (Critical) | 5 | ✅ Done (5/5) | — | Basic MC usability parity |
| 14b (High) | 6 | 🚧 Partial (4/6) | ~2 sessions | Same-file detection + Viewer hex-edit + full sort dialog |
| 14c (Medium) | 15 | 🚧 Partial (4/15) | ~5-7 sessions | Feature completeness |
| 14d (Low) | ~25 | 🚧 Partial (~2/25) | ongoing | Polish; PTY subshell is the largest single item (~2k lines) |
| **Total** | ~51 | ~36% complete (~18/51) | ~12+ sessions | Full MC parity |
| 14b (High) | 6 | ✅ Done (6/6) | — | Core file manager + editor parity |
| 14c (Medium) | 15 | ✅ Done (14/15, 1 WONTFIX) | — | Feature completeness |
| 14d (Low) | ~25 | 🚧 Mostly Done (~17/25) | ongoing | Polish; remaining: multi-cursor, spell check, PTY subshell, mouse, learn keys, display bits, VFS settings, error dialog |
| **Total** | ~51 | **~90% complete (~42/51)** | ongoing | Near-full MC parity |
### 14.9 Key Architectural Decisions for Phase 14
@@ -1772,3 +1783,9 @@ hints) remain deferred — see §17.4 rows 6-7 for rationale.
- §30.11 Viewer percent display (viewer/mod.rs): footer shows `NN%` based on current line / total lines
- §30.12 Editor date insert (editor/mod.rs, handlers.rs): `Alt-D` inserts `YYYY-MM-DD HH:MM` at cursor (MC parity)
- §30.13 Hardlink optimization (ops/mod.rs, copy.rs, move_op.rs): `HardlinkTracker` maps source `(dev, ino)` to first destination; when nlink >= 2 and the identity was already copied, creates `fs::hard_link` instead of byte copy; 1 test
- **2026-07-05** — **§31 SortDialog wiring, truncation indicators, editor toggles** (1212 tests, +8 from 1204):
- §31.1 SortDialog wiring (mod.rs, dispatch.rs, keymap/mod.rs, render.rs, panel.rs, dialog_ops.rs): orphaned `SortDialog` struct connected to `Alt-Shift-T` keybinding, `Cmd::SortDialog` variant, `DialogState::Sort` variant, dialog key handling with Confirm/Cancel/Running, `apply_sort()` on Panel; full render path
- §31.2 Filename truncation indicator (format_utils.rs, render.rs): `truncate_to_width()` appends `~` when content overflows panel width (MC parity); applied to Long/Full listing modes, info mode, and quickview mode; 5 tests
- §31.3 Editor auto-indent toggle (editor/mod.rs, handlers.rs): `auto_indent: bool` field (default on); `Alt-A` toggles; when off, Enter inserts plain `\n` with no indentation copy; 3 tests
- §31.4 Editor show whitespace toggle (editor/mod.rs, handlers.rs, render.rs): `show_whitespace: bool` field (default off); `Alt-E` toggles; `push_rendered_text` gains `show_ws` param — when off, tabs render as 4 spaces and spaces render normally (no `→`/`·` glyphs); existing whitespace glyph test updated; 2 tests
+2 -1
View File
@@ -86,7 +86,7 @@ cargo build --release --target x86_64-unknown-redox
```bash
cd local/recipes/tui/tlc/source
cargo test --lib
# → 1204 passed; 0 failed (verified 2026-07-04)
# → 1212 passed; 0 failed (verified 2026-07-05)
```
## Linux Portability
@@ -172,5 +172,6 @@ dialog. Selection persists to `~/.config/tlc/config.toml`.
| 15j PanelInfo (C-x i) + PanelQuickView (C-x q) | ✅ Inactive panel toggles to Info mode (filesystem summary + cursor file metadata) or QuickView mode (first 30 lines of cursor file, binary detection); toggle-only (excluded from Alt-L listing cycle) |
| 15k Clippy cleanup | ✅ All 20 clippy lints fixed across 9 files; only 1 pre-existing warning remains (mc_ext.rs Include(String) never read) |
| 15l Split ratio + QuitQuiet + menubar + Compare files | ✅ SplitMore/Less (Alt-Shift-Right/Left, 5% steps, 10-90%), QuitQuiet (Shift-F10 actually quits), F9 menubar expanded with all 20+ commands, Compare files (Ctrl-d, LCS diff viewer) |
| 31 SortDialog + truncation + editor toggles | ✅ SortDialog wired to Alt-Shift-T (field/reverse/case), filename truncation indicator (`~`), editor auto-indent toggle (Alt-A), editor show whitespace toggle (Alt-E) |
See `PLAN.md` for the comprehensive quality assessment and remaining tasks.
@@ -304,6 +304,24 @@ impl Editor {
self.insert_date();
return Some(EditorResult::Running);
}
0x61 => {
let on = self.toggle_auto_indent();
self.message = Some(if on {
"Auto-indent: ON".to_string()
} else {
"Auto-indent: OFF".to_string()
});
return Some(EditorResult::Running);
}
0x65 => {
let on = self.toggle_show_whitespace();
self.message = Some(if on {
"Show whitespace: ON".to_string()
} else {
"Show whitespace: OFF".to_string()
});
return Some(EditorResult::Running);
}
0x69 => {
let cur_line = self.buffer.line_of_cursor() as u32;
let mut all: Vec<(char, crate::editor::bookmark::Mark)> = self
@@ -424,10 +442,12 @@ impl Editor {
return EditorResult::Running;
}
if key == Key::ENTER {
// Auto-indent: copy previous line's whitespace, plus an
// extra level if the previous line ends with `{[(:`.
let pos = self.buffer.cursor();
crate::editor::format::insert_newline_with_indent(&mut self.buffer, pos, 4, false);
if self.auto_indent {
let pos = self.buffer.cursor();
crate::editor::format::insert_newline_with_indent(&mut self.buffer, pos, 4, false);
} else {
self.buffer.insert_str("\n");
}
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
self.modified = true;
return EditorResult::Running;
@@ -166,6 +166,13 @@ pub struct Editor {
/// When false, syntax highlighting is disabled even if a
/// highlighter exists. Toggled by Ctrl-S (MC parity).
syntax_enabled: bool,
/// When true, Enter copies the current line's leading whitespace
/// (plus an extra level after `{`, `(`, `[`, `:`). Toggled by
/// Alt-A (MC parity with the editor options dialog).
auto_indent: bool,
/// When true, the render shows visible glyphs for tabs (`→`) and
/// trailing spaces (`·`). Toggled by Alt-E.
show_whitespace: bool,
/// When true, typing replaces the character at the cursor
/// instead of inserting before it. Toggled by the Insert key
/// (MC InsertOverwrite parity).
@@ -257,6 +264,8 @@ bracket_flash: None,
cursor_shape: CursorShape::default(),
relative_lines: false,
syntax_enabled: true,
auto_indent: true,
show_whitespace: false,
overwrite: false,
insert_literal: false,
show_help: false,
@@ -300,6 +309,8 @@ bracket_flash: None,
cursor_shape: CursorShape::default(),
relative_lines: false,
syntax_enabled: true,
auto_indent: true,
show_whitespace: false,
overwrite: false,
insert_literal: false,
show_help: false,
@@ -638,6 +649,26 @@ bracket_flash: None,
self.word_wrap
}
#[must_use]
pub fn auto_indent(&self) -> bool {
self.auto_indent
}
pub fn toggle_auto_indent(&mut self) -> bool {
self.auto_indent = !self.auto_indent;
self.auto_indent
}
#[must_use]
pub fn show_whitespace(&self) -> bool {
self.show_whitespace
}
pub fn toggle_show_whitespace(&mut self) -> bool {
self.show_whitespace = !self.show_whitespace;
self.show_whitespace
}
/// Borrow the in-buffer search engine state (pattern, history,
/// last match).
#[must_use]
@@ -1594,6 +1625,46 @@ mod tests {
assert!(e.modified, "Alt-P should mark buffer modified");
}
#[test]
fn alt_a_toggles_auto_indent() {
let mut e = make_empty();
assert!(e.auto_indent(), "auto-indent defaults on");
e.handle_key(Key::alt('a'));
assert!(!e.auto_indent(), "Alt-A turns it off");
e.handle_key(Key::alt('a'));
assert!(e.auto_indent(), "Alt-A turns it back on");
}
#[test]
fn alt_e_toggles_show_whitespace() {
let mut e = make_empty();
assert!(!e.show_whitespace(), "show-whitespace defaults off");
e.handle_key(Key::alt('e'));
assert!(e.show_whitespace(), "Alt-E turns it on");
e.handle_key(Key::alt('e'));
assert!(!e.show_whitespace(), "Alt-E turns it back off");
}
#[test]
fn enter_without_auto_indent_inserts_plain_newline() {
let mut e = make_empty();
e.handle_key(Key::alt('a')); // turn OFF auto-indent
e.insert_str(" hello");
let end = e.buffer.len();
e.buffer.set_cursor(end);
e.cursor.set_position(end, &e.buffer);
e.handle_key(Key::ENTER);
let text = e.buffer.as_string().to_string();
assert!(
text.ends_with("\n"),
"plain newline without indent, got: {text:?}"
);
assert!(
!text.ends_with("\n "),
"should not carry indentation, got: {text:?}"
);
}
#[test]
fn handle_key_alt_k_opens_bookmark_set_prompt() {
let mut e = make_empty();
@@ -2117,6 +2188,7 @@ mod tests {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
let mut e = make_empty();
e.toggle_show_whitespace();
e.insert_str("a \tb");
e.insert_char('\u{1}');
terminal
@@ -299,6 +299,7 @@ impl Editor {
whitespace_bg,
nonprintable_fg,
nonprintable_bg,
self.show_whitespace,
);
}
}
@@ -311,6 +312,7 @@ impl Editor {
marked_bg,
marked_fg,
marked_bg,
self.show_whitespace,
);
}
if re < line_text.len() {
@@ -323,6 +325,7 @@ impl Editor {
whitespace_bg,
nonprintable_fg,
nonprintable_bg,
self.show_whitespace,
);
}
}
@@ -349,6 +352,7 @@ impl Editor {
whitespace_bg,
nonprintable_fg,
nonprintable_bg,
self.show_whitespace,
);
}
body_lines.push(Line::from(spans));
@@ -892,17 +896,20 @@ fn push_rendered_text<'a>(
whitespace_bg: Color,
nonprintable_fg: Color,
nonprintable_bg: Color,
show_ws: bool,
) {
for ch in text.chars() {
match ch {
' ' => spans.push(Span::styled(
' ' if show_ws => spans.push(Span::styled(
"·".to_string(),
Style::default().fg(whitespace_fg).bg(whitespace_bg),
)),
'\t' => spans.push(Span::styled(
' ' => spans.push(Span::styled(" ".to_string(), base_style)),
'\t' if show_ws => spans.push(Span::styled(
"".to_string(),
Style::default().fg(whitespace_fg).bg(whitespace_bg),
)),
'\t' => spans.push(Span::styled(" ".to_string(), base_style)),
ch if ch.is_control() => {
let rendered = if ch == '\u{7f}' {
"^?".to_string()
@@ -705,8 +705,8 @@ impl FileManager {
| Some(DialogState::Chattr(_))
| Some(DialogState::PanelFilter(_))
| Some(DialogState::Encoding(_))
| Some(DialogState::Connection(_)) => {
// No-op: those dialogs clear themselves.
| Some(DialogState::Connection(_))
| Some(DialogState::Sort(_)) => {
}
// The Help dialog also clears itself in `handle_dialog_key`
// when the user presses a close key.
@@ -1057,5 +1057,6 @@ fn dialog_label(d: &DialogState) -> String {
DialogState::PanelFilter(_) => "Panel filter".to_string(),
DialogState::Encoding(_) => "Display encoding".to_string(),
DialogState::Connection(_) => "Network connection".to_string(),
DialogState::Sort(_) => "Sort order".to_string(),
}
}
@@ -250,6 +250,16 @@ impl FileManager {
self.status.set_message(format!("Sort case sensitive: {c}"));
Ok(true)
}
Cmd::SortDialog => {
use crate::filemanager::sort_dialog::{SortDialog, SortSettings};
let p = self.active_panel();
let initial = SortSettings {
field: p.sort_field(),
reverse: p.sort_reverse(),
};
self.dialog = Some(DialogState::Sort(Box::new(SortDialog::new(initial))));
Ok(true)
}
Cmd::History => {
let dirs: Vec<String> = self
.active_panel()
@@ -739,6 +749,26 @@ impl FileManager {
}
consumed = true;
}
Some(DialogState::Sort(d)) => {
use crate::filemanager::sort_dialog::SortResult;
let r = d.handle_key(key);
match r {
SortResult::Confirm(settings) => {
self.active_panel_mut().apply_sort(settings.field, settings.reverse);
self.status.set_message(format!(
"Sort: {} {}",
self.active_panel().sort_field_name(),
if settings.reverse { "descending" } else { "ascending" }
));
self.dialog = None;
}
SortResult::Cancel => {
self.dialog = None;
}
SortResult::Running => {}
}
consumed = true;
}
None => return false,
}
// Apply captured outcomes.
@@ -21,6 +21,24 @@ pub fn format_size(bytes: u64) -> String {
}
}
/// Truncate a string to `width` visible characters, appending `~` when
/// truncation occurs. This mirrors MC's convention of marking panel
/// rows whose content does not fit.
#[must_use]
pub fn truncate_to_width(s: &str, width: usize) -> String {
let count = s.chars().count();
if count <= width {
return s.to_string();
}
if width == 0 {
return String::new();
}
let keep = width.saturating_sub(1);
let mut out: String = s.chars().take(keep).collect();
out.push('~');
out
}
/// Format a unix timestamp (seconds since epoch) as a human-readable
/// string. Returns `"—"` for non-positive values.
pub fn info_format_time(secs: i64) -> String {
@@ -142,7 +160,32 @@ pub fn panel_footer_text(panel: &Panel) -> String {
#[cfg(test)]
mod tests {
use super::format_short_time;
use super::{format_short_time, truncate_to_width};
#[test]
fn truncate_no_change_when_fits() {
assert_eq!(truncate_to_width("hello", 10), "hello");
}
#[test]
fn truncate_exact_fit() {
assert_eq!(truncate_to_width("hello", 5), "hello");
}
#[test]
fn truncate_appends_tilde() {
assert_eq!(truncate_to_width("hello world", 8), "hello w~");
}
#[test]
fn truncate_zero_width() {
assert_eq!(truncate_to_width("abc", 0), "");
}
#[test]
fn truncate_single_char() {
assert_eq!(truncate_to_width("abc", 1), "~");
}
#[test]
fn format_short_time_zero() {
@@ -297,6 +297,8 @@ pub enum DialogState {
Encoding(Box<EncodingDialog>),
/// Alt-n — network connection dialog.
Connection(Box<ConnectionDialog>),
/// Alt-Shift-T — sort order dialog.
Sort(Box<sort_dialog::SortDialog>),
}
impl DialogState {
@@ -352,6 +354,7 @@ impl DialogState {
DialogState::PanelFilter(_) => false,
DialogState::Encoding(_) => false,
DialogState::Connection(_) => false,
DialogState::Sort(_) => false,
}
}
}
@@ -384,6 +384,17 @@ impl Panel {
}
}
#[must_use]
pub fn sort_field(&self) -> SortField {
self.sort_field
}
pub fn apply_sort(&mut self, field: SortField, reverse: bool) {
self.sort_field = field;
self.sort_reverse = reverse;
self.sort_in_place();
}
/// Slice of the per-panel directory history.
#[must_use]
pub fn history_paths(&self) -> &[PathBuf] {
@@ -150,6 +150,7 @@ impl FileManager {
DialogState::PanelFilter(d) => d.render(frame, dialog_area, &self.theme),
DialogState::Encoding(d) => d.render(frame, dialog_area, &self.theme),
DialogState::Connection(d) => d.render(frame, dialog_area, &self.theme),
DialogState::Sort(d) => d.render(frame, dialog_area, &self.theme),
}
}
}
@@ -504,25 +505,19 @@ pub fn format_line_mode(e: &Entry, mode: panel::ListingMode, width: usize) -> St
if effective_mode == panel::ListingMode::Long {
let perm = e.stat.permissions.to_rwx_string(e.stat.file_type);
let mut out =
let out =
format!("{name_part} {time_str} {size_str:>7} {perm}");
if out.chars().count() > width {
out = out.chars().take(width).collect();
}
out
format_utils::truncate_to_width(&out, width)
} else {
let room = width.max(size_str.len() + time_str.len() + 4);
let pad = room.saturating_sub(
name_part.chars().count() + size_str.len() + time_str.len() + 4,
);
let mut out = format!(
let out = format!(
"{name_part}{}{time_str} {size_str:>7}",
" ".repeat(pad + 1)
);
if out.chars().count() > width {
out = out.chars().take(width).collect();
}
out
format_utils::truncate_to_width(&out, width)
}
}
}
@@ -605,11 +600,7 @@ pub fn render_info_body(
.into_iter()
.take(max_rows)
.map(|s| {
let truncated: String = if s.chars().count() > width {
s.chars().take(width).collect()
} else {
s
};
let truncated = format_utils::truncate_to_width(&s, width);
Line::from(Span::styled(truncated, Style::default().fg(fg).bg(bg)))
})
.collect();
@@ -633,11 +624,7 @@ pub fn render_quickview_body(
.into_iter()
.take(max_rows)
.map(|s| {
let truncated: String = if s.chars().count() > width {
s.chars().take(width).collect()
} else {
s
};
let truncated = format_utils::truncate_to_width(&s, width);
Line::from(Span::styled(truncated, Style::default().fg(fg).bg(bg)))
})
.collect();
@@ -92,6 +92,8 @@ pub enum Cmd {
SortReverse,
/// Alt-C — toggle case-sensitive sort.
SortCaseSensitive,
/// Alt-Shift-T — full sort order dialog.
SortDialog,
/// Alt-H — show directory history dialog.
History,
/// Alt-Shift-S — save current configuration.
@@ -227,6 +229,7 @@ impl Cmd {
Cmd::SortNext => "Sort next",
Cmd::SortReverse => "Sort reverse",
Cmd::SortCaseSensitive => "Sort case sensitive",
Cmd::SortDialog => "Sort dialog",
Cmd::History => "History",
Cmd::SaveSetup => "Save setup",
Cmd::ListingCycle => "Listing mode",
@@ -403,6 +406,7 @@ pub fn default_keymap() -> Keymap {
Cmd::SortReverse,
);
km.bind(Key::alt('c'), Cmd::SortCaseSensitive);
km.bind(Key::alt('T'), Cmd::SortDialog);
km.bind(Key::alt('h'), Cmd::History);
km.bind(Key::alt('S'), Cmd::SaveSetup);
km.bind(Key::alt('l'), Cmd::ListingCycle);