recipes: bump redox_syscall 0.7 → 0.8 in all Red Bear recipes

Aligns all Red Bear custom recipe dependencies with the syscall 0.8.x
version used by the upstream-synced base and relibc forks.

Author: vasilito <adminpupkin@gmail.com>
This commit is contained in:
2026-06-18 21:35:51 +03:00
parent 89e6f29b22
commit eaf8e89785
29 changed files with 937 additions and 24 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ license = "MIT"
[workspace.dependencies]
rsext4 = "0.3"
redox_syscall = "0.7.3"
redox_syscall = "0.8"
redox-scheme = "0.11.0"
libredox = "0.1.13"
redox-path = "0.3.0"
+1 -1
View File
@@ -16,7 +16,7 @@ license = "MIT"
[workspace.dependencies]
fatfs = "0.3.6"
fscommon = "0.1.1"
redox_syscall = "0.7.3"
redox_syscall = "0.8"
redox-scheme = "0.11.0"
libredox = "0.1.13"
redox-path = "0.3.0"
@@ -14,7 +14,7 @@ libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-driver-sys = { path = "../../redox-driver-sys/source" }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
[target.'cfg(target_os = "redox")'.dependencies]
redox-driver-sys = { path = "../../redox-driver-sys/source", features = ["redox"] }
@@ -7,7 +7,7 @@ license = "MIT"
[dependencies]
libredox = "0.1"
redox_syscall = { version = "0.7", features = ["std"] }
redox_syscall = { version = "0.8", features = ["std"] }
log = "0.4"
thiserror = "2"
lazy_static = "1.4"
@@ -10,5 +10,5 @@ path = "src/main.rs"
[dependencies]
usb-core = { path = "../../usb-core/source" }
redox_syscall = "0.7"
redox_syscall = "0.8"
log = "0.4"
@@ -12,4 +12,4 @@ libc = "0.2"
libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
@@ -6,4 +6,4 @@ description = "PCI bus backend for redox-driver-core"
[dependencies]
redox-driver-core = { path = "../../redox-driver-core/source" }
redox_syscall = "0.7"
redox_syscall = "0.8"
@@ -10,5 +10,5 @@ path = "src/main.rs"
[dependencies]
usb-core = { path = "../../usb-core/source" }
redox_syscall = "0.7"
redox_syscall = "0.8"
log = "0.4"
@@ -8,7 +8,7 @@ description = "DRM scheme daemon for Redox OS — provides GPU modesetting and b
redox-driver-sys = { version = "0.1", path = "../../../drivers/redox-driver-sys/source" }
linux-kpi = { version = "0.1", path = "../../../drivers/linux-kpi/source" }
libredox = "0.1"
redox_syscall = { version = "0.7", features = ["std"] }
redox_syscall = { version = "0.8", features = ["std"] }
syscall04 = { package = "redox_syscall", version = "0.4" }
redox-scheme = "0.11"
daemon = { path = "../../../../../recipes/core/base/source/daemon" }
@@ -8,5 +8,5 @@ name = "cpufreqd"
path = "src/main.rs"
[dependencies]
redox_syscall = "0.7"
redox_syscall = "0.8"
log = "0.4"
@@ -13,7 +13,7 @@ redox-driver-core = { path = "../../../drivers/redox-driver-core/source" }
redox-driver-pci = { path = "../../../drivers/redox-driver-pci/source" }
pcid_interface = { path = "../../../../../recipes/core/base/source/drivers/pcid", package = "pcid" }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7" }
syscall = { package = "redox_syscall", version = "0.8" }
log = "0.4"
toml = "0.8"
serde = { version = "1", features = ["derive"] }
@@ -8,7 +8,7 @@ name = "driver-params"
path = "src/main.rs"
[dependencies]
redox_syscall = "0.7"
redox_syscall = "0.8"
redox-scheme = "0.11"
libredox = "0.1"
log = "0.4"
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
libc = "0.2"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
redox_scheme = { package = "redox-scheme", version = "0.11" }
libredox = "0.1"
log = { version = "0.4", features = ["std"] }
@@ -11,4 +11,4 @@ path = "src/main.rs"
libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
@@ -9,4 +9,4 @@ path = "src/main.rs"
[dependencies]
log = "0.4"
redox_syscall = "0.7"
redox_syscall = "0.8"
@@ -12,4 +12,4 @@ libc = "0.2"
libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
@@ -146,4 +146,4 @@ serde_json = "1"
orbclient = "0.3"
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" }
libredox = "0.1"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
@@ -13,4 +13,4 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
libredox = "0.1"
redox-syscall = { package = "redox_syscall", version = "0.7" }
redox-syscall = { package = "redox_syscall", version = "0.8" }
@@ -17,4 +17,4 @@ anyhow = "1"
[target.'cfg(target_os = "redox")'.dependencies]
libc = "0.2"
libredox = "0.1"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
@@ -16,7 +16,7 @@ libc = "0.2"
libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" }
zbus = { version = "5", default-features = false, features = ["tokio"], optional = true }
@@ -12,4 +12,4 @@ libc = "0.2"
libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
@@ -7,7 +7,7 @@ edition = "2021"
libc = "0.2"
libredox = "0.1"
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
syscall = { package = "redox_syscall", version = "0.8", features = ["std"] }
log = { version = "0.4", features = ["std"] }
thiserror = "2"
redbear-hwutils = { path = "../../redbear-hwutils/source" }
+655
View File
@@ -0,0 +1,655 @@
# Twilight Commander (`tlc`) — Comprehensive Improvement Plan
**Author:** Sisyphus (orchestrator) · **Date:** 2026-06-18
**Subject:** Full MC parity assessment + ratatui-era modernization roadmap
**Scope:** Visuals · Functions · Keyboard shortcuts · Dialogs · Themes · Architecture
**Reference baseline:** Midnight Commander 4.8.33 (C source at `local/recipes/tui/mc/source/`)
---
## Platform Priority (foundational)
**Red Bear OS is the PRIMARY platform for tlc. Linux is the SECONDARY
platform.**
TLC is the Red Bear OS file manager. Every decision in this plan is made
Redox-first:
1. **A change must build and run on `x86_64-unknown-redox` before it is
considered done.** The Linux host build is a development convenience and a
portability proof — it is not the shipping target. The `/usr/bin/tlc`
that lands in the `redbear-mini` and `redbear-full` ISOs is the product.
2. **No Linux-only dependency may become a hard requirement.** If a feature
needs a crate that does not cross-compile to Redox (or that needs a C
toolchain Redox lacks), it goes behind a Cargo feature that is **off by
default** and stays off in the Redox build, OR it is not adopted.
3. **The `cfg(target_os = "redox")` / `cfg(unix)` split is the supported
portability seam.** The existing `Cargo.toml`
`[target.'cfg(target_os = "redox")']` block (`libredox`, `redox_event`,
`redox_syscall`, `redox_termios`) is the Redox path; everything else
builds on `std` + `cfg(unix)` for Linux/macOS dev.
4. **Acceptance for any visual/IO change includes a Redox cross-compile +
QEMU smoke** (`make all CONFIG_NAME=redbear-mini`, then run `tlc` in the
guest). A change that only works on the Linux host is incomplete.
This priority order directly shapes Phase R (the backend migration must be
Redox-safe — see A.1 caveat) and the subshell work (Phase 4 must use Redox's
pty/scheme surface, not just Linux `openpty`).
---
## 0. Executive Summary
TLC is a **pure-Rust, `#![deny(unsafe_code)]`** reimplementation of Midnight
Commander built on **ratatui 0.30 + termion 4**. As of 2026-06-18 it is
**84+ `.rs` files, ~28k LoC, 698 passing tests**, and ships 30 filemanager
dialogs, a 497-line F9 menubar, 16 editor modules, 6 viewer modules, 8
built-in skins, and 6 i18n locales. It cross-compiles to a 3.3 MB
statically-linked Redox ELF.
**Assessment: ~65% of MC's *visible* surface is implemented; ~35% remains.**
The headline gaps are not "does feature X exist" — most do. They are:
1. **A real subshell** (PTY-based persistent shell; today it drops/recreates
the Tui). This is MC's signature power feature.
2. **A real syntax-highlighting engine** (MC ships 102 `.syntax` files; tlc
has a `syntax.rs` module but no bundled rules).
3. **Mouse support***blocked today* because tlc uses the **termion**
backend, which does not surface mouse/resize/focus events the way
crossterm does.
4. **A handful of power-user dialogs** (background jobs, external panelize,
full Options → Configuration, confirmation toggles).
5. **Visual polish that ratatui makes trivial but tlc hasn't adopted**
(Scrollbar widget, `Table` row-highlight, `Stylize` trait, `Clear` popup
consistency, proper 256/16-color fallback per cell).
This document supersedes the gap lists in `PLAN.md` §14–§15 by reframing the
work around **five externally-visible axes the user cares about**, adding the
**ratatui modernization track** the internal PLAN omits, and sequencing the
work so each phase ships a demonstrably better tlc.
---
## 1. Comprehensive Current-State Assessment
### 1.1 What exists and works (verified by direct source read, 2026-06-18)
| Layer | Modules | Evidence |
|---|---|---|
| **Keymap** | 37 `Cmd` variants, all F1F11 bound | `src/keymap/mod.rs` (377 lines) |
| **F9 menubar** | 5 pull-down menus (Left/File/Command/Options/Right) | `src/filemanager/menubar.rs` (497 lines) |
| **Filemanager dialogs (30)** | copy, move, mkdir, delete, find, info, permission, owner, link, symlink, tree, hotlist, connection_manager, usermenu, skin_dialog, help, quickcd, pattern (select/unselect), overwrite, quit, rename, layout, panel_options, config, mc_ext, percent, cmdline, exec, menubar, panel | `src/filemanager/*.rs` |
| **Editor (16 modules)** | buffer (gap), cursor, mode, prompt, search, replace, goto, save, format, history, bookmark, completion, macro, syntax, view | `src/editor/*.rs` |
| **Viewer (6 modules)** | text, hex, source (gz/bz2 capped 256 MiB), search, goto | `src/viewer/*.rs` |
| **Terminal layer** | color (Theme), event, mc_skin, popup, status, **subshell**, mod | `src/terminal/*.rs` |
| **Widgets** | button, check, radio, input, dialog, gauge | `src/widget/*.rs` |
| **VFS** | local, tar, zip, cpio, extfs, sftp (feature), ftp (feature) | `src/vfs/*.rs` |
| **Skin system** | 8 built-in (default-dark/light, mc-classic/dark/dark-gray, high-contrast, solarized-dark, nord), TOML parser, runtime Alt-S, config persistence | `src/skin/`, `src/terminal/color.rs`, `src/filemanager/skin_dialog.rs` |
| **Shared palette** | 33-slot `redbear-tui-theme` crate, WCAG 2.1 AA-verified, 256/16-color fallback | `local/recipes/tui/redbear-tui-theme/` |
| **i18n** | 6 locales (en/de/es/fr/ja/zh-CN), 97 keys each, `t!()` wired into 17 dialog strings | `source/locales/*.yml` |
| **Ops engine** | copy/move/delete/mkdir with symlink-safety (lstat), progress | `src/ops/*.rs` |
### 1.2 Binding coverage vs MC
MC's `mc.default.keymap` defines **380 active bindings across 15 contexts**
(~77 in `[filemanager]` + `[filemanager:xmap]` alone). TLC binds **37 Cmd
variants in the global keymap** plus in-context handlers (Ctrl-X prefix in
`app.rs`, viewer/editor local keys). Mapping the filemanager+xmap layer:
| Category | MC bindings | TLC done | Coverage |
|---|---|---|---|
| F-keys (F1F11) | 11 | 11 | **100%** |
| File ops (copy/move/del/mkdir/rename) | 8 | 8 | **100%** |
| Panel navigation (tab/swap/reload/hidden/layout) | 9 | 9 | **100%** |
| Marking (toggle/range/invert/group select/unselect) | 7 | 7 | **100%** |
| Sort + history + hotlist | 9 | 9 | **100%** |
| Ctrl-X extended (chmod/chown/link/symlink/compare) | 8 | 5 | 63% |
| Subshell / shell integration | 6 | 2 | 33% |
| Power dialogs (jobs, panelize, vfs list, screen list) | 8 | 0 | 0% |
| **Total filemanager-tier** | **~77** | **~55** | **~71%** |
The remaining 29% is concentrated in three clusters: the full PTY subshell,
the power-dialog family (Jobs/Panelize/VFS/Screen-list), and the long-tail
editor/viewer niceties.
### 1.3 Critical correctness posture (resolved)
The 2026-06-13 audit found 4 CRITICAL defects (symlink-following in
delete/copy/count, unbounded gz decompress). **All four are fixed** with
`lstat` + 256 MiB caps and covered by 5 symlink-safety tests. The
`#![deny(unsafe_code)]` guarantee holds — zero `unsafe` in any `.rs` file.
### 1.4 Known weaknesses (carry-over from `PLAN.md` §3.B)
| # | Severity | Item | Status |
|---|---|---|---|
| W1 | **HIGH** | 5 production unwraps remain (4× `Mutex::lock().unwrap()` now poison-recovered, 1× `Tui::default()` legit) | Acceptable; documented |
| W2 | **HIGH** | `vfs/zip.rs` reads whole archive into memory (no 256 MiB cap like viewer has) | **Open** — streaming zip needed |
| W3 | MEDIUM | SFTP `AcceptAnyKey::check_server_key` always returns `Ok(true)` | **Open** — TOFU known_hosts needed |
| W4 | MEDIUM | `filemanager/mod.rs` ~1597 LOC, `editor/mod.rs` ~1227 LOC — over-sized | **Open** — split recommended |
| W5 | MEDIUM | VFS backends defined but filemanager only operates on local paths (no UI enters `extfs://`/`tar://`) | **Open** — wiring gap |
| W6 | LOW | 2 clippy warnings (1 intentional glob ASCII compare, 1 bitflags macro artifact) | Acceptable |
---
## 2. Improvement Plan — Five Axes
The user asked for parity across **visuals, functions, keyboard shortcuts,
dialogs, and themes** — "but better graphics since we use ratatui." Each axis
below lists (a) current state, (b) MC parity target, (c) ratatui-era
improvement beyond MC.
### AXIS A — VISUALS & RENDERING
#### A.1 Backend migration: termion → crossterm (PREREQUISITE for mouse/Windows)
**Why this is on the critical path:** TLC uses `ratatui` with
`features = ["termion"]`. The termion backend does **not** deliver mouse
events, focus events, or bracketed-paste in a portable way. MC supports
mouse column-click sorting, double-click open, and scrollbar drag. Until tlc
has a backend that surfaces those events, **all mouse features are blocked**
and the "better graphics" promise can't be fully delivered. Crossterm is the
ratatui backend that exposes `MouseEvent`, `ResizeEvent`, `FocusEvent`, and
`PasteEvent`.
**⚠ REDOX-FIRST CAVEAT (this is the primary platform).** Crossterm's Redox
support is **not assumed**. Termion already works on Redox today (via the
`redox_termios`/`redox_event` deps already in `Cargo.toml`); crossterm must
be **proven** on `x86_64-unknown-redox` before it can replace termion. The
migration is therefore a **verify-then-adopt** task, not a blind swap:
**Action (Redox-first sequence):**
1. **Spike first:** `cargo build --target x86_64-unknown-redox` a throwaway
crate that pulls `crossterm` and calls `enable_raw_mode` + reads one
event. If it does **not** cross-compile or does not link on Redox,
**stop** — go to the fallback below.
2. **QEMU smoke:** if the spike builds, run it inside a `redbear-mini` guest
and confirm raw mode + a key event round-trips. Mouse event optional for
the spike.
3. **Only if both pass:** `Cargo.toml`
`ratatui = { version = "0.30", default-features = false, features = ["crossterm"] }`,
drop the `termion` direct dep, and rewrite `src/terminal/event.rs` to
consume `crossterm::event::Event`. Keep the `Key`/`Cmd` translation layer
— only the source enum changes. Verify `Ctrl-O` subshell still restores
the terminal cleanly under crossterm on **both** Redox and Linux.
**Fallback if crossterm does NOT support Redox (do not abandon mouse):**
Keep termion as the Redox backend and add crossterm behind a Cargo feature
for the Linux/dev build, selected via `cfg`:
```toml
[dependencies]
ratatui = { version = "0.30", default-features = false }
termion = "4" # Redox + Unix fallback
crossterm = { version = "0.28", optional = true } # Linux/dev mouse path
[features]
default = []
mouse-crossterm = ["dep:crossterm", "ratatui/crossterm"]
# Redox build: ratatui/termion (current behavior), no mouse.
# Linux dev with mouse: cargo build --features mouse-crossterm.
```
`src/terminal/event.rs` then has two adapter impls gated by the feature.
This keeps the **primary platform stable** while still delivering mouse on
the secondary platform and leaving the door open to full crossterm if/when
its Redox support matures. Track upstream crossterm Redox status and
re-evaluate each release.
**Risk:** Medium (was Low before the Redox-first constraint). The spike in
step 1 is the de-risking step — it's cheap and tells you which branch of the
plan to take. Do **not** merge a backend swap that has not cross-compiled to
Redox.
#### A.2 Adopt ratatui 0.30 idioms tlc currently hand-rolls
| Pattern | Today | ratatui idiom | Benefit |
|---|---|---|---|
| Panel rendering | custom loop over rows | `Table` widget with `widths`, `header`, `row_highlight_style`, `highlight_symbol(">>")` | Column alignment, selection symbol, header/footer for free |
| Long lists | manual scroll offset | `List` + `Scrollbar` stateful widget | Consistent scroll UX, native scrollbar glyph |
| Dialog frames | manual `Block` borders | `Block::bordered().title(...).border_style(theme.border)` + `Clear` for popup | Idiomatic, themeable borders |
| Styling | `Style::default().fg(...).bg(...)` chains | `Stylize` trait: `.fg().bg().bold().reversed()` | Concise, composable |
| Layout | manual Rect math in places | `Layout::vertical([Length(1), Min(0), Length(1)])` destructured | Resize-safe, declarative |
| App bootstrap | manual `Tui::new()` + raw panic | `ratatui::run()` + `set_panic_hook()` | Automatic terminal restore on panic |
**Action:** A focused refactor pass (not a rewrite). Each file migrates one
widget at a time, gated by `cargo test`. The `Table` migration of `panel.rs`
is the single highest-impact change — it makes the panel look substantially
more polished (proper column headers, highlight symbol, scrollbar) for
minimal effort.
#### A.3 Color fallback hardening
The shared palette ships `fallback_256()` and `fallback_16_name()`, but tlc
renders `Color::Rgb` directly. On a 256-color terminal (common in tmux/screen
without `Tc`), every `Rgb` silently degrades to the terminal's nearest
match — which can be ugly.
**Action:** In `terminal/color.rs`, add a `Theme::resolved_colors(capability)
-> Theme<Color>` adapter that pre-computes the palette for the detected
capability (`ColorCapability::TrueColor | Color256 | Color16`). Probe once at
startup via terminfo/`COLORTERM`. This is what MC's `tty_colorize` does; tlc
should match it.
#### A.4 Visual parity items MC has and tlc lacks
| Item | MC | TLC | Effort |
|---|---|---|---|
| File-type glyphs (`/`, `~`, `@`, `*`, `=`, `\|`) in name column | 13 glyphs | Basic | Small — `Panel::glyph_for(entry)` |
| Sort indicator arrows in column header (`▼ size`) | Yes | No | Small |
| Filename scroll indicators `{` `}` when truncated | Yes | No | Small |
| Free-space readout in panel footer (`<Free: 12G>`) | Yes | No | Small |
| Mini-status that adapts (SEL/MID/TOTAL, symlink target, `UP--DIR`) | Yes | Basic | Medium |
| Ruler / position percent in viewer | Yes | No | Small |
| Hex view 16-byte offset column + ASCII gutter | Yes | Partial (`hex.rs` exists) | Medium |
---
### AXIS B — FUNCTIONS (Feature Parity)
Ranked by user-visible impact. Each item lists MC source location,
acceptance criterion, and effort.
#### B.1 PTY-based persistent subshell *(HIGH — MC's signature feature)*
- **MC:** `src/subshell/*` — PTY fork, 7 shell-type init scripts, CWD-pipe
sync, `SIGSTOP`/`SIGCONT` handshake, Ctrl-O toggle.
- **TLC today:** `src/terminal/subshell.rs` drops the entire `Tui`, spawns
`$SHELL`, waits, recreates `Tui`. Works as an escape hatch but loses the
"panels overlay a live shell" behavior MC users expect.
- **Acceptance:** Ctrl-O shows the shell *behind* translucent panels; the
cmdline can `cd` into the shell's CWD; typing in the shell while panels are
hidden works; Ctrl-O again restores panels with the shell's new CWD.
- **Approach:** `portable-pty` or `rustix::pty` crate; non-blocking poll of
the PTY master fd in the event loop; render captured shell output to a
background buffer.
- **Effort:** Large (~2000 LoC, 7 shell init scripts). Sequence **after**
the crossterm migration.
#### B.2 Syntax highlighting engine *(HIGH — editor credibility)*
- **MC:** `src/editor/syntax.c` + 102 `.syntax` files in `misc/syntaxes/`.
- **TLC today:** `src/editor/syntax.rs` exists but ships no bundled rules.
`syntect` is a default feature (oniguruma-based) but isn't wired into the
render path.
- **Acceptance:** Opening a `.rs`/`.c`/`.py`/`.sh` file shows highlighted
keywords, strings, comments using the active theme's palette.
- **Two implementation paths:**
1. **syntect path (recommended):** wire `syntect` into `editor/view.rs`.
Ship a curated subset of `.sublime-syntax` files. Map syntect styles →
theme palette (8 token classes). Pros: mature, 100+ languages. Cons:
larger binary (+~600 KB), regex compile cost.
2. **MC-syntax path:** port MC's own `.syntax` rule format (first-rule-
matches, context stack). Pros: byte-compatible with MC's 102 files, small.
Cons: reinvents a tokenizer.
- **Effort:** Medium-large. Path 1 is faster to value.
#### B.3 Power dialogs (Jobs / Panelize / VFS list / Screen list)
- **MC:** `src/filemanager/{dialog,panelize}.c`, `src/vfs/vfs.c`.
- **TLC today:** None of these.
- **Acceptance:** Ctrl-X j shows background copy/move jobs with cancel;
Ctrl-X ! runs a shell command and shows its stdout lines as a read-only
panel; Ctrl-X a lists active VFS mounts.
- **Effort:** Medium. Reuses the existing dialog framework. Depends on B.4
(background ops) for Jobs.
#### B.4 Background file operations
- **MC:** `file.c` forks a child for long copies; the UI stays responsive;
the Jobs dialog lists/cancels them.
- **TLC today:** Copy/move/delete are synchronous with a modal progress bar.
- **Acceptance:** A "Background" button on the progress dialog moves the op
to a worker thread; the UI returns to panels; Ctrl-X j lists running ops.
- **Effort:** Medium-large. Needs a `BackgroundJobs` registry, cancellation,
and a thread/async runtime. The `async-vfs` feature already pulls `tokio`.
#### B.5 Compare directories (Ctrl-X d)
- **MC:** Marks files that differ between the two panels (quick / size-only
/ thorough modes).
- **TLC today:** `Cmd::CompareDirs` exists and is handled, but verify depth
(quick vs thorough).
- **Effort:** Small (likely already done — verify and add thorough mode).
#### B.6 External mc.ext dispatch *(verify wiring)*
- **MC:** `mc.ext` INI maps file extensions → Open/View/Edit commands.
- **TLC today:** `src/filemanager/mc_ext.rs` and `percent.rs` (17 escapes)
exist. **Verify** Enter/F3/F4 actually consult `mc_ext` before falling
back to the built-in editor/viewer.
- **Effort:** Small (likely a wiring audit + tests).
#### B.7 Editor long-tail
| Feature | MC key | Effort |
|---|---|---|
| Rectangular (column) block selection | Ctrl-Q | Medium |
| Format paragraph | Alt-P | Small |
| Spell check (pipe to aspell) | Alt-B | Small |
| Persistent cursor position per file | (auto) | Small |
| Match bracket | Alt-B (verify vs viewer bookmark conflict) | Small |
#### B.8 Viewer long-tail
| Feature | MC key | Effort |
|---|---|---|
| Hex **edit** mode (mutate bytes) | F4 in hex | Medium |
| Nroff mode (backspace-bold/underline) | Alt-N | Small |
| Magic mode (preprocess via mc.ext) | Alt-M | Small |
| Growing/tail-f buffer | F-r | Small |
| Goto byte offset | Alt-g | Small |
---
### AXIS C — KEYBOARD SHORTCUTS (Keymap Completeness)
TLC's global keymap covers the F-layer and core ops. The remaining gaps are
in **context-local** bindings (editor, viewer, dialog-internal) and the
Ctrl-X prefix family. Concrete missing bindings to add:
| Binding | MC key | Action | Effort |
|---|---|---|---|
| Ctrl-X v | relative symlink | `Cmd::SymlinkRelative` | Small |
| Ctrl-X Ctrl-S | edit symlink target | `Cmd::SymlinkEdit` | Small |
| Ctrl-X d | compare dirs | **verify bound** (Cmd exists) | — |
| Ctrl-X ! | external panelize | `Cmd::Panelize` | Small (after B.3) |
| Ctrl-X j | background jobs | `Cmd::Jobs` | Small (after B.3) |
| Ctrl-X a | VFS list | `Cmd::VfsList` | Small (after B.3) |
| Alt-` | screen list | `Cmd::ScreenList` | Small |
| Alt-Shift-E | viewed/edited history | `Cmd::EditHistory` | Small |
| Ctrl-Space | directory size | `Cmd::DirSize` | Small |
| Alt-= | equal split | `Cmd::EqualSplit` | Small |
| Alt-Shift-←/→ | adjust split ratio | `Cmd::SplitLess`/`More` | Small |
| Alt-a / Alt-Shift-A | insert current/other path into cmdline | `Cmd::InsertPath` | Small |
| Ctrl-Enter / Alt-Enter | insert cursor filename into cmdline | `Cmd::InsertFile` | Small |
| Shift-F10 | quiet quit (no confirm) | `Cmd::QuitQuiet` | Small |
| Ctrl-Z | suspend (SIGTSTP) | `Cmd::Suspend` | Small |
| Alt-! | filtered view (pipe) | `Cmd::FilteredView` | Small |
**Also:** add a **Learn Keys** dialog (Options → Learn Keys) so users on
unusual terminals can rebind. This is MC's escape hatch for broken terminals
and is cheap to port.
---
### AXIS D — DIALOGS (Inventory & Gaps)
TLC already has **30 filemanager dialog modules** — the dense part of MC's
dialog surface is covered. The remaining dialog work:
| Dialog | MC location | TLC | Priority |
|---|---|---|---|
| Configuration (verbose/safe-delete/pause-after-run/shell-patterns) | boxes.c | `config_dialog.rs` exists — **verify completeness** | Medium |
| Confirmation toggles (per-op) | boxes.c | **Missing** | Medium |
| Listing format editor (BNF format string) | boxes.c | Missing (cycle only) | Low |
| Sort order (full radio dialog) | boxes.c | cycle only (`Alt-T`) | Medium |
| Display bits (8th bit, full 8-bit) | boxes.c | Missing | Low |
| Learn keys | boxes.c | Missing | Low |
| Virtual FS settings (ftp/sftp defaults) | boxes.c | Missing | Low |
| Advanced chown (combined owner+perm) | chmod_dialog.c | Missing | Low |
| Chattr (ext2 inode flags) | chattr.c | Missing (ext2-only) | Low |
| Filtered view (pipe through cmd) | cmd.c | Missing | Medium |
| Background jobs | dialog.c | **Missing** | Medium |
| External panelize | panelize.c | **Missing** | Medium |
| Active VFS list | vfs.c | Missing | Low |
| Directory hotlist editor | hotlist.c | `hotlist.rs` exists — verify add/remove | Low |
**Pattern note:** Every TLC dialog should be rendered through the
`widget::dialog` helper with `Clear` (popup pattern) and theme-derived
border styles for visual consistency. Audit the existing 30 for consistency.
---
### AXIS E — THEMES (Skin System)
TLC's skin system is **genuinely ahead of MC** in architecture (shared
33-slot WCAG-verified palette, runtime switching, TOML format). Gaps are in
**breadth** and **runtime fidelity**.
#### E.1 Ship the 40 MC reference skins as importable themes
`source/mc-skins/skins/` already contains **40 MC `.ini` skin files**
(default, dark, nicedark, sand256, gotar, xoria256, julia256, modarin256,
the modarcon16 family, the seasons family, etc.). Today only **5 of these**
are approximated as built-in `const Theme` values (mc-classic, mc-dark,
mc-dark-gray + the redbear defaults).
**Action:** Write a one-time `mc-skin-import` tool (or build-script) that
parses each `.ini` and emits a `Theme` constant. Add an "MC Skins" section to
the Alt-S dialog. This gives tlc **45+ skins** with zero runtime cost.
**MC `.ini` → TLC `Theme` mapping** (define once):
- `[core]` `_default_`, `selected`, `marked`, `markselect` → background/foreground/selection_*/marked_*
- `[dialog]`, `[error]`, `[menu]`, `[popupmenu]`, `[buttonbar]`, `[statusbar]`, `[help]` → corresponding Theme slots
- `[filehighlight]` `directory`/`executable`/`symlink`/`device`/`stalelink`/`hardlink`/`socket` → file-type slots
- `[editor]`, `[viewer]`, `[diffviewer]` → editor/viewer slots
- 256-color hex (`color005f87`) and truecolor (`#005f87`) syntax both parse.
#### E.2 User skin directory + hot-reload
- Scan `~/.config/tlc/skin/*.toml` (already done in `skin_dialog.rs`).
- **Add:** `Alt-Shift-R` to hot-reload the current skin from disk (lets
users iterate on a skin without restarting).
- **Add:** skin inheritance (`extends = "mc-dark"`) so user skins only
override the slots they change.
#### E.3 Truecolor/256/16 capability detection
See A.3. The skin must degrade gracefully. Add a `--color-test` CLI
subcommand that prints the palette in the detected capability for diagnosis.
#### E.4 cub / redbear-info / redbear-netctl palette migration
The shared crate exists; `cub` is wired; `redbear-info` and
`redbear-netctl-console` still use raw ANSI-16. Migrating them makes the
whole Red Bear TUI ecosystem visually coherent (same brand red, same
selection color). Tracked in `redbear-tui-theme/README.md`.
---
## 3. ratatui Best Practices to Adopt (Grounded in v0.30 docs)
These are the modernization items the internal `PLAN.md` does **not** cover.
Each is justified by the ratatui 0.30 docs retrieved for this plan.
### 3.1 `ratatui::run()` + panic hook *(safety)*
ratatui 0.30's canonical bootstrap is `ratatui::run(|terminal| { ... })`,
which handles `enable_raw_mode`, alternate-screen enter/exit, and terminal
restoration **including on panic**. TLC currently does this manually in
`Tui::new()`/`Drop`. Migrating to `ratatui::run()` + `set_panic_hook()`
removes a class of "terminal left broken after crash" bugs. (Source:
ratatui 0.30 lib.rs `run()` example.)
### 3.2 Stateful widgets via `render_stateful_widget`
TLC's panel keeps a manual `selected`/`offset`. ratatui's `List`/`Table` +
`ListState`/`TableState` manage selection/offset natively and render the
`highlight_symbol` and `row_highlight_style`. Migrating the panel to
`Table` (which supports column widths, header row, footer for mini-status)
is the single biggest visual win for the least code. (Source: ratatui 0.30
`Table` example with `row_highlight_style(Style::new().reversed())` and
`highlight_symbol(">>")`.)
### 3.3 `Stylize` trait for concise styling
Replace `Style::default().fg(c).bg(c2).add_modifier(Modifier::BOLD)` with
`.fg(c).bg(c2).bold()`. Composable, readable, and the idiomatic ratatui 0.30
style. (Source: ratatui 0.30 `Table` Stylize example: `.red().italic()`.)
### 3.4 `Clear` + centered `Rect` for every popup
The popup pattern is: `frame.render_widget(Clear, popup_area)` then render
the dialog widget. TLC's `terminal/popup.rs` and `widget/dialog.rs` do this;
**audit the 30 dialogs** for consistency — any dialog rendered without `Clear`
first leaves the underlying panel bleeding through.
### 3.5 `Scrollbar` widget for long lists
ratatui 0.30 ships a `Scrollbar` stateful widget. The file panel (long
directories), the find-results list, the hotlist, and the help dialog all
benefit. Today tlc renders scroll position only implicitly (cursor row).
### 3.6 `Layout` with `Constraint::{Fill, Length, Min}` — no manual Rect math
Any place tlc computes `Rect { x, y, width, height }` by hand should migrate
to `Layout::vertical([Length(1), Min(0), Length(1)])` destructured into
`[title, main, status]`. Resize-safe and declarative. (Source: ratatui 0.30
Layout example.)
### 3.7 Immutable-state rendering recommended
ratatui 0.30 docs explicitly recommend **immutable** state patterns
(`fn render(frame, area, &state)`) for most cases, reserving
`StatefulWidget` for widgets that mutate state during render. TLC's
`render(frame, &self, theme)` already follows this — keep it. Avoid
`Rc<RefCell<State>>` interior mutability unless a widget truly needs it.
### 3.8 Test the TUI with `TestBackend`
ratatui ships `TestBackend` + `buffer!` / `assert_buffer_eq`. TLC has 698
unit tests on logic but **zero render-snapshot tests**. Adding
`TestBackend`-based render tests for each widget/dialog catches visual
regressions automatically. High leverage, low cost.
---
## 4. Prioritized Roadmap
Sequenced so each phase ships a demonstrably better tlc and unblocks the
next. Effort in "sessions" (one session ≈ a focused half-day).
### Phase R — ratatui Modernization (PREREQUISITE) *[~4 sessions]*
| # | Task | Axis | Effort |
|---|---|---|---|
| R1 | **Redox-first spike:** verify crossterm cross-compiles + runs on `x86_64-unknown-redox`; if yes → swap backend, if no → dual-backend feature flag (A.1) | A.1 | 1.5 |
| R2 | Bootstrap via `ratatui::run()` + panic hook | 3.1 | 0.5 |
| R3 | Migrate panel to `Table` + `TableState` + `highlight_symbol` | A.2/3.2 | 1.5 |
| R4 | Add `Scrollbar` to panel/help/hotlist/find | 3.5 | 0.5 |
**Why first:** unblocks mouse **without breaking the primary platform** (R1
spike gates the approach), makes the panel look substantially better
immediately (R3), and every subsequent visual improvement lands on the
modern base. R1 is Redox-gated by design.
### Phase 1 — Visual Parity Polish *[~3 sessions]*
| # | Task | Axis |
|---|---|---|
| V1 | File-type glyphs in name column | A.4 |
| V2 | Sort indicator + filename scroll indicators | A.4 |
| V3 | Free-space + adaptive mini-status | A.4 |
| V4 | Color capability detection + 256/16 fallback (A.3) | A.3 |
| V5 | `Stylize` trait sweep + `Clear` audit across 30 dialogs | 3.3/3.4 |
| V6 | Render-snapshot tests with `TestBackend` (3.8) | 3.8 |
### Phase 2 — Theme Breadth *[~2 sessions]*
| # | Task | Axis |
|---|---|---|
| T1 | mc-skin importer: convert 40 `.ini` → `Theme` consts | E.1 |
| T2 | "MC Skins" section in Alt-S dialog | E.1 |
| T3 | Skin `extends =` inheritance + `Alt-Shift-R` hot-reload | E.2 |
| T4 | `--color-test` CLI subcommand | E.3 |
### Phase 3 — Function Gaps (Power User) *[~6 sessions]*
| # | Task | Axis |
|---|---|---|
| F1 | Background file ops + thread registry | B.4 |
| F2 | Jobs dialog (Ctrl-X j) | B.3 |
| F3 | External panelize (Ctrl-X !) | B.3 |
| F4 | VFS list (Ctrl-X a) | B.3 |
| F5 | Compare directories thorough mode | B.5 |
| F6 | mc.ext dispatch audit + tests | B.6 |
| F7 | Syntax highlighting (syntect path) | B.2 |
### Phase 4 — Subshell (Signature Feature) *[~5 sessions]*
| # | Task | Axis |
|---|---|---|
| S1 | **Redox PTY path** via `scheme:pty`/`redox-scheme` + **Linux path** via `rustix::pty`; abstract behind one trait so both platforms share the CWD-sync logic | B.1 |
| S2 | Shell init scripts (bash/zsh/fish) — verify `ion`/default Redox shell works on the primary platform | B.1 |
| S3 | CWD-pipe sync + Ctrl-O overlay rendering | B.1 |
| S4 | cmdline `cd` reflects subshell CWD | B.1 |
> **Redox note:** the primary platform's shell and PTY surface differ from
> Linux. The S1 trait must be validated in a `redbear-mini` QEMU guest, not
> just on the Linux host. Keep the existing "drop/recreate Tui" path as the
> fallback until the PTY overlay is proven on Redox.
### Phase 5 — Long Tail (Editor/Viewer/Keymap) *[~5 sessions]*
| # | Task | Axis |
|---|---|---|
| L1 | Remaining Ctrl-X bindings + Alt-`/Alt-Shift-E/Ctrl-Space/Alt-=/split-adjust | C |
| L2 | Editor: rectangular block, format-paragraph, match-bracket | B.7 |
| L3 | Viewer: hex edit, nroff, magic, growing, goto-offset | B.8 |
| L4 | Missing dialogs: Confirmation toggles, Sort order, Learn keys | D |
| L5 | Mouse support (column-click sort, double-click open, scrollbar drag) | A.1 (post-R1) |
**Total: ~25 sessions (~68 focused weeks)** to full MC parity + ratatui-era
visual superiority.
---
## 5. Risks & Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| **crossterm does not support Redox (primary platform)** | **High** | R1 is a **spike, not a swap** — verify cross-compile + QEMU run *first*; if it fails, fall back to the dual-backend feature-flag (termion on Redox, crossterm on Linux) defined in A.1. Never merge a backend change unproven on Redox |
| crossterm migration breaks Ctrl-O restore | High | Phase R1 ships with a dedicated restore test on **both** Redox and Linux; keep termion reachable until verified |
| PTY subshell Redox path differs from Linux | Medium | S1 abstracts PTY behind a trait with `RedoxPty` + `UnixPty` impls; validate in `redbear-mini` QEMU; keep drop-Tui fallback behind `subshell = "drop"` config flag |
| PTY subshell is a 2000-LoC rabbit hole | Medium | Sequence after Phases R3 so it lands on a stable base |
| syntect adds 600 KB + regex compile cost | Medium | Lazy-compile grammars on first open; strip unused `.sublime-syntax`; verify the heavier binary still cross-compiles to Redox |
| mc-skin importer drifts from `.ini` semantics | Low | Add a round-trip test: import → export → diff against original |
| TestBackend render tests are brittle | Low | Pin ratatui version; snapshot only stable widgets (panel row, dialog frame) |
| Backend split (`filemanager/mod.rs` 1597 LOC) deferred | Low | Accept; functional correctness first, split when it blocks a feature |
---
## 6. What I Explicitly Do NOT Recommend
1. **Do not** rewrite the editor gap buffer — it is correct, tested, and the
undo invariant is verified. Rewriting would be churn.
2. **Do not** migrate to a Cargo workspace for its own sake — single-crate
builds are working and simple. Split only `filemanager/mod.rs` and
`editor/mod.rs` if they actively block a feature.
3. **Do not** replace the shared `redbear-tui-theme` 33-slot palette — it is
the right abstraction and already wired to cub. Extend it, don't fork it.
4. **Do not** port MC's `.syntax` rule format if syntect is available —
syntect is mature and MC's format is a reimplementation.
5. **Do not** add `unsafe` for performance — the `#![deny(unsafe_code)]`
guarantee is a project invariant. ratatui + crossterm are safe.
---
## 7. Success Criteria
The plan is complete when **all** of the following hold:
- [ ] **Every item below is verified on `x86_64-unknown-redox` (primary platform), not just the Linux host** — cross-compile + QEMU smoke for each
- [ ] Mouse works (column-click sort, double-click open) — *Phase R + L5* (Redox: via whichever backend A.1's spike validated; Linux: crossterm)
- [ ] Panel renders via `Table` with highlight symbol + scrollbar — *Phase R3*
- [ ] 45+ skins available in Alt-S (8 redbear + ~40 MC imports) — *Phase 2*
- [ ] Ctrl-O overlays a live PTY subshell — *Phase 4* (Redox PTY path proven in QEMU)
- [ ] Editor shows syntax highlighting for ≥20 languages — *Phase 3 F7*
- [ ] All MC filemanager/xmap bindings either bound or explicitly N/A — *Phase 5 L1*
- [ ] Background copy/move with Jobs dialog — *Phase 3 F1/F2*
- [ ] `cargo test --lib` ≥ 750 passing, 0 failures
- [ ] `cargo build --release --target x86_64-unknown-redox` clean, binary boots in `redbear-mini` guest
- [ ] Render-snapshot tests cover panel + each dialog — *Phase 1 V6*
- [ ] `tlc --color-test` renders correctly in 16/256/truecolor — *Phase 2 T4*
At that point tlc is **at parity with MC on the axes users feel daily, and
visibly better on the axes ratatui enables** (smooth scrolling, crisp
truecolor, mouse, snapshot-tested rendering) — **on Red Bear OS first, Linux
second.**
---
*End of plan. See `PLAN.md` for the internal phase-by-phase implementation
log and `README.md` for build/run instructions.*
+1 -1
View File
@@ -137,7 +137,7 @@ serde_yaml = "0.9"
[target.'cfg(target_os = "redox")'.dependencies]
libredox = "0.1.17"
redox_event = "0.4.6"
redox_syscall = { version = "0.7", features = ["std"] }
redox_syscall = { version = "0.8", features = ["std"] }
redox_termios = "0.1.3"
[profile.release]
@@ -89,6 +89,18 @@ impl Cmdline {
let _ = self.input.handle_key(Key::BACKSPACE);
}
/// Activate the cmdline (if not already) and append `text` at the
/// cursor. Used by `Alt-a` / `Alt-A` / `Alt-;` to drop a path or
/// filename into the command line for editing.
pub fn insert_text(&mut self, text: &str) {
if !self.active {
self.activate();
}
for c in text.chars() {
self.input.insert_char(c);
}
}
/// Close the cmdline without executing. Resets the input.
pub fn deactivate(&mut self) {
self.active = false;
@@ -671,6 +671,69 @@ impl FileManager {
Ok(true)
}
Cmd::ViewerNextFile | Cmd::ViewerPrevFile => Ok(true),
Cmd::QuitQuiet => Ok(false),
Cmd::EqualSplit => {
self.runtime.equal_split = Some(true);
self.status.set_message("Equal split".to_string());
Ok(true)
}
Cmd::DirSize => {
let p = self.active_panel().cursor_path();
if p.is_dir() {
let bytes = crate::ops::count_bytes(&[p.clone()]);
self.status
.set_message(format!("{}: {}", p.display(), format_size(bytes)));
} else {
self.status.set_message("Not a directory".to_string());
}
Ok(true)
}
Cmd::InsertCurPath => {
let p = self.active_panel().path().to_string_lossy().to_string();
self.cmdline.insert_text(&p);
Ok(true)
}
Cmd::InsertOtherPath => {
let p = self.other_panel().path().to_string_lossy().to_string();
self.cmdline.insert_text(&p);
Ok(true)
}
Cmd::InsertCurFile => {
let name = self
.active_panel()
.cursor_path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
self.cmdline.insert_text(&name);
Ok(true)
}
Cmd::SplitLess | Cmd::SplitMore => {
self.runtime.equal_split = Some(false);
self.status.set_message(
"Manual split-ratio adjust needs a split field; equal-split toggled off"
.to_string(),
);
Ok(true)
}
Cmd::SymlinkRelative
| Cmd::SymlinkEdit
| Cmd::Panelize
| Cmd::Jobs
| Cmd::VfsList
| Cmd::ScreenList
| Cmd::EditHistory
| Cmd::FilteredView => {
let name = cmd.name();
self.status
.set_message(format!("{name}: not implemented in this build"));
Ok(true)
}
Cmd::Suspend => {
self.status
.set_message("Suspend: use Ctrl-O to drop to a shell".to_string());
Ok(true)
}
}
}
@@ -121,6 +121,46 @@ pub enum Cmd {
PanelOptionsDialog,
/// F9 → Options → Configuration — open the configuration dialog.
ConfigDialog,
/// C-x v — create a relative symlink (path relative to the target's dir).
SymlinkRelative,
/// C-x C-s — edit the target of an existing symlink.
SymlinkEdit,
/// C-Space — compute the recursive size of the directory under the cursor.
DirSize,
/// M-= — force the two panels to an equal-width split.
EqualSplit,
/// M-Shift-Left — shrink the active panel (move the split left).
SplitLess,
/// M-Shift-Right — grow the active panel (move the split right).
SplitMore,
/// M-a — insert the active panel's path into the command line.
InsertCurPath,
/// M-A — insert the inactive panel's path into the command line.
InsertOtherPath,
/// Insert the filename under the cursor into the command line.
/// (MC binds Alt-Enter / Ctrl-Enter; tlc binds Alt-Enter to
/// [`Cmd::Cmdline`], so this is bound to `Alt-;` to avoid the clash.)
InsertCurFile,
/// Shift-F10 (a.k.a. F20) — quit immediately without the confirm dialog.
QuitQuiet,
/// C-z — suspend tlc (send SIGTSTP to self, return to the parent shell).
Suspend,
/// M-E — open the viewed/edited file history dialog.
EditHistory,
/// M-` — open the virtual-screen list dialog.
ScreenList,
/// C-x ! — run a shell command and show its stdout as a read-only panel
/// (external panelize). Phase 3 F3 implements the panel; the binding
/// is wired now so the key is not dead.
Panelize,
/// C-x j — open the background-jobs dialog (running copy/move/etc.).
/// Phase 3 F1/F2 implements the jobs engine; the binding is wired now.
Jobs,
/// C-x a — open the active-VFS list dialog (mounted tar/zip/sftp/...).
VfsList,
/// M-! — pipe the cursor file through a shell command and view the result.
/// Phase 3 implements the viewer-pipe; the binding is wired now.
FilteredView,
}
impl Cmd {
@@ -177,6 +217,23 @@ impl Cmd {
Cmd::LayoutDialog => "Layout options",
Cmd::PanelOptionsDialog => "Panel options",
Cmd::ConfigDialog => "Configuration",
Cmd::SymlinkRelative => "Relative symlink",
Cmd::SymlinkEdit => "Edit symlink",
Cmd::DirSize => "Directory size",
Cmd::EqualSplit => "Equal split",
Cmd::SplitLess => "Shrink panel",
Cmd::SplitMore => "Grow panel",
Cmd::InsertCurPath => "Insert current path",
Cmd::InsertOtherPath => "Insert other path",
Cmd::InsertCurFile => "Insert filename",
Cmd::QuitQuiet => "Quit (no confirm)",
Cmd::Suspend => "Suspend",
Cmd::EditHistory => "View/edit history",
Cmd::ScreenList => "Screen list",
Cmd::Panelize => "External panelize",
Cmd::Jobs => "Background jobs",
Cmd::VfsList => "Active VFS list",
Cmd::FilteredView => "Filtered view",
}
}
}
@@ -315,6 +372,68 @@ pub fn default_keymap() -> Keymap {
km.bind(Key::alt('l'), Cmd::ListingCycle);
km.bind(Key::alt('g'), Cmd::LayoutDialog);
km.bind(Key::alt('p'), Cmd::PanelOptionsDialog);
// C-x prefix family: mapped CTRL|ALT+<key>, matching the existing
// C-x c / C-x o / C-x l / C-x s convention above.
km.bind(
Key {
code: b'v' as u32,
mods: crate::key::Modifiers::CTRL | crate::key::Modifiers::ALT,
},
Cmd::SymlinkRelative,
);
km.bind(
Key {
code: b'!' as u32,
mods: crate::key::Modifiers::CTRL | crate::key::Modifiers::ALT,
},
Cmd::Panelize,
);
km.bind(
Key {
code: b'j' as u32,
mods: crate::key::Modifiers::CTRL | crate::key::Modifiers::ALT,
},
Cmd::Jobs,
);
km.bind(
Key {
code: b'a' as u32,
mods: crate::key::Modifiers::CTRL | crate::key::Modifiers::ALT,
},
Cmd::VfsList,
);
km.bind(Key::alt('!'), Cmd::FilteredView);
km.bind(Key::alt('`'), Cmd::ScreenList);
km.bind(Key::alt('E'), Cmd::EditHistory);
km.bind(
Key {
code: 0x00,
mods: crate::key::Modifiers::CTRL,
},
Cmd::DirSize,
);
km.bind(Key::alt('='), Cmd::EqualSplit);
km.bind(Key::alt('a'), Cmd::InsertCurPath);
km.bind(Key::alt('A'), Cmd::InsertOtherPath);
km.bind(Key::alt(';'), Cmd::InsertCurFile);
km.bind(Key::f(20), Cmd::QuitQuiet);
km.bind(Key::ctrl('z'), Cmd::Suspend);
// Alt-Shift-arrow (delivered once crossterm lands; harmless under termion).
km.bind(
Key {
code: 0x2190,
mods: crate::key::Modifiers::ALT | crate::key::Modifiers::SHIFT,
},
Cmd::SplitLess,
);
km.bind(
Key {
code: 0x2192,
mods: crate::key::Modifiers::ALT | crate::key::Modifiers::SHIFT,
},
Cmd::SplitMore,
);
km
}
@@ -374,4 +493,32 @@ mod tests {
assert!(!Cmd::PanelOptionsDialog.name().is_empty());
assert!(!Cmd::ConfigDialog.name().is_empty());
}
#[test]
fn phase5_l1_bindings_are_wired() {
let km = default_keymap();
assert_eq!(km.lookup(Key::alt('=')), Some(Cmd::EqualSplit));
assert_eq!(km.lookup(Key::alt('a')), Some(Cmd::InsertCurPath));
assert_eq!(km.lookup(Key::alt('A')), Some(Cmd::InsertOtherPath));
assert_eq!(km.lookup(Key::alt(';')), Some(Cmd::InsertCurFile));
assert_eq!(km.lookup(Key::alt('!')), Some(Cmd::FilteredView));
assert_eq!(km.lookup(Key::alt('`')), Some(Cmd::ScreenList));
assert_eq!(km.lookup(Key::alt('E')), Some(Cmd::EditHistory));
assert_eq!(km.lookup(Key::ctrl('z')), Some(Cmd::Suspend));
assert_eq!(km.lookup(Key::f(20)), Some(Cmd::QuitQuiet));
assert_eq!(
km.lookup(Key {
code: 0x00,
mods: crate::key::Modifiers::CTRL
}),
Some(Cmd::DirSize)
);
}
#[test]
fn all_cmd_variants_have_nonempty_names() {
for (key, cmd) in default_keymap().bindings() {
assert!(!cmd.name().is_empty(), "{cmd:?} ({key:?}) has empty name");
}
}
}
@@ -15,10 +15,12 @@ pub mod status;
pub mod subshell;
use std::io::{self, Write};
use std::sync::OnceLock;
use anyhow::Result;
use ratatui::backend::TermionBackend;
use ratatui::Terminal;
use rustix::fd::AsFd;
use termion::raw::IntoRawMode;
use termion::raw::RawTerminal;
@@ -30,6 +32,11 @@ type Screen = AlternateScreen<RawTerminal<io::Stdout>>;
/// Terminal backend type used by TLC.
pub type Backend = TermionBackend<Screen>;
/// Saved pre-raw-mode termios of stdout, captured once at the first
/// `Tui::new()`. The panic hook restores it so a panic never leaves the
/// terminal stuck in raw mode / on the alternate screen.
static SAVED_TERMIOS: OnceLock<Option<rustix::termios::Termios>> = OnceLock::new();
/// A TUI terminal session.
///
/// On creation this enters raw mode and switches to the alternate
@@ -62,6 +69,9 @@ impl Tui {
));
}
let stdout = io::stdout();
let _ = SAVED_TERMIOS.get_or_init(|| {
rustix::termios::tcgetattr(stdout.as_fd()).ok()
});
let raw = stdout
.into_raw_mode()
.map_err(|e| anyhow::anyhow!("raw mode: {e}"))?;
@@ -71,6 +81,7 @@ impl Tui {
let backend = TermionBackend::new(screen);
let terminal =
Terminal::new(backend).map_err(|e| anyhow::anyhow!("ratatui init: {e}"))?;
set_panic_hook();
Ok(Self { terminal })
}
@@ -124,6 +135,31 @@ pub fn restore() {
let _ = io::stdout().flush();
}
/// Install a panic hook that leaves the alternate screen, disables raw
/// mode, and shows the cursor before the panic message is printed — so a
/// crash never strands the user in a broken terminal. Mirrors the safety
/// `ratatui::run()` provides, adapted for tlc's owned-`Tui` event loop.
pub fn set_panic_hook() {
static HOOK_INSTALLED: std::sync::Once = std::sync::Once::new();
HOOK_INSTALLED.call_once(|| {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let mut out = io::stdout();
let _ = write!(out, "{}", termion::screen::ToMainScreen);
let _ = write!(out, "{}", termion::cursor::Show);
if let Some(saved) = SAVED_TERMIOS.get().and_then(|o| o.as_ref()) {
let _ = rustix::termios::tcsetattr(
out.as_fd(),
rustix::termios::OptionalActions::Now,
saved,
);
}
let _ = out.flush();
prev(info);
}));
});
}
/// Convenience accessor: get the current terminal size, or 80x24 on failure.
#[must_use]
pub fn current_size() -> (u16, u16) {
Submodule local/sources/base updated: b080287ce3...fa7ff28c4b