W79: MC visual/behavioral parity — viewer ruler, Enter/F3, editor margin, char info
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%.
This commit is contained in:
@@ -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 `<EOF>` 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -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<Span> = (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 {
|
||||
|
||||
Reference in New Issue
Block a user