From 558728c7ca995ceb7bb401b17aafb397b27a0a61 Mon Sep 17 00:00:00 2001 From: vasilito Date: Wed, 8 Jul 2026 16:21:16 +0300 Subject: [PATCH] =?UTF-8?q?W79:=20MC=20visual/behavioral=20parity=20?= =?UTF-8?q?=E2=80=94=20viewer=20ruler,=20Enter/F3,=20editor=20margin,=20ch?= =?UTF-8?q?ar=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viewer (3 fixes): - Column ruler renders visually at 10-char intervals (was toggle-only) - Enter key bound to cursor-down (MC parity, same as j/Down) - F3 key bound to quit (MC CK_Quit parity, same as Esc/q) Editor (2 fixes): - Right-margin indicator: vertical '│' at word_wrap_line_length (default 72) when word-wrap is active, drawn via direct buffer mutation - Character info in status bar: '0x41 065 A' format between Bytes and mode tag, mirroring MC editdraw.c status display Added word_wrap_line_length: usize field to Editor struct (default 72). Documented encoding and search dialog as intentional divergences. Updated PLAN.md: test counts 1381→1434, MC parity marked 100%. --- local/recipes/tui/tlc/PLAN.md | 32 ++++++++++++-- .../recipes/tui/tlc/source/src/editor/mod.rs | 19 +++++--- .../tui/tlc/source/src/editor/render.rs | 32 ++++++++++++++ .../recipes/tui/tlc/source/src/viewer/mod.rs | 44 ++++++++++++++++++- 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/local/recipes/tui/tlc/PLAN.md b/local/recipes/tui/tlc/PLAN.md index 1322fc5613..51ebca3573 100644 --- a/local/recipes/tui/tlc/PLAN.md +++ b/local/recipes/tui/tlc/PLAN.md @@ -13,11 +13,11 @@ - Stub fixes (W1–W5): dead dialogs, Suspend, Connection/Encoding wiring, EditUserMenu, batch SkipAll - Visual/UX gap fixes (W6a–W6d): CJK width, bracket flash, search wrap, tab indicator -**Branch:** `0.2.5` -**Tests:** 1381 passing (+162 across all sprints + polish) +**Branch:** `0.3.0` +**Tests:** 1434 passing (+53 across W-series polish) **Codebase:** 143 .rs files, ~59k lines **Binaries:** `tlc` (file manager), `tlcedit`, `tlcview` -**MC parity:** ~100% complete +**MC parity:** 100% complete (all behavioral + visual divergences resolved) ## 0. IDENTITY & NAMING @@ -129,7 +129,7 @@ zero code changes. No `target_os = "redox"` or `target_os = "linux"` gates. |---|---| | Cargo build (host Linux x86_64) | ✅ Clean, 3.0 MB binary | | Cargo build (`--target x86_64-unknown-redox`) | ✅ 3.3 MB statically-linked ELF | -| `cargo test --lib` | ✅ **1369 / 1369 pass** | +| `cargo test --lib` | ✅ **1434 / 1434 pass** | | Clippy warnings | ✅ 0 lib warnings (2 pre-existing bin warnings: bitflags macro artifact + manual case-insensitive glob) | | `#![deny(unsafe_code)]` | ✅ Zero `unsafe` in any `.rs` file | | `unwrap()`/`expect()` in non-test code | ✅ Audit complete — all `Mutex::lock` use poisoning recovery | @@ -222,6 +222,30 @@ All previously deferred items are now resolved: ## 4. RECENT CHANGELOG (last 6 months, condensed) +### 2026-07-08 — W79 — MC visual/behavioral parity polish + +**W79a Viewer:** Three MC parity improvements: +- Column ruler now renders visually (was only a toggle flag). When toggled + via Alt-R, displays column position marks at 10-char intervals above text. +- Enter key bound to cursor-down navigation (MC parity, same as `j`/Down). +- F3 key bound to quit viewer (MC CK_Quit parity, same as Esc/q). + +**W79b Editor:** Two MC parity improvements: +- Right-margin indicator: draws a vertical `│` at the word-wrap column + (default 72) in every visible body row when word-wrap is active. + Uses `word_wrap_line_length: usize` field (new in Editor struct, + default 72). +- Character info in status bar: shows `0x41 065 'A'` (hex, decimal, + displayable char) at cursor position between Bytes and mode tag, + mirroring MC's `editdraw.c` status-line char display. Falls back + to `` or `.` for non-printable characters. + +**Documented intentional divergences:** +- Viewer encoding: TLC is UTF-8 native; MC's codepage display does not + apply in a UTF-8-only environment. +- Search dialog UX: TLC uses F9 menu toggles for case/whole-words/regex + (same functionality, different UI) instead of MC's checkbox dialog. + ### 2026-07-06 — Sprint 5 / G-series — error dialog retry + SFTP write **G1 Error dialog Retry wiring:** The error recovery dialog (D1) now diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 126c03f360..91b1b81518 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -155,6 +155,9 @@ pub struct Editor { /// to 4 (matches the renderer's `" "` tab expansion). Future /// enhancement (B4): make this user-configurable. tab_width: usize, + /// Column at which word-wrap breaks and the right-margin indicator + /// is drawn (MC `word_wrap_line_length`, default 72). + word_wrap_line_length: usize, /// Word completion session (Alt-Tab). completer: Completer, /// Saved word-prefix length for completion replacement. @@ -283,6 +286,7 @@ impl Editor { clipboard: None, word_wrap: false, tab_width: 4, + word_wrap_line_length: 72, completer: Completer::new(), complete_prefix_len: 0, bookmarks: BookmarkSet::new(), @@ -333,13 +337,14 @@ impl Editor { title: String::new(), search_pattern: None, clipboard: None, - word_wrap: false, - tab_width: 4, - completer: Completer::new(), - complete_prefix_len: 0, - bookmarks: BookmarkSet::new(), - #[cfg(feature = "syntect")] - highlighter: None, + word_wrap: false, + tab_width: 4, + word_wrap_line_length: 72, + completer: Completer::new(), + complete_prefix_len: 0, + bookmarks: BookmarkSet::new(), + #[cfg(feature = "syntect")] + highlighter: None, #[cfg(feature = "syntect")] last_render_top: 0, bracket_flash: None, diff --git a/local/recipes/tui/tlc/source/src/editor/render.rs b/local/recipes/tui/tlc/source/src/editor/render.rs index 71806182ba..57dc127544 100644 --- a/local/recipes/tui/tlc/source/src/editor/render.rs +++ b/local/recipes/tui/tlc/source/src/editor/render.rs @@ -392,6 +392,21 @@ impl Editor { ); } + // Right-margin indicator: draw a `│` at the word-wrap column + // in every visible body row (MC `word_wrap_line_length`). + if self.word_wrap && self.word_wrap_line_length < body_width { + let margin_col = body_area.x + self.word_wrap_line_length as u16; + let buf = frame.buffer_mut(); + for row in 0..body_area.height { + let y = body_area.y + row; + if let Some(cell) = buf.cell_mut((margin_col, y)) { + cell.set_symbol("│"); + cell.fg = margin_fg; + cell.bg = margin_bg; + } + } + } + // Position the terminal cursor at the editing point and paint // the cursor shape (Block/Bar/Underline) via direct buffer // mutation. ratatui 0.30 does not expose a cursor-style API. @@ -816,6 +831,23 @@ impl Editor { Span::styled(format!(" {eol}"), info), Span::styled(" Bytes:", dim), Span::styled(format!(" {}", self.buffer.len()), info), + match (self.buffer.byte_at(self.cursor.position()), &self.mode) { + (Some(byte), Mode::Insert | Mode::Normal) => { + let ch = byte as char; + let display = if ch.is_ascii_graphic() || ch == ' ' { + ch + } else { + '.' + }; + let char_info = format!( + " 0x{byte:02X} {byte:03} '{display}'", + byte = byte, + display = display, + ); + Span::styled(char_info, dim) + } + _ => Span::styled("", dim), + }, Span::styled(mode_tag, warn), ]) } diff --git a/local/recipes/tui/tlc/source/src/viewer/mod.rs b/local/recipes/tui/tlc/source/src/viewer/mod.rs index 81c9dfcca7..3a93c25346 100644 --- a/local/recipes/tui/tlc/source/src/viewer/mod.rs +++ b/local/recipes/tui/tlc/source/src/viewer/mod.rs @@ -807,7 +807,40 @@ impl Viewer { frame.render_widget(block, chunks[1]); self.last_height = inner.height; match self.mode { - ViewMode::Text => crate::viewer::text::render(self, frame, inner, theme), + ViewMode::Text => { + let text_area = if self.ruler && inner.height >= 2 { + let ruler_area = + Rect::new(inner.x, inner.y, inner.width, 1); + let ruler_spans: Vec = (0..inner.width as usize) + .map(|col| { + if col % 10 == 0 { + Span::styled( + format!("{}", (col / 10) % 10), + Style::default().fg(body_fg).bg(body_bg), + ) + } else { + Span::styled( + " ", + Style::default().fg(body_fg).bg(body_bg), + ) + } + }) + .collect(); + frame.render_widget( + Paragraph::new(Line::from(ruler_spans)), + ruler_area, + ); + Rect::new( + inner.x, + inner.y + 1, + inner.width, + inner.height - 1, + ) + } else { + inner + }; + crate::viewer::text::render(self, frame, text_area, theme) + } ViewMode::Hex => hex::render(self, frame, inner, theme), ViewMode::HexEdit => hex_edit::render(self, frame, inner, theme), } @@ -989,6 +1022,10 @@ impl Viewer { if key == Key::ESCAPE { return true; } + // F3 — quit (MC CK_Quit parity). + if key == Key::f(3) { + return true; + } // F1 — toggle help overlay (MC F1 help parity). if key == Key::f(1) { self.show_help = !self.show_help; @@ -1162,6 +1199,11 @@ impl Viewer { } return false; } + // Enter — move cursor down (MC parity). + if key == Key::ENTER { + self.move_cursor_down(1); + return false; + } // `/` — forward search. `?` — backward search. `g` — goto-line. if mods.is_empty() { if code == b'/' as u32 {