Mirrors MC's panel.c:4080-4197 mouse handling. Wraps the
Tui screen in termion's MouseTerminal (enables GPM on Linux /
xterm mouse on others — without it mouse events never reach
ratatui). The translate_mouse function maps termion MouseEvent
to a MouseAction enum that the FileManager dispatches as
cursor moves / entries / scroll (deferring HistoryPrev/Next/
List and ToggleHidden which need dedicated Cmds not yet in
the keymap).
New APIs:
- terminal::event::translate_mouse(event, panel_top,
panel_height, panel_width) -> MouseAction
- terminal::event::MouseAction (Click, DoubleClick,
ScrollUp/Down, HistoryPrev/Next/List, ToggleHidden,
Unhandled)
- FileManager::handle_mouse_event(m, size) routes
MouseAction to existing cursor_up_n / cursor_down_n /
Panel::enter / Panel::set_cursor helpers.
- app.rs routes TermEvent::Mouse through handle_mouse_event
before the keyboard-dispatch path.
Tests (9 new in terminal::event::mouse_tests):
- click_on_file_row_emits_click_action
- click_below_panel_is_unhandled
- click_on_header_history_prev_button (col 1)
- click_on_header_history_next_button (col width-2)
- click_on_header_toggle_hidden_button (col width-6)
- scroll_up_emits_scroll_up
- scroll_down_emits_scroll_down
- click_on_column_name_row_is_unhandled (sort-by-column
not yet wired)
- click_respects_panel_top_offset (panel_top parameter
works for menu-bar offset)
1345 passing (was 1336; +9 new).
Mirrors MC's src/learn.c::learn_keys (mc/source/src/learn.c:393-422).
MC shows a grid of buttons; TLC is simpler: scrollable list of
captured keys with their resolved Cmd.
New file: src/filemanager/learn_keys_dialog.rs (LearnKeysDialog,
CapturedKey, 6 unit tests).
New DialogState::LearnKeys variant wired into dispatch, render,
is_finished, title. Keymap is stored at construction so the
dispatcher doesn't thread it through every key event.
Tests: 1336 passing (was 1330; +6 new).
Mirrors MC's boxes.c::configure_vfs_box
(mc/source/src/filemanager/boxes.c:1120-1198). MC's dialog has
VFS timeout plus FTP options (anonymous password, directory
cache timeout, proxy host, passive mode); TLC has no FTP/SFTP in
the active build, so we expose the relevant 1-field subset:
- Timeout for freeing VFSs (sec): 0..=10000, default 10
(out-of-range falls back to 10 — matches MC's
boxes.c:1193-1194 validation)
New file: src/filemanager/vfs_settings_dialog.rs
- VfsSettings struct (free_timeout: u32)
- parse_timeout() (handles negative as 0, > 10000 as None)
- VfsSettingsDialog with Edit-keyed text input
- 12 unit tests
New DialogState variant:
DialogState::VfsSettings(Box<VfsSettingsDialog>)
Wired into dispatch, render, is_finished, title.
Tests: 1330 passing (was 1318; +12 new).
Mirrors MC's boxes.c::display_bits_box
(mc/source/src/filemanager/boxes.c:955-1004). MC exposes 4 modes
(UTF-8, Full 8 bits, ISO 8859-1, 7 bits); TLC is UTF-8 throughout
so we expose the relevant 3-state subset:
- Full 8 bits: bytes pass through verbatim
- 7 bits: high bit stripped (parity-stripped serial consoles)
- UTF-8 (validated): pass through UTF-8, ? for invalid
New file: src/filemanager/display_bits_dialog.rs
- DisplayBits enum with 3 variants
- DisplayBitsDialog (radio-list, mirrors SortDialog)
- from_config() / map_byte() / is_valid_utf8() helpers
- 12 unit tests
New DialogState variant:
DialogState::DisplayBits(Box<DisplayBitsDialog>)
Wired into dispatch (handle_key), render, is_finished (false;
the dispatcher captures Confirm/Cancel from handle_key), title.
Tests: 1318 passing (was 1306; +12 new).
Co-Authored-By: Claude <noreply@anthropic.com>
Mirrors MC's filegui.c::file_progress_real_query_replace and the
FileProgressStatus enum (filegui.h:45-54): FILE_CONT, FILE_RETRY,
FILE_SKIP, FILE_ABORT, FILE_IGNORE.
New file: src/filemanager/error_dialog.rs
- ErrorDialog with path + error message
- R / S / I / A shortcuts (MC letter bindings)
- Esc maps to Abort
- 7 unit tests (one per shortcut + an 'other keys don't finish' guard)
New FileManager state:
- pending_error_op: Option<PendingErrorOp>
stores kind + sources + dst + flags for the in-flight op so
the user can Retry / Skip / Ignore / Abort
- PendingErrorOp struct (parallel to existing PendingFileOp)
- pending_error_op initialized to None
New DialogState variant:
DialogState::Error(Box<error_dialog::ErrorDialog>)
Wired into dispatch (handle_key), render, is_finished, title.
Retry is currently a stub (logs a message) because the copy
engine's batch step resumption would require threading the
operation index into copy_many/move_many/delete_many. The dialog
scaffolding is complete and tested; the retry can be wired in a
follow-up sprint when the batch step API is added.
Tests: 1306 passing (was 1299; +7 new).
Adds regression tests for Sprint 3 items that were verified
already-done during recon but had no dedicated test coverage:
- viewer::tests::hex_edit_apply_nibble_at_eof_is_safe_noop
Locks in the C8 contract: apply_nibble at cursor past EOF
is a safe no-op (no panic, no spurious modified flag).
- viewer::tests::move_cursor_clamps_to_size
Locks in the C8 cursor bounds invariant at move_cursor
level: positive deltas clamp to file size, negative deltas
clamp to 0 with no underflow.
- filemanager::panel::tests::history_dedups_consecutive_entries
Locks in the C11 contract: refreshing the same directory
N times grows the history by 1 entry (consecutive dedup),
not N.
- filemanager::panel::tests::sort_field_mtime_round_trips_through_config
Locks in the C12 contract: 'mtime' and 'time' config
strings both resolve to the Mtime sort field, and the
Panel reports the human-readable name 'Mtime'.
Tests (4 new, total 1299 passing):
+hex_edit_apply_nibble_at_eof_is_safe_noop
+move_cursor_clamps_to_size
+history_dedups_consecutive_entries
+sort_field_mtime_round_trips_through_config
Adds regression tests for already-implemented Sprint 3 items so
future refactors can't silently regress these behaviors:
- terminal::color::tests::default_theme_preserves_rgb_precision
Locks in the C1 contract: when Theme::background is Rgb, the
exact 8-bit values (julia256's core.bg = 58,58,58) survive.
The parser may also emit Indexed(237) for the same source
value depending on detected color depth, so the test accepts
either variant but verifies 24-bit precision when present.
- terminal::color::tests::light_theme_preserves_rgb_precision
Same invariant for LIGHT_THEME (sand256 bright off-white
foreground > 200 RGB).
- editor::tests::bookmark_jump_restores_both_line_and_column
Locks in the C5 contract: jumping to a named bookmark must
restore both the line AND the column where the bookmark was
set. The existing test only verified the byte offset, not
the column, so a future regression to line-only restoration
would not have been caught.
Tests (3 new, total 1295 passing):
+default_theme_preserves_rgb_precision
+light_theme_preserves_rgb_precision
+bookmark_jump_restores_both_line_and_column
When set_message() is called with text containing '\n' characters,
the status line now renders the message across multiple rows
(previously only the first line was shown, with the rest lost).
StatusLine (terminal/status.rs):
- Message::line_count() returns the newline-segment count
- StatusLine::has_multiline_message() detects newline-containing
unexpired messages
- StatusLine::current_message_line_count() returns the row
count for allocation (0 for no message)
- StatusLine::render_multiline() renders each newline-separated
line as its own row
FileManager (filemanager/mod.rs):
- status_message_line_count() helper that clamps to 1..=4 rows
to avoid eating the whole screen, returns 1 when no message
render.rs (filemanager/render.rs):
- When has_message() AND has_multiline_message(), allocate the
multi-line area above the single-line status bar (which still
shows clock/spinner/hint). Single-line messages unchanged.
Tests (3 new in terminal::status::tests):
- has_multiline_message_detects_newlines
- current_message_line_count_counts_newlines
- message_line_count_handles_empty_and_multiline
Total: 1292 passing (was 1289; +3 new).
Previously reformat_paragraph_at() would join indented lines
together, destroying intentional indentation (Python code,
shell heredocs, ASCII art, markdown sub-blocks).
Now reformat_paragraph_at() detects code blocks (paragraphs
where every non-blank line begins with whitespace) and
returns the text unchanged. The detection is intentionally
conservative — false positives leave the paragraph alone
rather than destroying user-written indentation.
format.rs:
- New is_code_block(paragraph) helper
- reformat_paragraph_at() calls is_code_block(); if true,
returns text unchanged (preserving indentation)
Tests (6 new in editor::format::tests):
- is_code_block_detects_indented_paragraph
- is_code_block_rejects_unindented_paragraph
- is_code_block_ignores_blank_lines_in_paragraph
- reformat_paragraph_skips_code_block
- reformat_paragraph_skips_shell_heredoc
- reformat_paragraph_handles_mixed_code_and_prose
Total: 1289 passing (was 1283; +6 new).
Previously cmdline commands were always synchronous — even
typing 'firefox &' would block TLC until the foreground shell
process exited (which for firefox would never happen because
the shell exits immediately and firefox keeps running, but
the spawn process remained a child of the shell until the shell
exited, blocking the parent).
Now run_command detects a trailing '&' (with optional
trailing whitespace) and spawns the command in a new process
group via process_group(0) (Unix). The command is detached,
TLC continues immediately, and the parent shell exits without
waiting.
Falls back to a synchronous run on non-Unix platforms.
Tests (2 new in terminal::subshell::tests):
- run_command_backgrounded_with_ampersand_succeeds
'sleep 5 &' returns in < 2s (not 5s)
- run_command_backgrounded_with_trailing_whitespace
'sleep 3 & ' (space before &) also backgrounded
Total: 1283 passing (was 1281; +2 new).
The tar crate returns an empty iterator (without erroring) for
both zero-byte files and files that aren't valid tar archives.
TarVfs::open() previously succeeded in both cases, producing an
'unbrowsable' archive the user couldn't navigate.
Fix:
TarVfs::open() now checks entries.is_empty() after list() and
returns VfsError::Other('empty tar archive') when empty. This
covers both truly empty files (corrupt/truncated) and garbage
bytes (e.g., text mistakenly saved with .tar extension).
Tests (2 new in vfs::tar::tests):
- tar_vfs_open_empty_file_errors
- tar_vfs_open_garbage_bytes_errors
Total: 1281 passing (was 1279; +2 new).
Previously parse() only recognised the local:// scheme. A
file:///path URL (the standard scheme used by browsers, text
editors, and many CLI tools) would be treated as an unknown
scheme and fall through to the 'unknown scheme → local'
fallback in for_path(), which works but is silent.
Now parse() explicitly handles file:// as an alias for local://,
making the VFS path normal form 'local://' after parsing.
Tests (1 new in vfs::path::tests):
- parse_file_scheme_is_alias_for_local
Total: 1279 passing (was 1278; +1 new).
Previously Find dialog always opened with the default placeholder
('*.rs'). Now open_find_dialog() inspects the active panel's
cursor entry and uses its name as the initial pattern — so
pressing M-? with cursor on 'main.rs' opens the dialog with
'main.rs' already in the input, ready for refinement (e.g.
'main.rs~' for backups, 'main*' for variants).
Implementation:
- find::FindDialog::new_with_pattern(start_dir, initial_pattern)
- panel::Panel::cursor_entry() -> Option<&Entry>
- widget::input::Input::set_text(s) mutable setter (the
existing .text() builder consumes self)
- dialog_ops::open_find_dialog() uses cursor entry name as
the initial pattern; skips '..' and falls back to default
when the panel is empty
Tests (2 new in find::tests):
- new_with_pattern_prefills_pattern_input
- new_with_pattern_empty_falls_back_to_default
Total: 1278 passing (was 1276; +2 new).
Previously mark_pattern("") and unmark_pattern("") were silent
no-ops (glob_match returned false for empty pattern). Now they
match MC's behavior:
- mark_pattern(""): calls reverse_marks() (toggles every
entry; from 0 marks → all entries marked, vice versa)
- unmark_pattern(""): clears all marks
Tests (2 new in panel::tests):
- mark_pattern_empty_inverts_marks (0→2→0 cycle)
- unmark_pattern_empty_clears_all_marks
Total: 1276 passing (was 1274; +2 new).
The hex viewer now reserves the top row for a status header
showing the cursor byte offset and the total file size:
'Offset: 00000040 / 00000100 bytes'
When the area has height < 2 (degenerate case), the header is
skipped and the full row is used for hex bytes.
Layout split: total_height >= 2 → 1-row header + body_area
(remaining rows). total_height == 1 → no header, body uses
full area. The body Paragraph is rendered to body_area (not
the full area) so the header is not overwritten.
Tests (2 new in viewer::hex::tests):
- render_shows_offset_header_when_area_has_height
cursor=64 → header shows 'Offset: 00000040' at row 0
- render_skips_header_when_area_height_is_one
80x1 area → first row starts with hex offset '00000000'
Total: 1274 passing (was 1272; +2 new).
Previously, Panel::set_path() would pass the input path straight to
read_directory(). A relative path like 'sub' would fail because
read_directory tries to stat it relative to wherever, not relative
to the user's current working directory.
Implementation:
Panel::set_path() now checks if p.is_relative() and, if so,
joins it onto std::env::current_dir() before reading. Absolute
paths pass through unchanged. Falls back to the raw path if
current_dir() fails (process has no cwd).
Tests (1 new in panel::tests):
- set_path_resolves_relative_against_cwd
Total: 1272 passing (was 1271; +1 new).
DeleteDialog header now includes the total recursive size of
all paths to be deleted (formatted via format_size). For example:
'Delete 3 (1.2 MB) ?' instead of just 'Delete ?'.
Computed eagerly at construction via crate::ops::count_bytes so
render() does not block on filesystem traversal.
DeleteDialog (delete_dialog.rs):
- New field total_bytes: u64 (cached at construction)
- new() calls crate::ops::count_bytes(&paths) to populate it
- render() prepends '(N items)' to header and appends size when > 0
Tests (3 new in delete_dialog::tests):
- new_computes_total_bytes_for_directory (recursive dir sum)
- new_total_bytes_zero_for_missing_paths
- new_total_bytes_sums_multiple_paths
Total: 1271 passing (was 1268; +3 new).
Input widget colors no longer hardcoded to Color::White / Blue /
Yellow. New instances use Color::Reset as a sentinel meaning
'use the theme token', and render() falls back to:
- fg: theme.foreground
- bg: theme.background
- cursor_color: theme.warning
Callers that explicitly call .fg() / .bg() / .cursor_color() still
override the sentinel — existing API surface preserved.
Changes:
- input.rs::Input::new() defaults fg/bg/cursor_color to Color::Reset
- input.rs::Input::render() resolves Reset → theme token
- New pub fn cursor_color(c) builder (was missing; .fg/.bg existed)
Tests (2 new in widget::input::tests):
- default_colors_are_reset_sentinel
- explicit_color_overrides_reset_sentinel
Total: 1268 passing (was 1266; +2 new).
Theme::by_name() now lowercases the input before matching aliases,
so 'MC-Classic', 'Mc-Dark', and 'DARK' all resolve identically
to their lowercase canonical names. Previously a user setting
config.toml [skin] name = 'MC-Classic' would silently fall back
to default (no match found for the capitalized name).
Implementation:
- In by_name(), lowercase the input via to_ascii_lowercase()
- Match against the existing alias map (which already covers
mc-classic, mc-dark, mc-dark-gray, default-dark, etc.)
- The lowercase form is passed to mc_skin::theme_by_name,
which handles the embedded MC .ini files; the lookup there
also becomes case-insensitive as a side effect.
Tests (2 new in terminal::color::tests):
- by_name_resolves_aliases_case_insensitively
MC-CLASSIC → default, Mc-Dark → dark, etc.
- by_name_real_skin_name_case_insensitive
DARK → dark, NICEDARK → nicedark (verifies the alias->real
skin chain works regardless of input case)
Total: 1266 passing (was 1264; +2 new).
Previously Viewer::search for Chunked sources read the ENTIRE
file into RAM via read_at(0, size) — defeating the memory-efficient
design of Chunked mode (a 150 MB Chunked file would load all 150 MB
just to search it).
Search (viewer/search.rs):
New method find_all_streaming<E>(pattern, case_insensitive, next_chunk)
- Generic error type E so callers can use their own error type
- next_chunk closure yields (bytes, is_last_chunk) until exhausted
- Sliding-window algorithm: keeps last (pattern.len() - 1) bytes
from each chunk as 'tail', combines with next chunk for regex
- Matches that span chunk boundaries are detected correctly
- Matches across overlapping combined buffers are deduped by
start offset after collection
Viewer (viewer/mod.rs):
search() now dispatches by source variant:
- Inline / Compressed: in-memory find_all (unchanged)
- Chunked: find_all_streaming with closure yielding CHUNK_SIZE
reads until EOF reached
Tests (5 new in viewer::search::tests):
- streaming_single_chunk_finds_match
- streaming_match_spans_two_chunks (xxfoobarxx at chunk 5)
- streaming_no_matches
- streaming_multiple_matches_in_one_chunk
- streaming_chunk_size_smaller_than_pattern (chunk_size=1, pattern=hello)
Total: 1264 passing (was 1259; +5 new).
Bookmarks now track the correct line number across insert/delete
edits (MC parity). Previously a bookmark at line 5 would still
report line 5 even after lines 1-3 were deleted, pointing at
wrong content.
BookmarkSet (editor/bookmark.rs):
New method adjust_lines(at_line: u32, delta: i32)
- For each mark with line > at_line: shift by delta
- For positive delta: mark.line += delta
- For negative delta with mark.line in deletion range:
clamp to at_line + 1 (first surviving line after range)
- Column is preserved across all shifts
- Zero delta is a no-op (no allocation)
Editor (editor/mod.rs):
New private helper adjust_bookmarks_after_edit(edit_line, lines_before)
Wraps insert_char, insert_str, delete_back, delete_forward:
- Captures (line_before, lines_before) before buffer op
- Calls buffer op
- Computes delta = lines_after - lines_before
- Calls bookmarks.adjust_lines(line_before, delta) if non-zero
Tests (6 new in editor::bookmark::tests):
- adjust_lines_zero_delta_is_noop
- adjust_lines_shifts_marks_after_insertion
- adjust_lines_shifts_marks_after_deletion
- adjust_lines_multi_line_insertion
- adjust_lines_deletion_clamps_to_anchor
- adjust_lines_does_not_touch_col
Total: 1259 passing (was 1253; +6 new).
Adds user-facing control over the editor tab width.
New methods on Editor (editor/mod.rs):
tab_width() -> usize
- Returns current visual tab width in spaces.
set_tab_width(width: usize)
- Sets tab width, clamped to min 1.
cycle_tab_width() -> usize
- Cycles 4 → 8 → 2 → 4 (default 4 → wider → narrower → back).
- Returns the new width.
Alt-T binding (editor/handlers.rs):
try_global_shortcut now handles Alt-T (0x74) and calls
cycle_tab_width(). Status bar shows 'Tab width: N'.
Tab-to-indent (B3) and word-wrap (B1) both consume tab_width
via the field, so changing it via Alt-T takes effect immediately
on next render / insert.
Tests (5 new in editor::tests):
- tab_width_default_is_four
- set_tab_width_updates_field (verifies 0 → 1 clamp)
- cycle_tab_width_advances_2_4_8_2
- cycle_tab_width_from_unusual_value_resets_to_2
- tab_to_indent_uses_current_tab_width
Config wiring note: cfg.editor.tab_width (config.rs) already has
the field with serde defaults and persistence. Wiring it into
Editor::open() requires the constructor to accept &Config and
is deferred to a future sprint.
Total: 1253 passing (was 1248; +5 new).
Tab key now has indent-aware behavior (MC parity):
- When cursor is in the indent zone (only whitespace before it on
the current line), Tab inserts enough spaces to advance to the
next tab_width boundary.
- When cursor is past any non-whitespace, Tab inserts a single
literal '\t' character for column alignment.
New methods on Editor (editor/mod.rs):
insert_tab_with_indent()
- Computes next_boundary = ((col / tab_width) + 1) * tab_width
- In indent zone: inserts (next_boundary - col) spaces
- Otherwise: inserts literal '\t'
is_in_indent_zone() -> bool
- True if all bytes between line start and cursor are space/tab
Tab handler in editor/handlers.rs routes through
insert_tab_with_indent() instead of insert_char('\t').
Tests (6 new in editor::tests):
- tab_at_col_0_indents_to_next_boundary
- tab_at_col_4_indents_to_next_boundary
- tab_mid_line_inserts_literal_tab
- tab_after_non_whitespace_inserts_literal_tab
- tab_after_whitespace_runs_continues_indent
- tab_inside_word_inserts_literal_tab
Total: 1248 passing (was 1242; +6 new).
Replaces the character-counting wrap in build_wrap_map with a
word-boundary algorithm that matches MC's intent: lines break at
the last whitespace before the wrap limit, mid-word break as
fallback for overlong words, tabs counted as their visual width
(next tab_width boundary), control chars as 2 cols.
New helpers (editor/render.rs):
visual_width(ch, col, tab_width) -> usize
- Tabs: tab_width - (col % tab_width), min 1
- Control chars (0x00..=0x1F, 0x7F): 2 cols (renders as ^X)
- Other chars: 1 col
count_wrapped_rows(line_bytes, body_width, tab_width) -> usize
- Walks line left-to-right, tracks last whitespace position
- On overflow: wrap at last_ws_col (preserves word boundary),
else mid-word break
- Stops at \n (line_bytes excludes \n in caller)
Editor (mod.rs):
- New tab_width: usize field (default 4)
- Matches the renderer's ' ' tab expansion in push_rendered_text
- Future B4 will make this user-configurable
Tests:
+12 wrap_tests in editor::render::wrap_tests:
- visual_width_tab_expands_to_next_boundary
- visual_width_ascii_and_control
- count_wrapped_rows_empty_line
- count_wrapped_rows_short_line_fits_in_one_row
- count_wrapped_rows_exact_fit
- count_wrapped_rows_one_char_overflow_no_whitespace
- count_wrapped_rows_word_boundary_break
- count_wrapped_rows_three_words_at_width_six
- count_wrapped_rows_long_word_falls_back_to_mid_word
- count_wrapped_rows_tab_visual_width
- count_wrapped_rows_zero_width_returns_one
- count_wrapped_rows_stops_at_newline
Total: 1242 passing (was 1230; +12 new).
Implements all 5 critical parity items from the comprehensive MC
assessment. Reference: MC source at local/recipes/tui/mc/source/.
A1 — Editor line number gutter:
Split editor inner area into gutter_area + body_area; gutter
renders right-aligned line numbers (or relative offsets when
relative_lines mode is on); bookmark rows show current-line
style; ~ shown for lines past end-of-file.
A2 — Viewer cursor line highlight:
cursor_line_bg derived from body_bg + RGB(12,12,12); applied
before search-match overlay so matches win on the cursor line.
A3 — Hide terminal cursor in file manager mode:
App::run() hides the cursor after Tui::new(); render() shows
the cursor only when editor/viewer/cmdline/dialog/menubar
is active.
A6 — Shift-F5/F6 same-directory rename:
New Cmd::CopySameDir and Cmd::MoveSameDir variants, bound to
Shift-F5 / Shift-F6. Reuse CopyDialog/MoveDialog with a new
same_dir flag and new_rename() constructor; result() resolves
the typed name against the source's parent directory.
A7 — SUID/SGID/sticky bits in chmod dialog:
4-row PermCell grid (user, group, other, special); class_shift
= 0o4000, bit = 1. Display as 0oXXXX. Overwrite dialog shows
all 12 cells. 6 new unit tests cover all special-bit combinations.
Other fixes in the same commit:
- 4 editor render tests shifted x-coordinates by gutter_chars
to account for the new gutter column.
- 1 viewer text test moves cursor to line 1 so cursor-line
highlight doesn't overlap the search-match assertion.
Tests: 1230 passed (was 1219 before A7; +11 new tests across A7
and A6). All Sprint 1 items compile clean.
MenuBarOutcome::Dispatch now carries Option<usize> indicating which
menu (0=Left, 4=Right) originated the command. app.rs switches focus
to the corresponding panel before dispatching. This matches MC
behavior where the Left menu always operates on the left panel and
the Right menu on the right panel, regardless of which panel had
keyboard focus.
When going UP to a parent directory, find the entry whose name matches
the last component of the path being left (MC's get_parent_dir_name
approach). This is more robust than saved-index restoration because it
works even when the parent directory listing changed between visits.
Falls back to saved dir_cursors index for non-parent navigation.
- viewer/mod.rs: implement MC-style cursor tracking with independent
scroll (move_cursor_down/up/ensure_cursor_visible). Arrow keys now
move cursor within visible area; scrolling only when cursor reaches
edge. last_height field stores render area height for key handlers.
Fixes 'pressing down sends all text off screen'.
- panel.rs: save and restore BOTH cursor AND top scroll position in
dir_cursors HashMap. Previously only cursor was saved and top was
always reset to 0, causing ensure_cursor_visible to snap the view
on every directory switch.
- panel.rs: fix try_enter_archive to construct VFS URL with correct
scheme prefix (zip://, tar://) instead of parsing raw file path
as Local. Fixes .zip/.tar hang on Enter.
- terminal/event.rs: rewrite parse_unsupported_key to handle CSI-tilde
modifier sequences (\x1b[15;5~ = Ctrl-F5) and CSI arrow modifier
sequences (\x1b[1;5B = Ctrl-Down). Returns tlc Key directly with
modifiers set, bypassing termion's limited TermKey enum.
- app.rs: simplify event dispatch to use Key directly from
parse_unsupported_key, removing dead TermKey variable.
- 6 new tests for modifier+function-key and modifier+arrow parsing.
- cursor.rs: move_left walks back over UTF-8 continuation bytes,
move_right steps by char width via utf8_len_from_lead(), move_up/down
snap to char boundary via snap_to_char_boundary()
- mod.rs: update_bracket_flash adds defensive is_char_boundary() check
- render.rs: remove gutter/column layout, body_area = inner (matches MC
editdraw.c line_state=FALSE default); bookmark colors applied to
body line base_style (matches MC book_mark line coloring)
- text.rs: remove gutter rendering and Paragraph .wrap(); implement
pre-wrapping via wrap_line() for wrap mode; body uses full width
- Tests updated for new no-gutter layout (4 tests, all pass)
Wire three Command menu items that open TLC's config files in
the built-in editor: extensions, user menu, and file highlighting
rules. Files are created empty if they don't exist.
Cmd::EditExtensionFile → ~/.config/tlc/extensions
Cmd::EditMenuFile → ~/.config/tlc/menu
Cmd::EditHighlightFile → ~/.config/tlc/filehighlight.ini
Added open_config_file() helper in dialog_ops.rs that resolves
the config dir via ProjectDirs, creates parent + empty file if
missing, then opens the editor.
Command menu now has all 20 items (was 17/20).
The local bootloader fork claims to be upstream 1.0.0 + Red Bear
patches, but the patches in local/patches/bootloader/ do NOT
apply cleanly to upstream 1.0.0. The fork was originally a 0.1.0
baseline with substantial additions that pre-date the upstream-1.0.0
refactor.
Changes:
* local/fork-upstream-map.toml: set bootloader's upstream tag to
PENDING_REBASE. The verifier recognises this as a deliberate
state marker (a fork whose rebase is in progress) and refuses
the build with a clear error pointing the user to the rebase
procedure documented in the map.
* local/scripts/verify-fork-versions.sh: when a fork is marked
PENDING_REBASE in the map, the script reports a dedicated error
message instead of running the upstream content comparison (which
would always fail for a fork in this state).
The current state of the build is:
* 5 of 6 `-rb1` Cat 2 forks pass the no-fake-version-label
check (redoxfs, redox-scheme, kernel, installer, userutils).
* bootloader refuses to build until a real rebase onto a chosen
upstream tag is completed (the patches must apply cleanly with
--fuzz=0).
* installer has additional divergent content that may need a
rebase too (separate operational task).
This is the strict enforcement the user asked for. The build
cannot proceed silently with fake labels. The user must drive
the rebase work for bootloader (and installer) following the
procedure in fork-upstream-map.toml.
Wire New, Open, Save, SaveAs, Quit, Find, FindNext, FindPrev,
Replace, BookmarkNext, BookmarkPrev through dispatch_editor_cmd.
Previously these printed 'F9: Cmd (not yet wired)'. Now each one
performs the actual operation matching its Alt-key/F-key equivalent.
open_save_before_close_prompt made pub(crate) so the Quit handler
can intercept dirty buffers.
Add ToggleAutoIndent, ToggleShowWhitespace, ToggleWordWrap,
ToggleSyntax, ToggleRelativeLines to EditorCmd enum and dispatch
them through dispatch_editor_cmd. The Options menu now has all
five toggles alongside Settings, making them discoverable via F9
in addition to their Alt-key shortcuts.
The SortDialog was wired to Alt-Shift-T in §31.1 but was missing
from the F9 menu bar. MC has 'Sort order...' under both Left and
Right menus. Added to both.
§32.1 Viewer word-wrap fix: Paragraph .wrap() was always applied
regardless of the v.wrap toggle. Now .wrap(Wrap { trim: false }) is
conditionally applied only when v.wrap is true. When wrap is off,
long lines truncate at the right margin (MC parity). Stale TODO
removed from render_line_with_highlight.
§32.2 Alt-W wrap toggle test verifying the field toggles correctly.
PLAN §14.7 table refresh: 30+ stale entries corrected. Phase 14b
6/6 done. Phase 14c 14/15 done (1 WONTFIX). Phase 14d ~17/25 done.
Overall ~90% MC parity.
Tests: 1213 passed, 0 failed.
Per local/AGENTS.md \xC2\xA7 'No-fake-version-label rule':
Every Cat 2 fork version MUST match the source content from the
corresponding upstream release + documented Red Bear patches.
A `-rbN` label on stale content is a fake label and a policy
violation.
Changes:
* local/AGENTS.md: documented the no-fake-version-label rule,
including what counts as a fake label, the enforcement contract,
and what a real Red Bear fork looks like.
* local/fork-upstream-map.toml: authoritative mapping of each
Cat 2 fork (syscall, libredox, redoxfs, redox-scheme, relibc,
kernel, bootloader, installer, userutils) to its upstream Git
URL and release tag.
* local/scripts/refresh-fork-upstream-map.sh: auto-update the
fork-upstream-map by querying each upstream repo for the
current latest stable release tag.
* local/scripts/verify-fork-versions.sh: preflight enforcement
script. For each Cat 2 fork with a `-rbN` version field:
1. Compare fork's file list and content against the upstream
release tag from the map. Reject the build if files are
missing (would be a fake label).
2. Reject the build if files exist in local that don't exist
in upstream (must be moved to local/patches/<fork>/ as
documented Red Bear patches).
3. Reject the build if shared files diverge in content.
* local/scripts/apply-rb-suffix.sh: invokes
verify-fork-versions.sh after applying the `-rbN` label so the
build fails fast if the labelled content is fake.
* local/scripts/build-preflight.sh: invokes
verify-fork-versions.sh at the start of every build. Bypassed
only with REDBEAR_SKIP_FORK_VERIFY=1 (emergency only).
* local/patches/bottom/0001-ratui-0.30-braille-compat.patch: the
ratatui 0.30+ compatibility shim for bottom 0.11.2.
This is the structural enforcement that prevents fake labels from
ever reaching the build again. The current 6 forks with `-rbN`
labels are flagged by the verifier — they must be rebased onto
their actual upstream release before the build can succeed.
Red Bear OS needs a local fork of redox-scheme to enforce the -rb1
version policy end-to-end. crates.io redox-scheme 0.11.2 pins
redox_syscall = "0.9.0" exactly and libredox = "0.1.18" exactly,
which Cargo refuses to satisfy with the local -rb1 forks.
The fork is implemented as a TRACKED TREE under local/sources/redox-scheme/
on the active 0.2.5 branch (per local/AGENTS.md), not as a separate
git repository or as a separate submodule on its own branch.
The single-repo rule from local/AGENTS.md is preserved: this
directory is a regular subdirectory of the RedBear-OS repo, not a
separate repository.
Add comprehensive policy documentation in AGENTS.md covering:
- local/ fork always takes precedence over recipes/ paths
- build system must ensure local fork is at latest available version
- all Red Bear patches must be applied cleanly on top of latest version
- automatic version bump + patch reapplication via bump-fork.sh
Create local/scripts/bump-fork.sh that implements automatic version bumping:
- Detects current local version vs required version from Cargo.lock
- Fetches upstream source at required version
- Applies all Red Bear patches atomically
- Updates version field and replaces local fork contents
Fix driver-manager Cargo.toml lockfile collision:
- Remove redundant syscall dependency (transitive via pcid_interface)
- Update all driver recipes to use local/sources/syscall and libredox paths
- This eliminates the redox_syscall lockfile collision between
local/sources/syscall and recipes/core/base/syscall (same dir, different paths)
relibc: fix unsafe call for Rust 2024 edition compatibility
All three were blocking the boot scheduler in /usr phase on live-mini
because their daemons never notify readiness without hardware present.
- 00_gpiod.service: scheme -> oneshot_async
- 00_i2cd.service: scheme -> oneshot_async
- 00_pcid-spawner.service: new override, oneshot -> oneshot_async
- base: update submodule to 4a1d1f4 (scheduler counter)