From c02a79bc3e1f866490ff6e2bdda6518857ec2e52 Mon Sep 17 00:00:00 2001 From: Kanyin Cai Date: Sat, 21 Mar 2026 10:27:08 +0100 Subject: [PATCH] docs: map existing codebase --- .planning/codebase/ARCHITECTURE.md | 223 ++++++++++++++++++++++++ .planning/codebase/CONCERNS.md | 263 +++++++++++++++++++++++++++++ .planning/codebase/CONVENTIONS.md | 175 +++++++++++++++++++ .planning/codebase/INTEGRATIONS.md | 208 +++++++++++++++++++++++ .planning/codebase/STACK.md | 146 ++++++++++++++++ .planning/codebase/STRUCTURE.md | 256 ++++++++++++++++++++++++++++ .planning/codebase/TESTING.md | 251 +++++++++++++++++++++++++++ 7 files changed, 1522 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 00000000..eb09b7ad --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,223 @@ +# Architecture + +**Analysis Date:** 2026-03-21 + +## Pattern Overview + +**Overall:** Actor-based event-driven TUI application with layered state management + +**Key Characteristics:** +- Async event loop (Tokio) consuming terminal input events and dispatching to named `Actor` implementations +- Strict separation between state (`yazi-core`), actions (`yazi-actor`), and rendering (`yazi-fm` UI widgets) +- Lua scripting layer (`yazi-plugin`) hooks into the same state via `Lives` scoped userdata +- Cross-process pub/sub via a Unix socket server (`yazi-dds`) allowing multiple yazi instances and the `ya` CLI to communicate + +## Layers + +**Entry Point / Binary:** +- Purpose: Initialize all subsystems, start the Tokio event loop, drive `App::serve()` +- Location: `yazi-fm/src/main.rs` +- Contains: `main()`, subsystem `init()` calls in order, global allocator (jemalloc) +- Depends on: Every other crate +- Used by: OS process runner + +**App Loop (`App`):** +- Purpose: Receive `Event` variants from a Tokio MPSC channel; throttle renders (max 10ms interval); coordinate `Dispatcher` +- Location: `yazi-fm/src/app/app.rs` +- Contains: `Core`, `Term`, `Signals`; the `serve()` async loop +- Depends on: `yazi-core`, `yazi-term`, `yazi-actor`, `yazi-shared` +- Used by: `main()` + +**Dispatcher:** +- Purpose: Match each `Event` variant to the right handler — key events go to `Router`, action calls go to `Executor`, render/resize/mouse/paste handled directly +- Location: `yazi-fm/src/dispatcher.rs` +- Contains: `dispatch()` method with exhaustive `Event` match +- Depends on: `Router`, `Executor`, `yazi-actor::Ctx` +- Used by: `App::serve()` + +**Router:** +- Purpose: Translate a `Key` press into an `ActionCow` sequence by looking up `KEYMAP` for the current `Layer` +- Location: `yazi-fm/src/router.rs` +- Contains: `route()`, `matches()`; multi-key chords activate the `which` overlay +- Depends on: `yazi-config::KEYMAP`, `yazi-actor::Ctx` +- Used by: `Dispatcher::dispatch_key()` + +**Executor:** +- Purpose: Dispatch a named `ActionCow` to the correct `Actor::act()` call, organized by `Layer` +- Location: `yazi-fm/src/executor.rs` +- Contains: `execute()` with per-layer handler methods (`app`, `mgr`, `tasks`, `spot`, `pick`, `input`, `confirm`, `help`, `cmp`, `which`, `notify`) +- Depends on: `yazi-actor`, `yazi-macro::act!` +- Used by: `Dispatcher::dispatch_call()` + +**Core State (`Core`):** +- Purpose: Owns all live application state as a single flat struct +- Location: `yazi-core/src/core.rs` +- Contains: `Mgr`, `Tasks`, `Pick`, `Input`, `Confirm`, `Help`, `Cmp`, `Which`, `Notify` +- Depends on: `yazi-core` sub-modules +- Used by: `App`, `Ctx`, `Root` renderer + +**Mgr / Tab:** +- Purpose: File manager state — multiple tabs, yanked clipboard, filesystem watcher, MIME cache +- Location: `yazi-core/src/mgr/mgr.rs`, `yazi-core/src/tab/tab.rs` +- Contains: `Mgr { tabs, yanked, watcher, mimetype }`, `Tab { current, parent, history, selected, spot, preview, finder, search, ... }` +- Depends on: `yazi-fs`, `yazi-watcher`, `yazi-vfs` +- Used by: `Core`, `Ctx` + +**Actor Trait & Implementations:** +- Purpose: Each user-visible command is a zero-size struct implementing `Actor { type Options; fn act(cx, opt) -> Result }` +- Location: `yazi-actor/src/actor.rs` (trait), `yazi-actor/src/mgr/`, `yazi-actor/src/app/`, etc. +- Contains: ~60 mgr actors (cd, arrow, open, yank, paste, search, tab_create, upload...), app actors (bootstrap, resize, plugin, quit...), plus per-overlay actors +- Depends on: `yazi-actor::Ctx`, `yazi-proxy`, `yazi-parser`, `yazi-macro::act!` +- Used by: `Executor` via the `act!` macro + +**Ctx (Action Context):** +- Purpose: Borrows `Core` mutably for the duration of an action; provides convenience accessors for the active tab, current folder, hovered file +- Location: `yazi-actor/src/context.rs` +- Contains: `Ctx { core, term, tab, level, source }`; `Deref`/`DerefMut` into `Core` +- Depends on: `yazi-core::Core`, `yazi-term::Term` +- Used by: every `Actor::act()` implementation + +**Proxy Layer:** +- Purpose: Allow background async tasks (scheduler workers, plugin coroutines) to emit actions without holding `Core` references — they emit `Event::Call` via the shared channel +- Location: `yazi-proxy/src/` +- Contains: `MgrProxy`, `InputProxy`, `CmpProxy`, `TasksProxy`, etc. — thin wrappers that call `emit!(Call(relay!(...)))` +- Depends on: `yazi-shared::event::Event`, `yazi-macro` +- Used by: `yazi-actor` async tasks, `yazi-scheduler`, `yazi-plugin` + +**Parser Layer:** +- Purpose: Typed `Options` structs for every action; decouples serialization/deserialization from actor logic +- Location: `yazi-parser/src/` +- Contains: Per-overlay modules (`mgr`, `app`, `input`, ...) with structs like `CdOpt`, `OpenOpt`, `SortOpt` +- Depends on: `yazi-shared` +- Used by: `yazi-actor` implementations + +**Renderer (`Root`):** +- Purpose: Compose the ratatui `Widget` tree; delegate to Lua for the base UI layout; render overlays in z-order +- Location: `yazi-fm/src/root.rs` +- Contains: `Root::render()` calls Lua `Root:new(area):redraw()`, then renders `Preview`, `Modal`, and all visible overlays +- Depends on: `yazi-binding`, `yazi-plugin::LUA`, `yazi-core::Core` +- Used by: `App::render()` + +**Lua Plugin System:** +- Purpose: Provide a Lua 5.5 scripting environment; expose Rust state as read-only `UserData` via `Lives` scoped bindings +- Location: `yazi-plugin/src/`, `yazi-binding/src/`, `yazi-actor/src/lives/` +- Contains: `Lives::scope()` pins `Core` as a Lua global `cx`; `yazi-binding` exposes element types (Rect, Style, etc.) and state accessors +- Depends on: `mlua`, `yazi-core`, `yazi-config` +- Used by: `Root::render()`, `App::render()`, `AcceptPayload` actor + +**Scheduler:** +- Purpose: Execute file operations, preload/fetch tasks, plugin workers, and external processes with configurable concurrency and priority queues +- Location: `yazi-scheduler/src/` +- Contains: `fetch/`, `file/`, `plugin/`, `preload/`, `process/`, `size/` task types; a `Scheduler` runner +- Depends on: `yazi-proxy`, `yazi-fs`, `yazi-plugin` +- Used by: `yazi-core::Tasks` + +**DDS (Data Distribution Service):** +- Purpose: IPC broker — Unix domain socket server that routes typed `Payload` messages between yazi instances and the `ya` CLI +- Location: `yazi-dds/src/` +- Contains: `Server` (accepts connections, routes by receiver ID and `ability`), `Client` (connects, sends/receives), `Pubsub` (local/remote event subscriptions), `Payload`/`Ember` types +- Depends on: `tokio`, `yazi-shared`, `yazi-boot` +- Used by: `main()` (calls `yazi_dds::serve()`), `yazi-cli` for `emit`/`pub`/`sub` commands + +**VFS / SFTP:** +- Purpose: Abstract filesystem access; local filesystem ops go through `yazi-fs`; remote (SFTP) access goes through `yazi-sftp` + `yazi-vfs::provider` +- Location: `yazi-vfs/src/`, `yazi-sftp/src/` +- Contains: `VfsFile`, `provider::` with local and SFTP providers; `yazi-sftp` implements the SSH/SFTP protocol via `russh` +- Depends on: `russh`, `yazi-fs`, `tokio` +- Used by: `yazi-actor` cd/download/upload actors, `yazi-core` + +**Image Adapter:** +- Purpose: Detect terminal emulator capabilities and render inline images using the best available protocol +- Location: `yazi-adapter/src/` +- Contains: `Adapter` enum (Kgp, KgpOld, Iip, Sixel, Chafa, Ueberzug); `drivers/` with one file per protocol; `yazi-emulator` handles brand/capability detection +- Depends on: `yazi-emulator`, `ratatui` +- Used by: preview rendering in `yazi-fm` + +## Data Flow + +**Key press to screen update:** + +1. `crossterm` event arrives on `Signals` task → `Event::Key` emitted to shared MPSC channel +2. `App::serve()` drains up to 50 events per batch +3. `Dispatcher::dispatch_key()` → `Router::route()` looks up keymap for current `Layer` → emits `Event::Seq` or `Event::Call` +4. `Dispatcher::dispatch_call()` → `Executor::execute()` matches layer + action name → calls `act!(layer:name, cx, action)` +5. `Actor::act()` mutates `Core` fields, optionally spawns async tasks via `tokio::spawn` with result posted back through `Proxy::*` → `emit!(Call(...))` +6. After each event, `NEED_RENDER` atomic is checked; if set and ≥10ms since last frame, `App::render()` is called +7. `Root::render()` calls Lua `cx` (populated via `Lives::scope`), then renders Rust overlay widgets into ratatui `Buffer` +8. `Term::draw()` diffs buffer and writes escape sequences via `crossterm` + `TTY` + +**DDS message from `ya emit`:** + +1. `ya` CLI connects to Unix socket, sends `hi` then payload then `bye` +2. `yazi-dds::Server` routes to connected yazi instance(s) by receiver ID and ability set +3. Receiving yazi instance's `Client` loop receives line, parses `Payload`, calls `payload.emit()` → `Event::Call(accept_payload)` +4. `AcceptPayload::act()` looks up registered Lua handlers in `LOCAL`/`REMOTE` pubsub tables, calls each via `LUA` + +**State management:** + +- `Core` is owned by `App` and mutated synchronously on the Tokio local set thread +- Background tasks (async Tokio tasks) never hold `&mut Core`; they post results back via `Proxy` functions that emit `Event::Call` +- Lua scripts access a read-only snapshot of state through `Lives`-scoped `UserData` bound to the current `Core` reference + +## Key Abstractions + +**`Actor` trait:** +- Purpose: Uniform interface for every command in the system +- Examples: `yazi-actor/src/mgr/cd.rs`, `yazi-actor/src/mgr/open.rs`, `yazi-actor/src/app/bootstrap.rs` +- Pattern: Zero-size structs; `type Options` parsed from `ActionCow`; `fn act(cx, opt) -> Result`; optional `fn hook()` for post-action DDS sparks + +**`Event` enum:** +- Purpose: Central message bus shared between terminal input, action dispatch, and render scheduling +- Location: `yazi-shared/src/event/event.rs` +- Variants: `Call(ActionCow)`, `Seq(Vec)`, `Render(bool)`, `Key`, `Mouse`, `Resize`, `Focus`, `Paste` + +**`Layer` enum:** +- Purpose: Tracks which UI overlay is topmost, determining keymap lookup and executor routing +- Location: `yazi-shared/src/layer.rs` +- Values: `Mgr`, `Tasks`, `Spot`, `Pick`, `Input`, `Confirm`, `Help`, `Cmp`, `Which`, `App`, `Notify` + +**`Ctx` struct:** +- Purpose: Short-lived mutable borrow of `Core` enriched with active tab index and action source; passed to every `Actor::act()` +- Location: `yazi-actor/src/context.rs` +- Pattern: Created by `Ctx::new(&action, core, term)` at start of `Executor` method; `Deref` into `Core` for ergonomic access + +**`RoCell` / `SyncCell`:** +- Purpose: Thread-safe write-once containers used as module-level globals (replacing `lazy_static` / `OnceLock`) +- Location: `yazi-shared/src/ro_cell.rs`, `yazi-shared/src/sync_cell.rs` +- Pattern: `pub static FOO: RoCell = RoCell::new(); ... FOO.init(value);` + +## Entry Points + +**`yazi-fm` binary:** +- Location: `yazi-fm/src/main.rs` +- Triggers: User runs `yazi` in terminal +- Responsibilities: Calls `init()` on every crate in dependency order; starts DDS `serve()`; runs `App::serve()` on Tokio local set + +**`yazi-cli` binary (`ya`):** +- Location: `yazi-cli/src/main.rs` +- Triggers: User runs `ya emit|pub|sub|pkg` +- Responsibilities: Minimal init (shared + fs only); connects to running yazi DDS server for IPC commands; manages plugin packages + +## Error Handling + +**Strategy:** `anyhow::Result` propagated through the actor chain; dispatcher logs errors at `warn` level and continues the loop — failures do not crash the app + +**Patterns:** +- `act!` macro returns `Result`; callers can use `?` to propagate or `.ok()` to silently ignore +- `succ!()` macro returns `Ok(Data::default())` — used to terminate actors that have nothing to return +- `err!(expr)` macro logs expression result as a tracing `error` but does not propagate +- `render!()` macro emits a `Render` event from within `succ!()` when state changed + +## Cross-Cutting Concerns + +**Logging:** `tracing` crate; level controlled via `YAZI_LOG` env var; initialized in `yazi-fm/src/logs.rs` + +**Validation:** Config validation happens at `yazi-config::init()` — parse errors show a blocking prompt then fall back to preset defaults + +**Authentication:** N/A for local mode; SFTP uses `russh` with key-based or password auth negotiated in `yazi-sftp/src/session.rs` + +**Concurrency model:** Single Tokio `LocalSet` thread owns `Core` and runs the event loop; all background I/O is spawned as regular `tokio::spawn` tasks and communicate back via `Event` channel + +--- + +*Architecture analysis: 2026-03-21* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 00000000..8285deb3 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,263 @@ +# Codebase Concerns + +**Analysis Date:** 2026-03-21 + +--- + +## Tech Debt + +**SFTP URL rebase not implemented:** +- Issue: `UrlBuf::rebase()` panics at runtime for SFTP URLs via `todo!()` macro — hardest-blocking panic +- Files: `yazi-shared/src/url/buf.rs:166` +- Impact: Any code path that calls `rebase()` on an SFTP URL will unconditionally panic at runtime +- Fix approach: Implement the commented-out `Self::Sftp { loc: loc.rebase(base), domain: domain.clone() }` arm + +**`RwFile::metadata()` uses placeholder string for path:** +- Issue: Both Tokio and SFTP arms pass the literal `"// FIXME"` string as the file path +- Files: `yazi-vfs/src/provider/rw_file.rs:23-24` +- Impact: Any metadata query on an open file returns incorrect path information; affects file attributes display +- Fix approach: Capture and store the path when the file is opened; pass it through to `Cha::new()` + +**Splatter legacy `%*` shorthand pending removal:** +- Issue: Three `// TODO: remove` comments mark the `%*` variant (maps to `visit_selected`) as deprecated; dead code kept for backward compat +- Files: `yazi-fs/src/splatter.rs:85,158,247,346` +- Impact: Bloated match arms, confusing maintenance; breaking change deferred +- Fix approach: Remove `%*` handling after a deprecation grace period; update user-facing docs + +**`open_shell_compat` compatibility shim:** +- Issue: Entire `OpenShellCompat` actor, `TasksProxy::open_shell_compat()`, and `match_and_open()` helper all carry `// TODO: remove` comments +- Files: `yazi-actor/src/tasks/open_shell_compat.rs`, `yazi-proxy/src/tasks.rs:10`, `yazi-actor/src/mgr/open_do.rs:61` +- Impact: Multiple call sites continue routing through legacy path instead of new `process_open` +- Fix approach: Migrate all callers to use `TasksProxy::process_exec()` directly; delete the shim actor + +**`ProcessOpenOpt.spread` field marked for removal:** +- Issue: `spread: bool` field carries `// TODO: remove` annotation in the parser type +- Files: `yazi-parser/src/tasks/process_open.rs:17` +- Impact: Dead struct field increasing complexity of every `ProcessOpenOpt` construction +- Fix approach: Remove field; propagate removal to all construction sites in `mgr/shell.rs`, `mgr/open_do.rs`, `tasks/process_open.rs` + +**`ChaMode::is_exec()` deprecated but still in active use:** +- Issue: Method carries `// TODO: deprecate` comment yet is called from `yazi-binding`, `yazi-config` theme code +- Files: `yazi-fs/src/cha/mode.rs:153`, `yazi-binding/src/cha.rs:64`, `yazi-config/src/theme/is.rs:33`, `yazi-config/src/theme/icon.rs:66` +- Impact: Deprecated API surface remains public and actively maintained +- Fix approach: Determine replacement API, update callers, then remove method + +**`vfs::unique_name()` deprecated but infrastructure retained:** +- Issue: `yazi_vfs::unique_name()` is deprecated in favour of `fs.unique()`; Lua wrapper still exposes `fs.unique_name()` with a deprecation warning at runtime +- Files: `yazi-vfs/src/fns.rs:16`, `yazi-plugin/src/fs/fs.rs:234-241` +- Impact: Duplicate code path; external plugins still using the old API receive runtime warnings +- Fix approach: Track down plugin usages via community; remove API after sufficient migration time + +**`Data::into_any2()` has no satisfactory name:** +- Issue: Method acknowledged as needing a better name in comment `// FIXME: find a better name` +- Files: `yazi-shared/src/data/data.rs:233` +- Impact: Poor discoverability in the `Data` API surface +- Fix approach: Audit all callsites and rename; update callers + +**Multiple FIXME-annotated compat `impl AsRef` / `impl Default` on URL types:** +- Issue: `LocBuf`, `UrlCow`, `UrlBuf`, `LocBuf` all carry `// FIXME: remove` impls that exist only for transitional compat +- Files: `yazi-shared/src/loc/buf.rs:23`, `yazi-shared/src/url/cow.rs:21`, `yazi-shared/src/url/buf.rs:16`, `yazi-shared/src/loc/loc.rs:39` +- Impact: These impls can mask type errors; they block further API hardening +- Fix approach: Identify callers relying on these impls; replace with explicit conversions; remove impls + +**Lua `ya.select()` stub not implemented:** +- Issue: `ya.select()` is registered but returns `Ok(())` unconditionally; body is a TODO placeholder +- Files: `yazi-plugin/src/utils/sync.rs:126-128` +- Impact: Any plugin that attempts to call `ya.select()` silently succeeds with no result; misleading API +- Fix approach: Implement select semantics or return an explicit "not implemented" Lua error until complete + +**`spot:copy "line"` unimplemented:** +- Issue: The `"line"` copy type in the spot view has an empty TODO body; no text is copied +- Files: `yazi-actor/src/spot/copy.rs:28` +- Impact: Silent no-op when user copies a line from spot view +- Fix approach: Implement line extraction from the table widget + +**`ya.layer()` cursor and list fields unset:** +- Issue: `cursor: None` and `list: Default::default()` in `ya.input()` / `ya.confirm()` carry `// TODO` comments indicating incomplete options plumbing +- Files: `yazi-plugin/src/utils/layer.rs:57,89` +- Impact: Plugins cannot programmatically position the cursor or pre-populate confirm lists via Lua +- Fix approach: Thread these fields through `InputCfg` / `ConfirmCfg` from the Lua table argument + +**`WATCHER` semaphore acquire unwrap in file watcher:** +- Issue: `WATCHER.acquire().await.unwrap()` can panic if the semaphore is closed during shutdown +- Files: `yazi-watcher/src/local/local.rs:96` +- Impact: Panics possible during clean shutdown sequence +- Fix approach: Use `?` propagation or check for `SemaphoreAcquireError` explicitly + +--- + +## Known Bugs + +**DDS `pub_inner_hi()` error silently swallowed:** +- Symptoms: Peer hello messages fail without any error propagated or logged +- Files: `yazi-dds/src/pubsub.rs:124-125` +- Trigger: IPC transport error during initial hi broadcast +- Workaround: None; failure is invisible + +**Watcher notification batching workaround:** +- Symptoms: File change events are coalesced over 250 ms windows with batch size 1000 as an acknowledged workaround +- Files: `yazi-watcher/src/local/local.rs:89` +- Trigger: Comment says `// TODO: revert this once a new notification is implemented`; current impl is not the intended design +- Workaround: Current behaviour is functional but has fixed latency overhead + +**LINKED returns Path type instead of Url:** +- Symptoms: Symlink-based watch targets use an incorrect type, requiring an extra `Url::regular()` wrapping +- Files: `yazi-watcher/src/reporter.rs:33-35` +- Trigger: Any access to watched symlinked directories +- Workaround: Current adapter in place but acknowledged as wrong + +**`UrlCov::parent()` not used in selection remove path:** +- Symptoms: Parent traversal in `selected.rs` uses a manual workaround instead of the correct `UrlCov::parent()` API +- Files: `yazi-core/src/tab/selected.rs:111` +- Trigger: Removing selected files that have deep parent chains +- Workaround: Manual `while let Some(u) = parent` loop + +--- + +## Security Considerations + +**`unsafe std::env::set_var()` in async context:** +- Risk: `set_var` is unsound in multi-threaded programs; marked `unsafe` in Rust 1.83+. Called from a `spawn_blocking` closure that runs concurrently with other threads reading the environment +- Files: `yazi-fs/src/cwd.rs:101,108`, `yazi-dds/src/lib.rs:25-33` +- Current mitigation: `unsafe` block is present acknowledging the risk; init happens early in `dds::init()` +- Recommendations: Migrate environment variables to explicit `Arc` passing or use `std::env::set_var` only before any threads are spawned + +**`str::from_utf8_unchecked()` without SAFETY comments:** +- Risk: If upstream data is not validated as valid UTF-8 before calling the unchecked variant, memory safety of `str` references is violated +- Files: `yazi-adapter/src/drivers/kgp.rs:360,366`, `yazi-adapter/src/drivers/kgp_old.rs:49,55`, `yazi-plugin/src/external/highlighter.rs:188`, `yazi-shared/src/strand/buf.rs:68,85,90` +- Current mitigation: No `// SAFETY` comment explaining the invariant; workspace lint `missing_safety_doc = "allow"` suppresses the Clippy warning globally (`Cargo.toml:87`) +- Recommendations: Add `// SAFETY:` comments to each site explaining why the bytes are valid UTF-8; re-enable `missing_safety_doc` lint + +**`missing_safety_doc` lint globally suppressed:** +- Risk: All `unsafe fn` blocks in the workspace are exempt from documentation requirements +- Files: `Cargo.toml:87` +- Current mitigation: None +- Recommendations: Remove `missing_safety_doc = "allow"` from workspace lints; fix each site + +--- + +## Performance Bottlenecks + +**`Refresh::trigger_dirs()` acknowledged slow path:** +- Problem: Spawns async tasks to stat and reload every visible folder on every refresh event; acknowledged in comment `// TODO: performance improvement` +- Files: `yazi-actor/src/mgr/refresh.rs:44` +- Cause: No caching layer; unconditional `Files::assert_stale()` and `Files::from_dir_bulk()` calls per folder +- Improvement path: Track change timestamps and skip re-reads when stat hasn't changed; debounce refresh triggers + +**`icon()` method computes icon match on every call without caching:** +- Problem: `yazi-actor/src/lives/file.rs:91` and `yazi-binding/src/file.rs:93` both call `THEME.icon.matches()` per file on every render with `// TODO: use a cache` +- Files: `yazi-actor/src/lives/file.rs:91`, `yazi-binding/src/file.rs:93` +- Cause: No memoization on `File` for the computed icon +- Improvement path: Cache the icon result on the `File` struct after first computation; invalidate on theme reload + +**`Pos::to_rect()` caching absent:** +- Problem: Rect computation from `Pos` has `// TODO: cache` comment indicating repeated recomputation +- Files: `yazi-binding/src/elements/pos.rs:84` +- Cause: No cached field +- Improvement path: Store computed `Rect` alongside `Pos`; invalidate when terminal size changes + +**245 `.clone()` calls across the codebase:** +- Problem: URL types (`UrlBuf`, `UrlCow`) are cloned pervasively; 126 `unwrap()`/`expect()` calls in non-test production code +- Files: spread across all crates +- Cause: URL types carry owned strings with no interning beyond the `Pool` for symbols; clone-heavy IPC message passing +- Improvement path: Audit hot paths using `clone()` on URLs; leverage existing `UrlCow` borrow types where ownership is not needed + +--- + +## Fragile Areas + +**`yazi-fs/src/splatter.rs` — shell argument expansion:** +- Files: `yazi-fs/src/splatter.rs` +- Why fragile: Multiple `unreachable!()` arms in `visit_hovered()`, `visit_dirname()` etc. that panic if the character dispatch logic in `visit()` is ever inconsistent; legacy `%*` arms awaiting removal add more match paths +- Safe modification: Run full `cargo test -p yazi-fs` after any change; ensure all `visit_*` dispatch matches the `visit()` character table exactly +- Test coverage: Inline `#[test]` block exists for basic cases; no exhaustive round-trip tests for all interpolation forms + +**`yazi-shared/src/url/` — URL type hierarchy:** +- Files: `yazi-shared/src/url/url.rs`, `yazi-shared/src/url/buf.rs`, `yazi-shared/src/url/cow.rs` +- Why fragile: Parallel enum hierarchies (`Url`, `UrlBuf`, `UrlCow`) must stay in sync; SFTP arm incomplete in `rebase()`; several compat `impl` blocks marked for removal; any variant addition requires changes in all three types plus `Loc`/`LocBuf` +- Safe modification: Add variants to all three enums simultaneously; verify no `_ =>` arms silently absorb new variants; check all `From`/`Into` impls +- Test coverage: Unit tests exist in `yazi-shared/src/url/buf.rs` and `cow.rs` but do not cover SFTP variant paths + +**`yazi-dds` — inter-process pub/sub:** +- Files: `yazi-dds/src/pubsub.rs`, `yazi-dds/src/lib.rs` +- Why fragile: Uses `unsafe { std::env::set_var }` during init; `pub_inner_hi()` silently ignores errors; global statics (`ID`, `PEERS`, `LOCAL`, `REMOTE`) initialized via `RoCell::init()` which panics on double-init +- Safe modification: Only call `dds::init()` once before spawning any async tasks; never call from tests without isolation +- Test coverage: No tests found in `yazi-dds/` + +**`yazi-adapter` — image protocol drivers:** +- Files: `yazi-adapter/src/drivers/kgp.rs`, `yazi-adapter/src/drivers/kgp_old.rs` +- Why fragile: Two parallel KGP driver implementations exist (current and `_old`); both use `str::from_utf8_unchecked()` without SAFETY docs; base64-encoded image data passed through format strings +- Safe modification: Treat `kgp_old.rs` as read-only reference; do not add features there; changes to base64 chunking must preserve chunk boundary alignment +- Test coverage: No unit tests found; runtime-only validation + +**`yazi-plugin/src/isolate/` — Lua plugin isolation:** +- Files: `yazi-plugin/src/isolate/peek.rs`, `yazi-plugin/src/isolate/preload.rs`, `yazi-plugin/src/isolate/fetch.rs`, `yazi-plugin/src/isolate/spot.rs` +- Why fragile: Uses `Handle::current().block_on()` which blocks the calling OS thread; if called from within an async context on the Tokio thread pool this starves the executor +- Safe modification: Only invoke isolate entry points from dedicated `spawn_blocking` threads, never from `async` tasks directly +- Test coverage: No unit tests; integration-tested only via running yazi + +--- + +## Scaling Limits + +**Lua `ya.select()` stub:** +- Current capacity: Not functional; always returns empty result +- Limit: Any plugin relying on multi-item selection UI is broken +- Scaling path: Implement in `yazi-plugin/src/utils/sync.rs` + +**File watcher batch size hardcoded at 1000:** +- Current capacity: 1000 change events per 250 ms window +- Limit: Repositories or directories with high churn (e.g., build output) may overwhelm the batch and drop events +- Scaling path: Make `chunks_timeout` size/duration configurable; implement proper OS-level coalescing + +--- + +## Dependencies at Risk + +**`ratatui` unstable features:** +- Risk: Uses `unstable-rendered-line-info` and `unstable-widget-ref` feature flags; these APIs can break without a semver guarantee between minor ratatui versions +- Impact: Build breaks or behavioural regressions on ratatui upgrades +- Migration plan: Track ratatui changelog; stabilisation of these features is planned upstream but no ETA + +**`mlua` with Lua 5.5:** +- Risk: `lua55` feature targets Lua 5.5 which is not yet a stable release; API changes between Lua 5.5 preview builds could require code updates +- Impact: `yazi-plugin` and `yazi-binding` crates rely on Lua 5.5 semantics +- Migration plan: Pin to a tested mlua version; test against new Lua 5.5 release candidates in CI + +--- + +## Test Coverage Gaps + +**`yazi-dds` — zero tests:** +- What's not tested: IPC message serialization, pub/sub routing, peer discovery, all `pubsub.rs` paths +- Files: `yazi-dds/src/pubsub.rs`, `yazi-dds/src/client.rs`, `yazi-dds/src/server.rs` +- Risk: Silent regressions in cross-instance communication; `pub_inner_hi()` error swallowing undetected +- Priority: High + +**`yazi-adapter` — zero tests:** +- What's not tested: All image protocol drivers (KGP, KGP-old, Sixel, IIP, Ueberzug), ICC colour transform, image encoding +- Files: `yazi-adapter/src/drivers/`, `yazi-adapter/src/icc.rs` +- Risk: Image rendering regressions not caught until runtime +- Priority: High + +**`yazi-vfs` SFTP provider paths — sparse tests:** +- What's not tested: `RwFile` metadata with SFTP, `try_absolute()` for archive URLs, SFTP-specific `Cha` conversion +- Files: `yazi-vfs/src/provider/rw_file.rs`, `yazi-vfs/src/provider/provider.rs` +- Risk: Archive and SFTP URL handling silently broken; placeholder `"// FIXME"` path in production unnoticed +- Priority: High + +**`yazi-actor` core actions — partial tests:** +- What's not tested: `bulk_rename` cycle detection edge cases, `open_do::match_and_open()` legacy path, `spot::copy "line"` variant +- Files: `yazi-actor/src/mgr/bulk_rename.rs`, `yazi-actor/src/mgr/open_do.rs`, `yazi-actor/src/spot/copy.rs` +- Risk: Silent regressions in file operations; no-op copy undetected +- Priority: Medium + +**Only 23 of 912 Rust source files contain tests (~2.5%):** +- What's not tested: The vast majority of crate logic including scheduler, config parsing, most of the actor layer, watcher, and binding +- Files: All crates except `yazi-shared`, `yazi-fs`, `yazi-core` (partial), `yazi-config` (partial) +- Risk: Wide surface area for undetected regressions; TDD mandate in CLAUDE.md is not yet reflected in coverage +- Priority: High + +--- + +*Concerns audit: 2026-03-21* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 00000000..4e5d5de2 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,175 @@ +# Coding Conventions + +**Analysis Date:** 2026-03-21 + +## Naming Patterns + +**Files:** +- `snake_case.rs` for all Rust source files +- Module directories named after the concept they contain (e.g., `yazi-shared/src/url/`, `yazi-fs/src/path/`) +- Error types live in dedicated `error.rs` or `error/` files per crate +- Config presets in `preset/` subdirectories + +**Structs and Enums:** +- `PascalCase` for all types: `UrlBuf`, `LocBuf`, `Backstack`, `Selected`, `SchemeCow` +- `Buf`-suffixed types are owned buffers; reference types use the bare name (e.g., `Url` vs `UrlBuf`, `Loc` vs `LocBuf`) +- `Cow`-suffixed types are clone-on-write wrappers (e.g., `UrlCow`, `SchemeCow`, `PathCow`) +- Error enums use `Error` suffix with domain prefix: `PathDynError`, `StripPrefixError`, `SetNameError` + +**Functions:** +- `snake_case` for all functions and methods +- Boolean getters often use `is_` prefix: `is_empty()`, `is_dir()`, `is_local()`, `is_regular()` +- Public accessor stubs that return a borrowed view often match the field name: `uri()`, `urn()`, `base()`, `trail()` +- Fallible constructors use `try_` prefix: `try_join()`, `try_set_name()`, `try_strip_prefix()` +- Conversion constructors named by intent: `zeroed()`, `floated()`, `saturated()`, `from_components()` +- Async variants of sync functions stay named the same (not `_async` suffixed) + +**Variables:** +- `snake_case`, single-letter names only for iterators (`i`, `c`, `b`) and closures +- Avoid abbreviations; prefer `url`, `path`, `domain` over `u`, `p`, `d` in function signatures +- Tuple/temporary intermediate values named `a`, `b` when context is obvious + +**Traits:** +- `PascalCase`; `Able` suffix for marker/capability traits: `LocAble`, `LocBufAble`, `Splatable` +- `Like` suffix for view-oriented traits: `UrlLike`, `PathLike`, `SchemeLike`, `StrandLike` +- `As` prefix for conversion traits: `AsUrl`, `AsPath`, `AsScheme`, `AsStrand` + +**Constants and Statics:** +- `UPPER_SNAKE_CASE` for statics: `ADAPTOR`, `WSL`, `WATCHED`, `WATCHER` +- Associated constants inside `impl` use `UPPER_SNAKE_CASE`: `const NAME: &str = "bulk_rename"` + +## Code Style + +**Formatting:** +- Tool: `rustfmt` (nightly toolchain required) +- Config: `rustfmt.toml` at repo root +- Hard tabs for indentation (not spaces) +- Tab width: 2 spaces equivalent +- Edition 2024 style +- Imports grouped: std → external crates → workspace crates (via `group_imports = "StdExternalCrate"`) +- Imports within a group horizontally collapsed: `use std::{borrow::Cow, ffi::OsStr, ...}` +- One-liner functions on a single line when possible (`fn_single_line = true`) +- Struct field alignment enabled up to 99 chars: fields visually aligned with spaces + +**Linting:** +- Tool: Clippy (stable) +- Config: `[workspace.lints.clippy]` in `Cargo.toml` +- `format_push_string = "warn"` – avoid `push_str(&format!(...))`, use `write!` instead +- `implicit_clone = "warn"` – `.to_owned()` or `.clone()` must be explicit +- `use_self = "warn"` – use `Self` instead of the concrete type name inside `impl` blocks +- `if_same_then_else = "allow"` – duplicate branches permitted +- `module_inception = "allow"` – `mod foo { struct Foo }` pattern is allowed + +**Lua:** +- Tool: StyLua +- Config: `stylua.toml` at repo root +- Lua 5.4 syntax +- Indent width: 2 spaces +- `call_parentheses = "NoSingleTable"` – omit parentheses when the only argument is a table +- `sort_requires = true` – `require()` calls are sorted alphabetically + +## Import Organization + +**Rust order (enforced by rustfmt):** +1. `std` / `core` / `alloc` +2. External crates (e.g., `anyhow`, `mlua`, `tokio`) +3. Workspace crates (e.g., `yazi_shared`, `yazi_fs`) +4. Local / crate-internal: `crate::`, `super::`, `self::` + +**Path Aliases:** +- No `use` aliases used in production code; type aliases via `type` declaration where needed +- In tests, `use super::*` is the universal import pattern + +**Module Export Pattern:** +- `yazi_macro::mod_pub!(a b c)` – re-exports submodules as `pub mod` (subdirectories visible) +- `yazi_macro::mod_flat!(a b c)` – makes private `mod` items `pub use`d flat into current namespace +- Avoids manual `mod x; pub use x::*;` repetition throughout all crates + +## Error Handling + +**Boundary strategy:** +- Functions that can fail return `anyhow::Result` at application layer (actors, async tasks) +- Domain-specific errors use `thiserror::Error`-derived enums with precise variants (see `yazi-shared/src/path/error.rs`, `yazi-shared/src/strand/error.rs`) +- Error propagation uses `?` operator; `.context()` added where clarification helps +- `bail!()` from `anyhow` used for early failure with message +- `ensure!()` from `anyhow` used for condition checks + +**Error macro (`err!`):** +- `yazi_macro::err!(expr)` – logs error via `tracing::error!` and discards result; used for fire-and-forget side effects +- `yazi_macro::err!(expr, "fmt {}", args)` – same but with custom message +- Do NOT use `unwrap()` in production code; `expect("message")` is acceptable only when invariant is guaranteed +- `debug_assert!(...)` used inside `impl` blocks to verify post-conditions cheaply + +**Panics:** +- `unreachable!()` used only inside exhaustive match arms that should logically never be hit +- `expect("...")` used in constructors/init code where failure means programmer error + +## Logging + +**Framework:** `tracing` crate + +**Import pattern:** +```rust +use tracing::{debug, error, warn}; +// or targeted: +use tracing::error; +``` + +**Patterns:** +- `tracing::error!(...)` – non-recoverable errors in async tasks +- `tracing::warn!(...)` – degraded state that can continue +- `tracing::debug!(...)` – diagnostic information (only in debug builds via `max_level_debug`) +- `yazi_macro::err!(expr)` – shorthand to log and discard a `Result::Err` +- `yazi_macro::time!(expr)` – wraps an expression in a debug-level timing log +- Log level controlled at runtime via `YAZI_LOG` environment variable + +## Comments + +**When to Comment:** +- Inline comments explain WHY, not WHAT: `// Only keep 30 URLs before the cursor, the cleanup threshold is 60` +- Code sections within large files use `// ---` separator lines: `// --- Tuple`, `// --- Tests` +- Platform-conditional code annotated: `// "/" is both a directory separator and the root directory per se` +- Pending work uses `// TODO:` or `// FIXME:` markers (not `//TODO:`) + +**Doc comments (`///`):** +- Used only on public API items with non-obvious contracts +- CLI arguments all have `///` docs (powers `--help` output via `clap`) +- Internal implementation functions rarely have doc comments + +**Commented-out code:** +- Not present; dead code is removed immediately + +## Function Design + +**Size:** Typically 5–25 lines; business logic delegated to private helpers + +**Parameters:** Max 3 in most cases; larger argument sets use an Options struct (e.g., `BulkRenameOpt`, `ShowOpt`) + +**Return Values:** +- `Option` for absence; `Result` for failure +- `bool` return for mutation success/failure: `add()`, `remove()` on `Selected` +- Avoid `(bool, T)` tuples; use named return types or structs + +**Boolean Parameters:** +- Avoided in public APIs; use enums or named Options structs instead +- Acceptable in private helpers when the call site is immediately adjacent + +## Module Design + +**Exports:** +- All crates have a `pub fn init()` entry point for global state initialization +- Each crate exposes a flat public API via `mod_flat!` – callers do not need to know internal module layout +- Sub-namespaces exposed via `mod_pub!` when logical grouping is useful (e.g., `elements/`, `path/`) + +**Barrel Files:** +- `lib.rs` serves as the crate root; uses macros instead of manual barrel re-exports +- No separate `mod.rs` barrel files – directory modules are declared in `lib.rs` + +**Struct Internals:** +- Private fields by default; getters expose them where needed +- `pub(super)` used for intra-module access (e.g., `pub(super) inner`, `pub(super) area`) +- Newtype pattern common for domain types: `struct Backstack { cursor: usize, stack: Vec }` + +--- + +*Convention analysis: 2026-03-21* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 00000000..a1a80d68 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,208 @@ +# External Integrations + +**Analysis Date:** 2026-03-21 + +## APIs & External Services + +**None (network-based APIs):** +- Yazi is a local terminal file manager. It does not call any external web APIs or cloud services at runtime. +- All "integrations" are protocol-level or subprocess-based. + +## SSH / SFTP + +**SSH/SFTP Protocol:** +- Implementation: `yazi-sftp/` crate +- SDK/Client: `russh` 0.57.1 (pure-Rust SSH2 implementation, ring + rsa features) +- Purpose: Browse and operate on remote filesystems over SFTP +- Session management: `yazi-sftp/src/session.rs` — async session with per-request oneshot callbacks +- Protocol: Custom SFTP packet serialization/deserialization in `yazi-sftp/src/ser.rs`, `de.rs` +- Auth: SSH key-based (RSA supported via `russh`); no env var required, handled by SSH session negotiation +- VFS integration: Remote paths handled by `typed-path` for cross-platform path normalization + +## Image Display Protocols + +**Terminal Image Rendering (`yazi-adapter/`):** + +Yazi probes the terminal emulator and selects the best supported image protocol: + +| Protocol | Driver file | Terminals | +|---|---|---| +| Kitty Graphics Protocol (KGP) | `yazi-adapter/src/drivers/kgp.rs` | Kitty, Ghostty | +| KGP Old (chunked) | `yazi-adapter/src/drivers/kgp_old.rs` | Konsole, Warp | +| iTerm2 Inline Protocol (IIP) | `yazi-adapter/src/drivers/iip.rs` | iTerm2, WezTerm, VSCode, Tabby, Hyper, Mintty, Rio, Bobcat | +| Sixel | `yazi-adapter/src/drivers/sixel.rs` | Foot, Microsoft Terminal, BlackBox, VSCode, Rio, Bobcat | +| Ueberzug++ | `yazi-adapter/src/drivers/ueberzug.rs` | Fallback via external process | +| Chafa | `yazi-adapter/src/drivers/chafa.rs` | Default fallback (text-mode rendering) | + +Detection logic: `yazi-adapter/src/adapters.rs` — maps `yazi_emulator::Brand` enum to adapter list. + +**ICC Color Management:** +- `moxcms` 0.8.1 — reads embedded ICC profiles from images +- `palette` 0.7.6 — color space conversion +- `quantette` 0.5.1 — color quantization for Sixel/Chafa output + +## Terminal Emulator Detection + +**Emulator Database (`yazi-emulator/`):** +- Detects running terminal via escape sequence queries and environment variables +- Recognized brands: Kitty, Konsole, iTerm2, WezTerm, Foot, Ghostty, Microsoft Terminal, Warp, Rio, BlackBox, VSCode, Tabby, Hyper, Mintty, Tmux, VTerm, Apple Terminal, Urxvt, Bobcat +- Light/dark theme detection feeds into `yazi-config` flavor selection +- Tmux multiplexer passthrough handled via `yazi-adapter/src/lib.rs` +- WSL detection via `yazi_shared::in_wsl()` + +## Data Storage + +**Databases:** +- None (no embedded database) + +**File Storage:** +- Local filesystem only, via Rust standard library and `yazi-fs/` crate +- Trash/recycle bin: `trash` 5.2.5 crate (all non-Android platforms) in `yazi-fs/` +- Preview image cache: configurable directory via `preview.cache_dir` in `yazi-config/preset/yazi-default.toml` + +**Caching:** +- In-process LRU cache: `lru` 0.16.3 in `yazi-scheduler/` +- No external cache service + +## Plugin System + +**Lua Plugin System (`yazi-plugin/`):** +- Runtime: Lua 5.5 embedded via `mlua` 0.11.6 (vendored by default) +- Prebuilt assets: `yazi-prebuilt` 0.1.0 crate bundles compiled Lua plugins +- Built-in plugins: `yazi-plugin/preset/plugins/` — archive, code, dds, image, video, pdf, svg, json, font, fzf, zoxide, mime, session, vfs, etc. +- Plugin API exposed as `ya` global in Lua; see `yazi-plugin/preset/ya.lua` +- Async plugin execution via `ya.co()` coroutine API + +## Data Distribution Service (DDS) + +**Inter-process Communication (`yazi-dds/`):** +- Purpose: Communication between multiple yazi instances (e.g., nesting, shell integration) +- Transport: Unix domain socket (or named pipe on Windows), using Tokio async I/O +- Protocol: JSON-serialized messages (`serde_json`) +- Environment variables set/read: `YAZI_ID`, `YAZI_PID`, `YAZI_LEVEL` +- Build embeds git metadata via `vergen-gitcl` + +## Shell Integration + +**CLI Tool (`yazi-cli/` → binary `ya`):** +- Shell completion generated at build time for: bash, zsh, fish, elvish, nushell, fig +- Libraries: `clap_complete` 4.6.0, `clap_complete_nushell` 4.6.0, `clap_complete_fig` 4.5.2 +- `ya pub` / `ya emit` commands for DDS messaging from shell scripts + +## Filesystem Watching + +**File Watcher (`yazi-watcher/`):** +- Library: `notify` 8.2.0 with `macos_fsevent` feature +- macOS: FSEvents API via feature flag +- Linux: inotify (via notify default backend) +- Windows: ReadDirectoryChanges (via notify default backend) + +## External Programs (Subprocess Integrations) + +Yazi shells out to external programs configured in `yazi-config/preset/yazi-default.toml`. These are not hard dependencies but expected to be present in `$PATH`: + +**File Operations:** +- `$EDITOR` / `vi` — text editing +- `code` — VS Code editor (Windows) +- `xdg-open` — open files (Linux) +- `open` — open files (macOS) +- `termux-open` — open files (Android) + +**Media:** +- `mediainfo` — media file metadata display +- `exiftool` — EXIF data display + +**Archives:** +- Handled internally via `ya pub extract` (DDS command) + +**Fuzzy Finder:** +- `fzf` — optional, via `yazi-plugin/preset/plugins/fzf.lua` + +**Directory Jumping:** +- `zoxide` — optional, via `yazi-plugin/preset/plugins/zoxide.lua` + +**Image Fallback:** +- `ueberzug++` — external process fallback for image display +- `chafa` — text-mode image rendering fallback + +**Video Preview:** +- External video thumbnailing via `yazi-plugin/preset/plugins/video.lua` + +**PDF Preview:** +- External PDF rendering via `yazi-plugin/preset/plugins/pdf.lua` + +**SVG Preview:** +- External SVG rendering via `yazi-plugin/preset/plugins/svg.lua` + +**Font Preview:** +- `yazi-plugin/preset/plugins/font.lua` + +## Authentication & Identity + +**Auth Provider:** +- None (no user accounts or auth service) +- SSH auth for SFTP handled by `russh` (key-based) + +## Monitoring & Observability + +**Error Tracking:** +- None (no external error tracking service) + +**Logging:** +- `tracing` 0.1.44 framework with `tracing-subscriber` 0.3.23 +- Log appender: `tracing-appender` 0.2.4 writes to file +- Max log level: `debug` in both dev and release builds (via `max_level_debug` and `release_max_level_debug` features) +- Configurable via `RUST_LOG` environment variable (env-filter feature) + +**Panic Handling:** +- `better-panic` 0.3.0 in `yazi-fm/` for improved panic display + +## CI/CD & Deployment + +**Hosting:** +- GitHub Releases (binary archives per platform/arch) +- Snap Store (Linux, via `snap/`) +- Winget (Windows Package Manager) +- Nix (via `flake.nix` and nixpkgs) + +**CI Pipeline:** +- GitHub Actions (`.github/workflows/`) + - `test.yml` — build + `cargo test --workspace` on Ubuntu, Windows, macOS + - `check.yml` — clippy (stable), rustfmt (nightly), stylua + - `publish.yml` — on release: publish to Winget and Snap Store + - `cachix.yml` — Nix binary cache + - `draft.yml` — release draft automation + - `lock.yml` — dependency lock file updates +- Build cache: `sccache` via `mozilla-actions/sccache-action` + +**Release Distribution:** +- Binary installs via `cargo-binstall` (metadata in `[package.metadata.binstall]`) +- Archive naming: `yazi-{target}.{archive-suffix}` from GitHub Releases + +## Webhooks & Callbacks + +**Incoming:** +- None + +**Outgoing:** +- None + +## Environment Configuration + +**Required for build:** +- Rust stable toolchain (MSRV 1.92.0) +- `MACOSX_DEPLOYMENT_TARGET=10.12` (set automatically via `.cargo/config.toml`) + +**Required for CI secrets:** +- `WINGET_TOKEN` — publishing to Windows Package Manager +- `SNAPCRAFT_TOKEN` — publishing to Snap Store +- `GITHUB_TOKEN` — stylua action authentication + +**Optional runtime:** +- `EDITOR` — preferred text editor (falls back to `vi`) +- `YAZI_CONFIG_HOME` — override config directory +- `RUST_LOG` — log level filter + +--- + +*Integration audit: 2026-03-21* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 00000000..ff415f92 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,146 @@ +# Technology Stack + +**Analysis Date:** 2026-03-21 + +## Languages + +**Primary:** +- Rust 2024 edition - All core crates (`yazi-fm/`, `yazi-cli/`, `yazi-core/`, all `yazi-*` crates) + +**Secondary:** +- Lua 5.5 (embedded via mlua) - Plugin system (`yazi-plugin/preset/plugins/*.lua`, `yazi-plugin/preset/ya.lua`) +- Nix - Development environment and packaging (`flake.nix`, `nix/`) +- TOML - Configuration format (`yazi-config/preset/*.toml`) + +## Runtime + +**Environment:** +- Native binary, no runtime required +- Platform targets: Linux, macOS, Windows, Android (via Termux) +- WSL (Windows Subsystem for Linux) supported with special detection logic + +**Package Manager:** +- Cargo (Rust) +- Lockfile: `Cargo.lock` (present, committed) + +## Frameworks + +**Core:** +- `ratatui` 0.30.0 - TUI rendering framework (used in `yazi-fm/`, `yazi-widgets/`, `yazi-adapter/`) +- `crossterm` 0.29.0 - Cross-platform terminal I/O, with `use-dev-tty` on macOS +- `tokio` 1.50.0 (full features) - Async runtime powering the entire application + +**Lua Embedding:** +- `mlua` 0.11.6 - Lua 5.5 embedding with `async`, `anyhow`, `serde`, `macros` features +- `vendored-lua` feature flag bundles Lua at compile time (default) + +**Serialization:** +- `serde` 1.0.228 with derive - universal serialization +- `serde_json` 1.0.149 - JSON for DDS protocol +- `toml` 1.0.6 - Configuration file parsing +- `serde_with` 3.18.0 - Advanced serde helpers + +**Build/Dev:** +- `rustfmt` nightly - Code formatting (`rustfmt.toml`) +- `clippy` stable - Linting (rules in root `Cargo.toml` `[workspace.lints.clippy]`) +- `stylua` latest - Lua code formatting (`stylua.toml`, Lua 5.4 syntax target) +- `vergen-gitcl` 9.1.0 - Build-time git metadata embedding (in `yazi-dds`, `yazi-cli` build scripts) +- `sccache` - Build caching in CI + +## Key Dependencies + +**Critical:** +- `mlua` 0.11.6 - Lua plugin system; entire extensibility model depends on this +- `ratatui` 0.30.0 - All UI rendering; uses unstable features `unstable-rendered-line-info`, `unstable-widget-ref` +- `tokio` 1.50.0 - All async I/O; scheduler, watcher, DDS, SFTP all depend on it +- `russh` 0.57.1 - SSH/SFTP client (ring + rsa features) used in `yazi-sftp/` + +**Image Rendering:** +- `image` 0.25.10 - Image decoding (avif, bmp, dds, exr, gif, hdr, ico, jpeg, png, pnm, qoi, tga, tiff, webp) in `yazi-adapter/` +- `moxcms` 0.8.1 - ICC color management in `yazi-adapter/` +- `palette` 0.7.6 - Color manipulation in `yazi-adapter/` +- `quantette` 0.5.1 - Color quantization in `yazi-adapter/` + +**Syntax Highlighting:** +- `syntect` 5.3.0 - Syntax highlighting with `plist-load` and `regex-onig` features in `yazi-plugin/` + +**Filesystem:** +- `notify` 8.2.0 - Filesystem watching with `macos_fsevent` feature in `yazi-watcher/` +- `trash` 5.2.5 - Cross-platform trash/recycle bin (all non-Android targets) in `yazi-fs/` +- `typed-path` 0.12.3 - Cross-platform path handling (for SFTP and VFS) + +**Performance:** +- `tikv-jemallocator` 0.6.1 - jemalloc allocator on non-macOS/non-Windows (configured in `.cargo/config.toml`) +- `parking_lot` 0.12.5 - Fast mutexes and RwLocks throughout +- `foldhash` 0.2.0 - Fast hash function +- `hashbrown` 0.16.1 - Fast hash maps +- `lru` 0.16.3 - LRU cache in `yazi-scheduler/` +- `twox-hash` 2.1.2 - xxHash3_128 for content hashing + +**Utility:** +- `anyhow` 1.0.102 - Error handling throughout +- `thiserror` 2.0.18 - Error type derivation +- `clap` 4.6.0 - CLI argument parsing in `yazi-cli/`, `yazi-boot/` +- `clap_complete`, `clap_complete_nushell`, `clap_complete_fig` - Shell completion generation +- `chrono` 0.4.44 - Date/time +- `regex` 1.12.3 - Regular expressions +- `globset` 0.4.18 - Glob pattern matching for file rules +- `percent-encoding` 2.3.2 - URL encoding for VFS paths +- `base64` 0.22.1 - Encoding for image protocols +- `rand` 0.9.2 - Random number generation +- `tracing` 0.1.44 + `tracing-subscriber` 0.3.23 - Structured logging +- `tracing-appender` 0.2.4 - Log file appending +- `async-priority-channel` 0.2.0 - Priority task queue in `yazi-scheduler/` +- `yazi-prebuilt` 0.1.0 - Prebuilt Lua plugin assets bundled into `yazi-plugin/` + +**Platform-specific:** +- `libc` 0.2.183 - Unix FFI (Linux, macOS) +- `uzers` 0.12.2 - Unix user/group info (Linux, macOS) +- `windows-sys` 0.61.2 - Windows API bindings (Windows; JobObjects, Storage, UI Shell) +- `core-foundation-sys` 0.8.7 - macOS CoreFoundation (macOS only) +- `objc2` 0.6.4 - Objective-C bindings (macOS only, `yazi-fs/`) +- `signal-hook-tokio` 0.4.0 - Unix signal handling in `yazi-fm/` + +## Configuration + +**Environment:** +- No `.env` files; configured through TOML files placed in XDG config dirs +- Runtime env vars used: `YAZI_ID`, `YAZI_LEVEL`, `YAZI_PID`, `EDITOR`, `YAZI_CONFIG_HOME` +- Build-time env via `.cargo/config.toml`: `MACOSX_DEPLOYMENT_TARGET=10.12`, jemalloc settings + +**Build:** +- Root `Cargo.toml` - Workspace manifest with centralized dependency versions +- `.cargo/config.toml` - Allocator config, macOS deployment target, aarch64 CPU flags +- `rustfmt.toml` - Formatter config (nightly, hard tabs, 2-space tab width, Unix newlines) +- `stylua.toml` - Lua formatter config (Lua 5.4 syntax, 2-space indent) + +**Configuration files (user-facing):** +- `yazi-config/preset/yazi-default.toml` - Manager, preview, opener, open rules +- `yazi-config/preset/keymap-default.toml` - Keybindings +- `yazi-config/preset/theme-dark.toml`, `theme-light.toml` - Color themes +- `yazi-config/preset/vfs-default.toml` - Virtual filesystem settings + +## Build Profiles + +**dev:** Line-tables debug info only; dependencies built without debug info +**release:** LTO=fat, codegen-units=1, panic=abort, strip=true +**release-windows:** Inherits release but panic=unwind +**dev-opt:** Release settings but 256 codegen-units, incremental=true, LTO off + +## Platform Requirements + +**Development:** +- Rust stable toolchain (minimum MSRV 1.92.0) +- Rust nightly for `rustfmt` formatting only +- `stylua` for Lua formatting +- Nix (optional, via `flake.nix` for reproducible dev shell) + +**Production:** +- Self-contained binary; no runtime dependencies beyond system libc +- Platforms: Linux x86_64/aarch64, macOS x86_64/aarch64 (min 10.12), Windows x86_64/aarch64 +- jemalloc linked statically on Linux; system allocator on macOS/Windows +- Apple M1 optimized build via `-Ctarget-cpu=apple-m1` rustflag + +--- + +*Stack analysis: 2026-03-21* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 00000000..1d496d2b --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,256 @@ +# Codebase Structure + +**Analysis Date:** 2026-03-21 + +## Directory Layout + +``` +yazi/ # Workspace root +├── Cargo.toml # Workspace manifest, centralized deps, clippy rules +├── Cargo.lock # Lockfile (committed) +├── rustfmt.toml # Hard tabs, nightly formatting features +├── stylua.toml # Lua formatter config (for plugin files) +├── .cargo/config.toml # jemalloc global allocator, linker flags +├── assets/ # Icons, screenshots, misc assets +├── scripts/ # Dev utilities (icon validation, form scripts) +├── nix/ # Nix flake derivations +├── snap/ # Snapcraft packaging +├── .github/ # CI workflows, issue templates +├── .planning/ # GSD planning documents (not shipped) +│ +├── yazi-fm/ # BINARY: main file manager (default workspace member) +├── yazi-cli/ # BINARY: `ya` CLI tool (default workspace member) +│ +├── yazi-actor/ # Actor trait + all command implementations +├── yazi-adapter/ # Terminal image protocol adapters (kgp/sixel/iip/chafa/ueberzug) +├── yazi-binding/ # Lua ↔ Rust type bindings (mlua UserData) +├── yazi-boot/ # CLI arg parsing (clap), boot configuration +├── yazi-build/ # Build-time code generation helper binary +├── yazi-codegen/ # Proc-macro support crate +├── yazi-config/ # Config parsing (yazi.toml, keymap.toml, theme.toml) +├── yazi-core/ # Core state structs (Core, Mgr, Tab, Tasks, overlays) +├── yazi-dds/ # IPC pub/sub server (Unix socket, multi-instance) +├── yazi-emulator/ # Terminal emulator detection (brand, dimensions) +├── yazi-ffi/ # macOS FFI bindings (CoreFoundation, IOKit, DiskArbitration) +├── yazi-fs/ # Filesystem abstractions (File, Files, sorting, filtering) +├── yazi-macro/ # Proc-macros (act!, emit!, succ!, mod_pub!, mod_flat!, render!, ...) +├── yazi-packing/ # Plugin packaging utilities +├── yazi-parser/ # Options structs for every action (parsed from ActionCow) +├── yazi-plugin/ # Lua runtime, plugin loader, isolate sandboxing +├── yazi-proxy/ # Fire-and-forget action emitters for async contexts +├── yazi-scheduler/ # Task runner (file ops, preload, fetch, plugin, process workers) +├── yazi-sftp/ # SFTP client (russh-based remote filesystem) +├── yazi-shared/ # Shared primitives (Event, Layer, Id, RoCell, URL types, ...) +├── yazi-shim/ # Patches/shims for crossterm and ratatui +├── yazi-term/ # Ratatui Terminal wrapper with partial-render support +├── yazi-tty/ # Raw TTY handle (separate from ratatui for direct writes) +├── yazi-vfs/ # Virtual filesystem (local + SFTP provider abstraction) +└── yazi-watcher/ # Filesystem watcher (local inotify/FSEvents + remote polling) +``` + +## Directory Purposes + +**`yazi-fm/src/`:** +- Purpose: Binary crate for the main `yazi` process +- Contains: `main.rs`, `App` event loop, `Dispatcher`, `Router`, `Executor`, `Root` renderer, per-overlay UI widgets +- Key files: `yazi-fm/src/main.rs`, `yazi-fm/src/app/app.rs`, `yazi-fm/src/dispatcher.rs`, `yazi-fm/src/executor.rs`, `yazi-fm/src/router.rs`, `yazi-fm/src/root.rs` + +**`yazi-cli/src/`:** +- Purpose: Binary crate for the `ya` companion CLI +- Contains: `main.rs`, `args.rs`, command handlers for `emit`, `emit-to`, `pub`, `pub-to`, `sub`, `pkg` +- Key files: `yazi-cli/src/main.rs`, `yazi-cli/src/package/` + +**`yazi-actor/src/`:** +- Purpose: All command/action implementations; the `Actor` trait definition; `Ctx` context struct; `Lives` Lua scope manager +- Sub-directories: `app/` (15 actors), `mgr/` (~60 actors), `cmp/`, `confirm/`, `help/`, `input/`, `notify/`, `pick/`, `spot/`, `tasks/`, `which/`, `lives/`, `core/` +- Key files: `yazi-actor/src/actor.rs`, `yazi-actor/src/context.rs`, `yazi-actor/src/lives/lives.rs` + +**`yazi-core/src/`:** +- Purpose: Pure state structs; no I/O, no async; mutated only by actors via `Ctx` +- Sub-directories: `mgr/` (Mgr, Tabs, Yanked, Mimetype), `tab/` (Tab, Folder, Preview, Finder, Selected, History, Backstack, Mode), `tasks/`, `cmp/`, `confirm/`, `help/`, `input/`, `notify/`, `pick/`, `spot/`, `which/` +- Key files: `yazi-core/src/core.rs`, `yazi-core/src/mgr/mgr.rs`, `yazi-core/src/tab/tab.rs` + +**`yazi-config/src/`:** +- Purpose: TOML config parsing; exposes global `static` values (`YAZI`, `KEYMAP`, `THEME`, `LAYOUT`) +- Sub-directories: `keymap/`, `mgr/`, `open/`, `opener/`, `plugin/`, `popup/`, `preview/`, `tasks/`, `theme/`, `vfs/`, `which/` +- Key files: `yazi-config/src/lib.rs`, `yazi-config/preset/yazi-default.toml`, `yazi-config/preset/keymap-default.toml` + +**`yazi-config/preset/`:** +- Purpose: Embedded default configurations shipped with the binary +- Contains: `yazi-default.toml`, `keymap-default.toml`, `theme-dark.toml`, `theme-light.toml`, `vfs-default.toml` + +**`yazi-shared/src/`:** +- Purpose: Cross-crate primitives with no domain logic +- Contains: `event/` (Event enum, ActionCow), `url/` (UrlBuf, UrlLike, Scheme), `data/`, `errors/`, `pool/`, `strand/`, and misc types (`Id`, `Layer`, `Source`, `RoCell`, `SyncCell`, `Debounce`) +- Key files: `yazi-shared/src/event/event.rs`, `yazi-shared/src/layer.rs` + +**`yazi-dds/src/`:** +- Purpose: IPC broker for multi-instance communication +- Contains: `client.rs`, `server.rs`, `pubsub.rs`, `payload.rs`, `stream.rs`, `ember/` (message frame types: Hi, Hey, Bye, payload body) +- Key files: `yazi-dds/src/client.rs`, `yazi-dds/src/server.rs`, `yazi-dds/src/lib.rs` + +**`yazi-plugin/src/`:** +- Purpose: Lua 5.5 scripting engine initialization, plugin loader, coroutine/isolate sandboxing +- Sub-directories: `runtime/` (Lua globals composer), `loader/` (plugin discovery), `isolate/` (sandboxed execution), `pubsub/`, `fs/`, `external/`, `process/`, `theme/`, `utils/` +- Key files: `yazi-plugin/src/lua.rs`, `yazi-plugin/src/runtime/runtime.rs` + +**`yazi-binding/src/`:** +- Purpose: Lua UserData implementations for Rust types (Url, Rect, Style, Color, File, Id, Image, etc.) +- Contains: `elements/` (ratatui widget helpers), and flat modules for each bound type +- Key files: `yazi-binding/src/elements/`, `yazi-binding/src/file.rs`, `yazi-binding/src/url.rs` + +**`yazi-scheduler/src/`:** +- Purpose: Async task runner with priority queues +- Sub-directories: `fetch/`, `file/`, `hook/`, `plugin/`, `preload/`, `process/`, `size/` +- Key files: `yazi-scheduler/src/scheduler.rs`, `yazi-scheduler/src/runner.rs` + +**`yazi-fs/src/`:** +- Purpose: Local filesystem abstractions: `File`, `Files` collection, URL/path helpers, sorting, filtering, XDG paths +- Contains: `file.rs`, `files.rs`, `cha.rs` (file characteristics), `sorter.rs`, `filter.rs`, `mounts.rs`, `provider/` +- Key files: `yazi-fs/src/file.rs`, `yazi-fs/src/files.rs` + +**`yazi-vfs/src/`:** +- Purpose: Unified virtual filesystem provider interface; delegates to local or SFTP backend +- Contains: `provider/` with `local/`, `sftp/`; `cha.rs`, `file.rs`, `files.rs`, `op.rs` +- Key files: `yazi-vfs/src/provider/providers.rs`, `yazi-vfs/src/provider/provider.rs` + +**`yazi-adapter/src/`:** +- Purpose: Image rendering to terminal; auto-selects protocol per emulator brand +- Contains: `adapter.rs`, `adapters.rs`, `drivers/` (chafa, iip, kgp, kgp_old, sixel, ueberzug), `image.rs`, `info.rs` +- Key files: `yazi-adapter/src/adapter.rs`, `yazi-adapter/src/adapters.rs` + +**`yazi-macro/src/`:** +- Purpose: Proc-macros used throughout the workspace +- Contains: `actor.rs` (`act!`), `event.rs` (`emit!`, `relay!`), `render.rs` (`render!`), `module.rs` (`mod_pub!`, `mod_flat!`), `context.rs`, `fs.rs`, `fmt.rs`, `log.rs`, `stdio.rs`, `platform.rs` + +**`yazi-proxy/src/`:** +- Purpose: Thin wrappers allowing async tasks to emit actions without holding `Core` +- Contains: one file per layer: `app.rs`, `mgr.rs`, `input.rs`, `cmp.rs`, `tasks.rs`, `notify.rs`, `pick.rs`, `which.rs`, `confirm.rs` + +**`yazi-parser/src/`:** +- Purpose: Typed `Options` structs (`CdOpt`, `OpenOpt`, `SortOpt`, ...) deserialized from `ActionCow` parameters +- Sub-directories: one per layer matching `yazi-actor` structure + +**`yazi-watcher/src/`:** +- Purpose: Watch local directory changes (platform FS events) and poll remote paths +- Sub-directories: `local/` (OS watcher backend), `remote/` (polling for VFS paths) + +## Key File Locations + +**Entry Points:** +- `yazi-fm/src/main.rs`: `yazi` binary entry; subsystem initialization order +- `yazi-cli/src/main.rs`: `ya` CLI entry + +**Event Bus:** +- `yazi-shared/src/event/event.rs`: `Event` enum definition and global channel + +**Core State:** +- `yazi-core/src/core.rs`: `Core` struct aggregating all UI component states +- `yazi-core/src/mgr/mgr.rs`: File manager state (tabs, yanked, watcher) +- `yazi-core/src/tab/tab.rs`: Per-tab state (current folder, history, selection, preview) + +**Action Dispatch:** +- `yazi-fm/src/executor.rs`: Routes `ActionCow` by layer to `act!` macros +- `yazi-fm/src/router.rs`: Translates key events to action sequences via `KEYMAP` +- `yazi-actor/src/actor.rs`: `Actor` trait definition + +**Actor Context:** +- `yazi-actor/src/context.rs`: `Ctx` struct — short-lived mutable `Core` borrow passed to actors + +**Rendering:** +- `yazi-fm/src/root.rs`: `Root` widget — Lua-driven base + Rust overlay z-stack +- `yazi-fm/src/app/render.rs`: `App::render()` and `App::render_partially()` + +**Configuration:** +- `yazi-config/src/lib.rs`: Global statics `YAZI`, `KEYMAP`, `THEME`, `LAYOUT` +- `yazi-config/preset/`: Embedded default TOML files + +**IPC:** +- `yazi-dds/src/client.rs`: Client connect/reconnect loop and `shot()`/`draw()` for `ya` +- `yazi-dds/src/server.rs`: Unix socket server routing messages by ability + +**Lua Integration:** +- `yazi-actor/src/lives/lives.rs`: `Lives::scope()` — pins Rust state as Lua globals +- `yazi-plugin/src/runtime/runtime.rs`: `cx` global composer (args, mgr, preview, tasks) + +**Testing:** +- Tests are co-located within each crate under `src/` using `#[cfg(test)]` modules + +## Naming Conventions + +**Files:** +- `snake_case.rs` for all Rust source files +- `mod.rs` for module root files within subdirectories +- `lib.rs` for crate root files + +**Directories:** +- `snake_case` for all source subdirectories +- Directory name matches the module name it exposes (e.g., `yazi-actor/src/mgr/` → `pub mod mgr`) + +**Crates:** +- All crates prefixed with `yazi-` (hyphenated); Rust module names use `yazi_` (underscored) + +**Types:** +- `PascalCase` for structs, enums, traits +- `SCREAMING_SNAKE_CASE` for module-level statics (`YAZI`, `KEYMAP`, `TX`, `RX`) +- `snake_case` for functions and variables + +**Macros:** +- `snake_case!` for all proc-macros (`act!`, `emit!`, `succ!`, `render!`, `mod_pub!`) + +## Where to Add New Code + +**New actor (user command):** +- Implementation: `yazi-actor/src/{layer}/{command_name}.rs` implementing `Actor` trait +- Options struct: `yazi-parser/src/{layer}/{command_name}.rs` or add to existing file +- Registration: Add to `yazi-fm/src/executor.rs` in the relevant layer method using `on!(command_name)` +- Proxy helper (if needed from async): Add method to `yazi-proxy/src/{layer}.rs` + +**New UI overlay:** +- State struct: `yazi-core/src/{overlay_name}/` with `mod.rs` +- Add field to `Core` in `yazi-core/src/core.rs` +- Actor implementations: `yazi-actor/src/{overlay_name}/` +- Renderer widget: `yazi-fm/src/{overlay_name}/` implementing `ratatui::Widget` +- Add `Layer` variant to `yazi-shared/src/layer.rs` +- Add executor routing in `yazi-fm/src/executor.rs` +- Register in `Core::layer()` priority chain in `yazi-core/src/core.rs` + +**New configuration section:** +- Config struct: `yazi-config/src/{section}/` +- Add field to the relevant config aggregate struct +- Add default values to `yazi-config/preset/yazi-default.toml` + +**New Lua binding (expose Rust type to plugins):** +- UserData impl: `yazi-binding/src/{type_name}.rs` +- Add to `yazi-actor/src/lives/` if it needs to be accessible via the `cx` global + +**Shared utilities:** +- Cross-crate primitives: `yazi-shared/src/` +- Filesystem helpers: `yazi-fs/src/` +- New proc-macro: `yazi-macro/src/` + +## Special Directories + +**`yazi-config/preset/`:** +- Purpose: Default TOML configurations embedded into the binary at build time +- Generated: No (hand-authored) +- Committed: Yes + +**`.planning/`:** +- Purpose: GSD planning documents and codebase analysis (not shipped) +- Generated: By GSD tooling +- Committed: No (gitignored or team-local) + +**`target/`:** +- Purpose: Cargo build artifacts +- Generated: Yes +- Committed: No + +**`assets/`:** +- Purpose: Non-code resources (icon files, screenshots) +- Generated: No +- Committed: Yes + +--- + +*Structure analysis: 2026-03-21* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 00000000..6829e349 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,251 @@ +# Testing Patterns + +**Analysis Date:** 2026-03-21 + +## Test Framework + +**Runner:** +- Rust's built-in `cargo test` +- No external test runner; no `jest.config` or `vitest.config` + +**Assertion Library:** +- `assert_eq!`, `assert_ne!`, `assert!` from Rust standard library +- `anyhow::Result` used as test return type for `?` propagation + +**Run Commands:** +```bash +cargo test --workspace --verbose # Run all tests across all crates +cargo test -p yazi-core # Test a specific crate +cargo test test_backstack # Run a single test by name +cargo test --workspace # Full suite (CI equivalent) +``` + +## Test File Organization + +**Location:** +- Co-located: tests live in a `#[cfg(test)] mod tests { ... }` block at the bottom of the source file they test +- No separate `tests/` integration test directories found +- No external test fixtures directory + +**Naming:** +- Test module: always named `mod tests` +- Test functions: `test_` prefix followed by the function or concept being tested + - `fn test_backstack()`, `fn test_natsort()`, `fn test_clean_url()`, `fn test_split()` + - Behavior-specific names also used without `test_` prefix: `fn insert_many_success()`, `fn test_insert_conflicting_parent()` + +**Structure:** +``` +src/ +├── some_module.rs # Implementation + inline test module +│ └── #[cfg(test)] mod tests { ... } +├── path/ +│ ├── clean.rs # Tests at bottom of file +│ └── relative.rs # Tests at bottom of file +``` + +## Test Structure + +**Suite Organization:** +```rust +#[cfg(test)] +mod tests { + use super::*; // Always import everything from parent + + #[test] + fn test_something() { + // arrange + let mut subject = Subject::default(); + // act + assert + assert_eq!(subject.method(), expected); + } + + #[test] + fn test_something_else() -> anyhow::Result<()> { + // Use Result return type for tests needing ? propagation + let u: UrlBuf = "/some/path".parse()?; + assert_eq!(format!("{u:?}"), "/some/path"); + Ok(()) + } +} +``` + +**Section Separator:** +```rust +// --- Tests +#[cfg(test)] +mod tests { +``` +The `// --- Tests` separator comment appears above the test module in some files. + +**Patterns:** +- Setup: `Subject::default()` for zero-state; `yazi_shared::init_tests()` for global state +- No `before_each` / `after_each`; each test is fully self-contained +- Data tables (slices of tuples) used for parameterized assertions +- Helper functions within the `mod tests` block used to reduce assertion boilerplate + +## Mocking + +**Framework:** None – no mocking crate used + +**Patterns:** +- No mock objects; tests call real implementations directly +- Platform-specific tests gated with `#[cfg(unix)]` / `#[cfg(windows)]` on individual test functions +- Global state initialized with `yazi_shared::init_tests()` where needed (see below) +- Tests that require filesystem state initialize `yazi_fs::init()` directly + +**What to Mock:** +- Nothing – the codebase uses no mocking framework + +**What NOT to Mock:** +- Filesystem, URLs, sorting – all tested with real values + +## Fixtures and Factories + +**Test Data:** +The dominant pattern is inline slice literals of tuples serving as parameterized test cases: + +```rust +let cases = [ + // Comment describing the category + ("/a/b", "/a/b/c", "c"), + ("/a/b/c", "/a/b", ".."), + ("/a/b/d", "/a/b/c", "../c"), +]; +for (from, to, expected) in cases { + let result = function_under_test(from, to); + assert_eq!(result, expected); +} +``` + +**Helper closures and functions inside test modules:** +```rust +fn cmp(left: &[&str]) { + let mut right = left.to_vec(); + right.sort_by(|a, b| natsort(a.as_bytes(), b.as_bytes(), true)); + assert_eq!(left, right); +} + +fn matches(glob: &str, url: &str) -> bool { + Pattern::from_str(glob).unwrap().match_url(UrlCow::try_from(url).unwrap(), false) +} +``` + +**Global state initialization:** +```rust +#[test] +fn test_something() { + yazi_shared::init_tests(); // Must call before any URL/path parsing + yazi_fs::init(); // Must call before CWD-dependent tests + // ... +} +``` +`init_tests()` is idempotent via `OnceLock` so ordering between tests doesn't matter. + +**Location:** +- No separate fixtures directory; all test data is inlined in the test block + +## Coverage + +**Requirements:** Not enforced – no minimum coverage threshold configured + +**View Coverage:** +```bash +# No coverage tooling configured in the repository. +# Use cargo-llvm-cov or cargo-tarpaulin manually if needed: +cargo llvm-cov --workspace +``` + +## Test Types + +**Unit Tests:** +- Dominant form: all tests are unit tests co-located with the code they test +- Scope: individual functions and struct methods +- Approach: pure input/output; no I/O or side effects except where `init_tests()` sets up global state + +**Integration Tests:** +- Not present as a separate category; complex behavior tested via unit tests against real types + +**E2E Tests:** +- Not present + +**Property-based Tests:** +- Not present (no `proptest` or `quickcheck` dependencies) + +## Common Patterns + +**Table-driven tests (most common):** +```rust +let cases: &[(&str, &str)] = &[ + ("input_a", "expected_a"), + ("input_b", "expected_b"), +]; +for &(input, expected) in cases { + assert_eq!(function(input), expected, "input: {:?}", input); +} +``` + +**Fallible tests with `anyhow::Result`:** +```rust +#[test] +fn test_join() -> anyhow::Result<()> { + crate::init_tests(); + let base: UrlBuf = "/a".parse()?; + assert_eq!(format!("{:?}", base.try_join("b/c")?), "/a/b/c"); + Ok(()) +} +``` + +**Platform-conditional tests:** +```rust +#[cfg(unix)] +#[test] +fn test_split() { + yazi_shared::init_tests(); + yazi_fs::init(); + // unix-specific assertions +} + +#[cfg(windows)] +#[test] +fn test_split() { + yazi_fs::init(); + // windows-specific assertions +} +``` + +**Struct state verification tests:** +```rust +#[test] +fn test_remove() { + let mut s = Selected::default(); + assert!(s.add(Path::new("/a/b"))); + assert!(!s.remove(Path::new("/a/c"))); // non-existent + assert!(s.remove(Path::new("/a/b"))); // exists + assert!(s.inner.is_empty()); + assert!(s.parents.is_empty()); +} +``` +Note: test code accesses private fields (`s.inner`, `s.stack`, `s.cursor`) directly because tests are in the same file. + +**Error testing:** +```rust +// Testing absence (None/false) explicitly +assert_eq!(bs.shift_backward(), None); +assert!(!s.add(Path::new("/a/b"))); // returns false when conflict + +// Testing error propagation +let result = risky_function(); +assert!(result.is_err()); +// OR via ? in Result-returning tests +``` + +## CI Integration + +**Matrix:** Ubuntu, Windows, macOS (all via `cargo test --workspace --verbose`) +**Trigger:** push/PR to `main` branch +**Lint checks run separately:** Clippy (`cargo clippy --all`), rustfmt (`rustfmt +nightly --check **/*.rs`), StyLua +**Caching:** sccache via `mozilla-actions/sccache-action` + +--- + +*Testing analysis: 2026-03-21*