docs: bootstrap vault, roadmap and research

This commit is contained in:
2026-10-17 21:14:19 +03:00
parent 749afadcd4
commit 8fa0632d69
34 changed files with 1508 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# === Obsidian app state (notes themselves ARE tracked in git) ===
.obsidian/
.trash/
# === Rust / Cargo ===
/target/
debug/
**/*.rs.bk
*.pdb
# === Secrets & tokens (never commit) ===
.env
.env.*
*.token
*_token.txt
# === OS / editor noise ===
.DS_Store
Thumbs.db
*.swp
*.swo
*~
+31
View File
@@ -0,0 +1,31 @@
# viexp
A fast, modular terminal file manager for Linux, written in Rust — a spiritual successor to
[ranger](https://github.com/ranger/ranger) that fixes its weak spots: slow startup, fragile
image previews, and a hard-to-extend Python core.
**Status:** planning / learning project (see the [roadmap](docs/roadmap.md)).
## Goals
- Blazing-fast navigation and previews (kitty graphics protocol first, SIXEL / ueberzug++ / chafa fallbacks).
- Out-of-the-box support for images, video, audio, PDF, DJVU, EPUB and Markdown — with every
file-type module easy to **disable or replace** at runtime, no recompilation required.
- Extension model that feels native to Linux users: drop-in scripts in any language first,
embedded Lua scripting later.
- Vim-style ergonomics: modal keys, tabs, marks, bulk rename, `:` command line.
## This repo is also a learning lab
viexp is deliberately built as a guided learning project: Rust from scratch and technical
English, one 20-hour sprint at a time (Josh Kaufman's "First 20 Hours" method).
All documentation lives in English inside the [Obsidian vault](docs/index.md) in this repo.
- [Knowledge index (start here)](docs/index.md)
- [Roadmap: 20-hour sprints](docs/roadmap.md)
- [Research notes](docs/research/)
- [Architecture decisions (ADRs)](docs/decisions/)
## License
TBD (decided before the first public release).
+27
View File
@@ -0,0 +1,27 @@
---
type: article
status: draft
created: {{date}}
updated: {{date}}
tags: [article]
sources: []
---
# {{title}}
> **TL;DR** — one or two sentences: what the reader should learn from this article.
## Introduction
Why this article exists and who it is for.
## Main part
### Section
## Conclusions
## Sources
-
---
*This article doubles as English writing practice: prefer short, simple sentences; add new vocabulary to the glossary.*
+28
View File
@@ -0,0 +1,28 @@
---
type: decision
adr:
status: proposed
date: {{date}}
supersedes:
tags: [adr]
---
# {{title}}
## Context
The problem or force that makes this decision necessary.
## Options considered
| Option | Pros | Cons |
|--------|------|------|
| | | |
## Decision
What we chose and why — one paragraph, no hedging.
## Consequences
- Positive:
- Negative / risks:
- Mitigations:
<!-- status: proposed → accepted → superseded by a newer ADR (set `supersedes` on the new one) -->
+25
View File
@@ -0,0 +1,25 @@
---
type: devlog
date: {{date}}
sprint:
minutes:
tags: [devlog]
---
# Dev log — {{date}}
## What I did
-
## Code review notes
Reviewer feedback and what I changed because of it.
## Rust notes
New syntax, borrow-checker battles (→ ErrorJournal), aha-moments.
## English practice
New words and phrases I used today:
- *word / phrase* — перевод, my own example sentence
## Next session
-
+21
View File
@@ -0,0 +1,21 @@
---
type: error
created: {{date}}
tags: [error-journal, rust]
---
# {{title}}
## The error
```text
paste the exact compiler / runtime error here
```
## What I expected
## What actually caused it
## The fix
## Self-correction cue
One-line rule to recognize and fix this error class next time on my own.
+11
View File
@@ -0,0 +1,11 @@
---
type: glossary
created: {{date}}
tags: [glossary, english]
---
# {{title}}
| Term / phrase | Translation | Where I met it | My example sentence |
| ------------- | ----------- | -------------- | ------------------- |
| | | | |
+29
View File
@@ -0,0 +1,29 @@
---
type: research
status: inbox
confidence: low
created: {{date}}
question:
tags: [research]
---
# {{title}}
## Research question
What exactly do we need to learn, and why does it matter for viexp?
## Sources
| # | Source | Kind | Freshness | Key takeaway |
|---|--------|------|-----------|--------------|
| 1 | | docs / code / issue / reddit / blog | | |
## Findings
-
## Open questions
- [ ]
## Outcome
Which decisions, tasks or articles came out of this research (link them).
<!-- status: inbox → in-progress → analyzed → archived · confidence: low → medium → high -->
+43
View File
@@ -0,0 +1,43 @@
---
type: sprint
number:
status: planned
started:
finished:
hours-planned: 20
hours-logged: 0
tags: [sprint]
---
# Sprint {{title}}
## Theme
One line: the single sub-skill + app feature this sprint is about.
## App deliverable
The concrete, demoable thing that must exist at the end of the sprint.
## Rust focus — the vital 20%
Syntax and concepts to practice, each mapped to real app code.
## Practice drills (20–30 min each)
- [ ] Drill 1
- [ ] Drill 2
- [ ] Drill 3
## English focus
What and how we practice this sprint (README section, commit messages, article…).
## Session rhythm (45 min)
5 min — review yesterday's log and errors → 30 min — focused practice → 10 min — DevLog + ErrorJournal.
## Definition of done
- [ ] Deliverable works
- [ ] `cargo test` green
- [ ] clippy clean
- [ ] Retro filled in below
## Retrospective
### Shipped
### Frustrations (link ErrorJournal entries)
### Hours logged / English notes
+27
View File
@@ -0,0 +1,27 @@
---
type: task
status: todo
priority: P2
sprint:
gitea-issue:
created: {{date}}
due:
tags: [task]
---
# {{title}}
## Goal
One sentence describing what must exist when this task is done.
## Acceptance criteria
- [ ] Criterion 1
- [ ] Criterion 2
## Notes
Context, links to research notes and decisions, sketches.
## Log
- {{date}} — task created.
<!-- status: todo | in-progress | blocked | review | done | cancelled · priority: P0 urgent … P3 nice-to-have -->
+75
View File
@@ -0,0 +1,75 @@
---
type: article
status: published
created: 2026-10-17
updated: 2026-10-17
tags: [article, comparison, file-manager]
sources:
- "[[research/ranger-deep-dive]]"
- "[[research/competitor-landscape]]"
- "[[research/rust-preview-stack]]"
- "[[research/plugin-architecture]]"
---
# Terminal file managers in 2026: what viexp should learn from them
> **TL;DR** — Ranger defined the interaction vocabulary; yazi proved a Rust rewrite can be both
> faster and more extensible; nnn and lf showed that scripts-as-APIs beat embedded interpreters
> for Linux users; joshuto and hunter showed how scope and toolchain choices kill projects.
> viexp takes ranger's UX, yazi's architecture patterns, and an extension model that starts
> declarative and grows to scripts, then Lua.
## Introduction
Before writing viexp we studied nine terminal file managers (ranger, yazi, nnn, lf, joshuto,
felix, hunter, vifm, broot, mc). This article condenses the four research notes above into
the product decisions behind viexp. Full data lives in the linked research notes.
## The three eras
1. **The blueprint (ranger, 2009–)**. Miller columns, vim keys, `:bulkrename`, rifle opener
rules, `--choosefile`. Still the most feature-complete — and still slow for structural
reasons: a synchronous Python loop that stats every file inline, `statvfs` on every frame
(4–10 s j/k lag on network mounts, issue #1025), ~1 s interpreter startup, and an image
preview stack built on an abandoned X11 tool (ueberzug).
2. **The reaction (nnn, lf, vifm, 2015–)**. Minimalism and Unix philosophy. nnn fits in
150 KiB with zero config and extends through executables; lf keeps the UI responsive with
async I/O and pushes previews to an external script; vifm goes the opposite way and makes
config a programming language. Each proves a point: speed is a feature; process contracts
are a complete extension API; but none ships native graphics, so preview ecosystems
fragmented into incompatible script zoos.
3. **The synthesis (yazi, broot, 2023–)**. yazi is what happens when you take ranger's UX and
rebuild it on tokio + ratatui: paged preloading (only visible rows get work), prioritized
worker pools, three cancellation mechanisms, protocol auto-detection (kitty/iterm2/sixel/
ueberzug++/chafa), TOML config with `prepend_/append_` merge, and Lua plugins through the
same dispatch tables as built-ins. broot rethinks navigation itself around an interruptible,
composable tree search.
## The five decisions viexp takes from this
| # | Decision | Source | ADR |
|---|----------|--------|-----|
| 1 | ratatui + crossterm UI, single-threaded actor fed by background workers | yazi, felix | ADR-001 |
| 2 | Extension model in three phases: TOML rules → external script contract → Lua | yazi tables, nnn/lf contracts, joshuto's failure | ADR-002 |
| 3 | Every file-type module (image/video/audio/pdf/djvu/epub/md) is a registry entry with a runtime `enabled` switch; cargo features only trim binaries | yazi, Cargo book | ADR-003 |
| 4 | Image protocol chain: kitty → iterm2 → sixel → ueberzug++ → chafa/halfblocks, detected at runtime | yazi-adapter | ADR-004 |
| 5 | Async adopted gradually: sync core first, threads for previews, tokio for the full pipeline | lf (threads suffice) → yazi (tokio) | ADR-005 |
## What we deliberately do not copy
- **Full ranger parity in one release** — the joshuto trap (5 config files, half-built command
line, 14-month release gap). viexp ships a narrow polished core first.
- **Nightly Rust or linked C media frameworks** — the hunter trap. Stable toolchain; media via
spawned `ffmpeg`/`ffprobe` processes.
- **A bespoke config DSL** — the lf trap. TOML now, Lua later, never a private language.
- **GPL-linked crates in the binary** (`epub` crate) — spawned GPL tools (`pdftoppm`, `ddjvu`,
ueberzug++) are fine; statically linked GPL is not.
## Conclusions
The market gap viexp aims at: ranger's ergonomics with yazi-grade speed, a **script-first
extension model** (Lua last, not first), and documentation-driven module replaceability —
built as a learning project that teaches Rust and technical English at 20 hours per sprint.
## Sources
See the four linked research notes; each ends with its own verified source list.
View File
+33
View File
@@ -0,0 +1,33 @@
---
type: decision
adr: 001
status: accepted
date: 2026-10-17
supersedes:
tags: [adr, ratatui, tui]
---
# ADR-001: ratatui + crossterm as the TUI foundation
## Context
viexp needs a terminal UI with a three-pane Miller layout, an event loop, and later image
graphics. The Rust TUI ecosystem has one de-facto standard and several niche options.
## Options considered
| Option | Pros | Cons |
|--------|------|------|
| **ratatui + crossterm** | De-facto standard (yazi, felix, broot); 0.30 modular facade; ratatui-image companion; huge docs/community | Immediate-mode model to learn; subcrate churn in 2026 |
| cursive | widget-toolkit feel | slower cadence, different paradigm |
| tui-realm | retained mode on ratatui | extra complexity for one maintainer |
| tui-rs | — | dead upstream |
| hand-rolled on crossterm | total control | reinvents widgets; hunter died with this approach |
## Decision
Use **ratatui 0.30 (facade) with the crossterm backend**. Pin minor versions in Cargo.toml;
adopt the ratatui-org `ratatui-image` crate when previews arrive (Sprint 6).
## Consequences
- Positive: best-documented path; skills transfer to yazi/helix-adjacent tooling; image widget exists.
- Negative: immediate-mode rendering must be learned early (owned by Sprint 2); 0.30 subcrate
split may cause dependency churn.
- Mitigations: pin versions; single render function per pane; UI state isolated in one struct.
@@ -0,0 +1,45 @@
---
type: decision
adr: 002
status: accepted
date: 2026-10-17
supersedes:
tags: [adr, plugins, architecture]
---
# ADR-002: three-phase extension architecture (TOML → scripts → Lua)
## Context
viexp must let Linux users disable, replace and add file-type behaviors easily. Research
([[research/plugin-architecture]]) shows: declarative rules carry configuration, external
process contracts carry ~90% of real-world extension, and embedded interpreters are the most
expensive layer to stabilize (yazi is still "beta" partly because of it). WASM (zellij) costs
host complexity that a single-maintainer learning project cannot afford.
## Options considered
| Option | Pros | Cons |
|--------|------|------|
| Phased: TOML rules → script contract → Lua (mlua) | each phase shippable; users' language of choice; no unsafe early | three layers to document eventually |
| Lua from day 1 | yazi-grade power early | API design + FFI before first working product; high frustration risk |
| WASM components (wasmtime/extism) | best sandboxing | WIT churn, users must compile components; zellij-scale effort |
| External-only | simplest | no stateful in-process previews/fetchers ever |
## Decision (confirmed with the project owner, 2026-10-17)
1. **v0.1 — declarative registry**: `modules.toml` maps mime/glob → previewer/opener; per-module
`enabled` switch; user rules merge via `prepend_*`/`append_*` (yazi semantics). All built-in
modules dispatch through the same tables.
2. **v0.2 — external process contract (frozen, documented)**: previewer argv = `(path, width,
height, x, y)`; env context (mime, selection file, session dir); write-back via FIFO/socket
(nnn-style opcodes); `cleaner` hook; documented exit codes + SIGPIPE semantics.
Reference plugins in bash and Python ship in-repo.
3. **v1.0+ — embedded Lua** via **mlua with vendored Lua**, registered through the same tables;
only for what scripts cannot do (stateful/in-process previews). Re-evaluate Luau if sandbox
isolation becomes a requirement. **WASM is explicitly rejected for v1.x.**
## Consequences
- Positive: shippable increments; extension by Linux-familiar means; Lua postponed until the
host API is stable enough to expose.
- Negative: three layers of documentation eventually; write-back protocol must be designed
carefully once and frozen.
- Mitigations: one dispatch model for all layers (yazi lesson); contract docs written as part
of the v0.2 sprint DoD, not after.
@@ -0,0 +1,38 @@
---
type: decision
adr: 003
status: accepted
date: 2026-10-17
supersedes:
tags: [adr, modules, config]
---
# ADR-003: file-type modules are registry entries, disableable at runtime
## Context
Requirement: images, video, audio, PDF, DJVU, EPUB and Markdown previews work out of the box,
but a user can disable or replace any of them without recompiling. Cargo features alone fail
this: feature unification breaks packagers, and removing a default feature is a breaking
change ([[research/plugin-architecture]]).
## Options considered
| Option | Pros | Cons |
|--------|------|------|
| Runtime registry + per-module `enabled` flag (+ optional cargo features for binary size) | one-line disable; replaceable by script rule; packager-safe | slightly larger default binary |
| Cargo features only | smallest binaries | recompile to disable; unification traps; breaking-change semantics |
| Everything external scripts (ranger scope.sh model) | max flexibility | out-of-box experience degrades; 700-line bash nobody dares edit |
## Decision
Every file-type capability is a **module** registered in a TOML registry:
`name`, `match` (mime/glob), `previewer` (built-in id or external command), `opener`,
`enabled` (default true). Precedence: user rules → built-in rules, with `prepend_/append_`
merge. Built-in implementations live behind additive cargo features (default-on: `image`,
`pdf`, `media`, …) purely to trim binaries; a feature-disabled module degrades to a TOML rule
pointing at an external tool (e.g. `pdftotext`) instead of disappearing.
## Consequences
- Positive: `enabled = false` in one line; third-party replacement without recompile; matches
ADR-002 phase 1 exactly.
- Negative: two config concepts (registry + features) need clear documentation.
- Mitigations: features documented as "binary size only, not functionality"; docs page
"Disabling and replacing modules" is part of the v0.1 docs DoD.
@@ -0,0 +1,43 @@
---
type: decision
adr: 004
status: accepted
date: 2026-10-17
supersedes:
tags: [adr, preview, kitty, sixel]
---
# ADR-004: image preview protocol chain (kitty first)
## Context
Ranger's biggest UX liability is image preview depending on unmaintained X11-only ueberzug.
Research ([[research/rust-preview-stack]]): terminals support one of several incompatible
graphics protocols; no single one covers everything. The owner's daily terminal is **kitty
without a multiplexer**, but viexp targets all Linux users.
## Options considered
| Option | Pros | Cons |
|--------|------|------|
| Multi-protocol chain behind one trait, runtime detection (yazi model) | works everywhere; upgrade path | most implementation work |
| kitty-only | minimal work for owner's setup | useless for everyone else; vifm/nnn trap |
| Always chafa/halfblocks | works everywhere incl. ssh | ugly; not a differentiator |
## Decision
Implement a `GraphicsAdapter` trait with a **priority chain, first match wins**:
1. **kitty graphics** (unicode-placeholder mode) — kitty ≥0.28, Ghostty — implemented first
2. **iTerm2 inline** images
3. **sixel** (via ratatui-image/quantette)
4. **ueberzug++ subprocess** (X11 + wlroots Wayland)
5. **chafa/halfblocks** — universal floor
6. plain text (name, size, mime)
Detection: `$TERM`/`$TERM_PROGRAM`/`$XDG_SESSION_TYPE` env + terminal capability queries;
shortcut via `ratatui-image` `Picker`, with ueberzug++/chafa as our tiers 4–5.
Thumbnails: decode once at preview size → disk cache → in-memory LRU → neighbor preloading.
## Consequences
- Positive: owner gets tier-1 quality from Sprint 6; everyone else degrades gracefully;
no dead X11 dependency.
- Negative: five adapters is real work; tmux/alacritty edge cases documented, not solved.
- Mitigations: chain built incrementally (kitty in Sprint 6, sixel/ueberzug++ in the backlog);
adapter chosen once per session, cached.
+38
View File
@@ -0,0 +1,38 @@
---
type: decision
adr: 005
status: accepted
date: 2026-10-17
supersedes:
tags: [adr, async, tokio, threads]
---
# ADR-005: adopt async gradually — sync core, then threads, then tokio
## Context
Ranger's fatal flaw is a synchronous loop that stats every file inline ([[research/ranger-deep-dive]]).
yazi proves tokio solves it but is a lot of machinery to learn while also learning Rust.
lf (Go goroutines) shows threads alone already keep a UI responsive.
## Options considered
| Option | Pros | Cons |
|--------|------|------|
| Gradual: sync → std threads + channels → tokio | learnable steps; working product at every stage | one refactor of the preview pipeline in Sprint 8 |
| tokio from day 1 | final architecture sooner | async lifetimes + borrow checker + TUI at once = frustration barrier |
| threads-only forever | simpler mental model | ecosystem (notify bridges, timeouts, process spawning) is tokio-first |
## Decision
- **Sprints 1–5**: synchronous core. The TUI loop owns all state; directory loading and file
ops are fast enough synchronously for a first product.
- **Sprints 6–7**: previews move to `std::thread` + `std::sync::mpsc` — UI stays responsive,
ownership lessons are explicit.
- **Sprint 8**: adopt **tokio** for the full preview/media pipeline (yazi architecture:
runtime workers marshal results to the single-threaded UI actor via a channel). Introduce
cancellation via a generation-counter ("ticket") pattern before adopting tokio `abort`.
## Consequences
- Positive: each concurrency model is learned in isolation; no big-bang async rewrite of the UI.
- Negative: preview pipeline is written twice (thread → task) — accepted as a deliberate
learning repetition (Kaufman: quantity of practice beats premature perfection).
- Mitigations: keep the preview pipeline behind a small interface (`Previewer` trait, ADR-002)
so the swap is local, not global.
+27
View File
@@ -0,0 +1,27 @@
---
type: devlog
date: 2026-10-17
sprint: 0
minutes: 120
tags: [devlog]
---
# Dev log — 2026-10-17
Project bootstrap day: research, vault setup and roadmap.
## What I did
- Defined the product: a ranger-inspired terminal file manager in Rust, extensible by Linux users.
- Ran market research across ranger, yazi, nnn, lf, joshuto, vifm, broot and others.
- Set up the Obsidian vault: templates in `TMP/`, docs structure, workflows.
- Chose the direction: ratatui + crossterm, kitty graphics first, script-based plugins first
with Lua later (ADR-001 … ADR-003).
- Wrote the 20-hour sprint roadmap.
## English practice
- *bootstrap* — первичная настройка, «раскрутка» проекта; *we bootstrapped the vault*.
- *milestone* — контрольная точка, веха; *M1 is the daily-driver milestone*.
- *trade-off* — компромисс; *Lua plugins are a trade-off between power and complexity*.
## Next session
- Start Sprint 0: Rust toolchain, cargo project, Gitea board and CI.
+33
View File
@@ -0,0 +1,33 @@
# viexp — Knowledge Index
This folder is the project's Obsidian vault: all long-lived knowledge about viexp lives here,
in English. The Obsidian app config (`.obsidian/`) is gitignored; the notes themselves are part
of the repository.
## Entry points
- [[roadmap]] — the master plan: 20-hour learning sprints (Josh Kaufman, "First 20 Hours")
- [[workflows/research-workflow|Research workflow]] — how we collect and analyze research artifacts
- [[workflows/gitea-workflow|Gitea workflow]] — how the board, labels, milestones and issues are organized
- [[workflows/english-workflow|English workflow]] — how the project doubles as English practice
## Folders
| Folder | Contents | Template |
|--------|----------|----------|
| `research/` | Raw research notes: one question per note | `TMP/Research.md` |
| `articles/` | Polished syntheses, comparisons, guides | `TMP/Article.md` |
| `decisions/` | Architecture Decision Records (ADRs) | `TMP/Decision.md` |
| `sprints/` | One plan+retro note per 20-hour sprint | `TMP/Sprint.md` |
| `tasks/` | Task notes (mirrored as Gitea issues) | `TMP/Task.md` |
| `devlog/` | Daily practice journal | `TMP/DevLog.md` |
| `learning/` | Glossary, error journal, cheat sheets | `TMP/ErrorJournal.md`, `TMP/Glossary.md` |
| `attachments/` | Images and other research artifacts | — |
| `TMP/` | Obsidian templates (vault root) | — |
## Conventions
- Note titles in `kebab-case` English; frontmatter `type`/`status` is required (used for filtering).
- Headings are stable across notes of one type, so Obsidian folding always works the same way.
- Every decision must trace back to at least one `research/` note; every task to a decision or the roadmap.
- New templates live in `TMP/` (Obsidian: Settings → Templates → folder = `TMP`).
+19
View File
@@ -0,0 +1,19 @@
---
type: glossary
created: 2026-10-17
tags: [glossary, english]
---
# Glossary
Terms met while working on viexp. Reuse each term in your own sentence before moving it to
the "active" mental column.
| Term / phrase | Translation | Where I met it | My example sentence |
| ------------- | ----------- | -------------- | ------------------- |
| bootstrap | первичная настройка | devlog 2026-10-17 | We bootstrapped the repo before writing code. |
| milestone | веха, контрольная точка | roadmap | Sprint 3 ends at the first daily-driver milestone. |
| trade-off | компромисс | research notes | Every plugin system is a trade-off. |
| dogfooding | использование своего продукта ежедневно | fm research | We start dogfooding after Sprint 3. |
| out of the box | «из коробки», без настройки | README | Image preview works out of the box. |
| fallback | запасной вариант | preview research | chafa is the fallback when graphics protocols fail. |
+76
View File
@@ -0,0 +1,76 @@
---
type: research
status: analyzed
confidence: high
created: 2026-10-17
question: Which terminal file managers exist in 2026, what does each do best, and which ideas should viexp adopt or avoid?
tags: [research, file-manager, yazi, nnn, lf, vifm, broot]
---
# Competitor landscape (October 2026)
## Feature matrix
| Manager | Lang | TUI | Image preview | Extensions | Config | Async | State (last release) |
|---|---|---|---|---|---|---|---|
| **yazi** | Rust | ratatui | native kitty/iterm2/sixel/ueberzug++/chafa | Lua (mlua, vendored) + `ya pack` | TOML ×3 | tokio, fully | Very active, v26.9.1 (2026-09) |
| **nnn** | C | ncurses | via FIFO plugins | any executable; `NNN_PIPE`/`NNN_FIFO` protocol | none (flags+env) | no (tiny by design) | Active, v5.3 (2026-08) |
| **lf** | Go | own | sixel native; else scripts | shell cmds, `push`, remote socket API | lfrc DSL | async I/O | Active, r42 (2026-07) |
| **joshuto** | Rust | tui-rs/termion | external scripts | config only | 5 TOMLs | partial | Semi-stalled, v0.9.9 (2025-05) |
| **felix** | Rust | termion | chafa + bat | none | YAML | partial | Dormant since 2025-04 |
| **hunter** | Rust | own | halfblock/sixel/kitty | mime actions | conf+keymap | heavily threaded | Dead (2020/2022) |
| **vifm** | C | ncurses | community scripts only | vim-like `:commands` + Lua | vifmrc | no | Active, v0.14.4 (2026-05) |
| **broot** | Rust | crossterm+termimad | kitty graphics | verbs, skins | TOML | interruptible search | Very active, v1.61 (2026-10) |
| **mc** | C | own | none | extfs VFS helpers | menus | background ops | Active, 4.8.33 |
## What the winners do right
- **yazi** (~43k stars): ~25-crate workspace, everything on tokio; micro/macro worker pools with
priorities (mime/previews preempt copying); **paged preloading** — only visible rows get work;
three cancellation mechanisms (tokio abort, atomic tickets for CPU work, instruction-count
checks for Lua); two-pass image pipeline (downscale+cache → fit to terminal); DDS pub/sub
between instances without a daemon. v25.4 doubled startup speed via config parser rewrite.
- **nnn**: 150 KiB binary, zero config; plugins = executables with env-var contract
(`$1` hovered file, `$2` cwd, `$3` picker) and a tiny write-back protocol via `NNN_PIPE`;
`NNN_FIFO` streams hovered paths so external scripts render live previews.
- **lf**: external `previewer(file, w, h, x, y)` + `cleaner` contract with exit-code-controlled
caching; `push` simulates keystrokes making mappings composable; server model over Unix socket.
- **broot**: every keystroke interrupts the running search; fuzzy+regex+content combined with
boolean operators; collapsed tree respecting .gitignore.
- **felix**: built-in trash + undo/redo + zoxide `z` jump — small features with huge daily value.
## Ideas worth stealing (ranked)
1. yazi's paged task scheduling + prioritized workers + cancellation tickets (perceived speed)
2. Protocol auto-detection adapter layer behind one trait (kitty/iterm2/sixel/ueberzug++/chafa)
3. lf's previewer/cleaner argv contract — cheapest path to infinite formats
4. nnn's FIFO hover event stream for external live previews
5. yazi's TOML tables with `prepend_*`/`append_*` merge semantics for rules
6. TOML for config + (later) Lua for behavior + pinned plugin packages
7. broot's interruptible, composable search
8. lf's `push` key-simulation primitive
9. felix's trash + undo + `z` jump
10. Performance as a marketable feature (both nnn and yazi publish benchmarks)
## Anti-patterns to avoid
| Trap | Victim | Lesson |
|------|--------|--------|
| Full ranger parity alone | joshuto | 5 config files, janky command line, 14-month release gap → audience left for yazi. Ship narrow and polished. |
| Nightly compiler + heavy C deps | hunter | GStreamer + nightly = unbuildable for users. Stable Rust, spawned binaries for media. |
| No native graphics path | vifm, nnn, mc | Preview zoo of mutually incompatible scripts. Build the adapter layer early. |
| Bespoke config DSL | lf | A whole parser users must learn; standard TOML + scripting runtime beats it. |
| Doctrinal non-features | lf (no tabs) | Minimal defaults and power features are not contradictory (yazi proves it). |
| Monolith | mc | Thin core + clean contracts. |
| API churn without pinning | yazi (mild) | Plugin version pinning story from day one. |
| Silence without governance | felix, hunter | Release cadence = perceived life. |
## Outcome
Shaped ADR-001 (ratatui), ADR-002 (extension phases), ADR-004 (protocol chain), and the
roadmap order (core narrow first, previews second, plugins third, Lua last).
## Sources
- yazi: github.com/sxyazi/yazi, https://yazi-rs.github.io/blog/why-is-yazi-fast/, releases v25–v26
- nnn: github.com/jarun/nnn + plugins/README.md
- lf: github.com/gokcehan/lf + doc.md
- joshuto, felix, hunter, vifm, broot, mc: repos + release APIs (dates verified 2026-10-17)
+69
View File
@@ -0,0 +1,69 @@
---
type: research
status: analyzed
confidence: high
created: 2026-10-17
question: How should viexp's file-type modules and plugins be designed so Linux users can disable, replace and extend them easily?
tags: [research, plugins, architecture, lua, wasm]
---
# Extension architecture research
## Mechanism comparison
| Mechanism | Safety | Ease for users | Host complexity | Hot reload | Proof |
|---|---|---|---|---|---|
| External process contract (argv/env/stdout) | low (= shell) | **highest** — any language | very low | free | nnn plugins, lf previewer, ranger scope.sh |
| Declarative rules (TOML / rifle / mailcap) | high (data, not code) | high — one-liners | low | free | rifle.conf, yazi `[plugin]` tables |
| In-process Lua (mlua) | medium — sandboxes leak (2025 CVE); Luau is the hardened dialect | medium | medium-high | yes | yazi (mlua 0.12, vendored Lua 5.5), wezterm, neovim |
| Rhai | high | low (niche language) | low-medium | yes | niche |
| JS (deno_core/V8) | medium-high | medium | high (~30 MB) | yes | Deno |
| WASM (wasmtime/extism) | **highest** | lowest (compile a component) | high (WIT churn) | yes | zellij |
| Host-language plugins (Python) | lowest (monkeypatching) | high for pythonistas | medium, fragile | poor | ranger |
## Case-study lessons
- **ranger**: the script pipeline (scope.sh + rifle) carried ~90% of real customization; the
Python layer exists because previews/commands need *state*. Three disjoint mechanisms = confusion.
- **nnn**: cleanest "process as API": `$1`=file, `$2`=cwd, `$3`=picker; context in env
(`NNN_FIFO` hover stream); control back via `NNN_PIPE` with tiny opcodes (`1c/etc` = cd ctx 1).
- **lf**: minimum viable preview contract = `(path, w, h, x, y)` + `cleaner` hook + exit-code
caching semantics. Document SIGPIPE/exit codes or users suffer.
- **yazi**: built-ins are preset plugins routed through the same `[plugin]` TOML tables users
edit (`prepend_*`/`append_*` merge) — one dispatch model for built-in and user code. mlua
with vendored Lua so users install nothing. Still BETA → in-process scripting is the most
expensive layer to stabilize.
- **neovim**: Lua won because the host API is rich, stable and uniform — half-exposed APIs are
worse than external scripts.
- **zellij**: WASM gives real sandboxing but locks architecture (single-threaded synchronous
plugins; WASIp2 migration "a significant undertaking"). Not worth it for v1.
- **kakoune**: `disabled_hooks` regex = cheap runtime kill-switch worth copying.
## Cargo feature flags = compile-time layer, not the primary switch
Features (`pdf`, `video`, … default-on) trim binary size and C deps, but feature unification
breaks packagers, combinations explode, and removing a default feature is a breaking change.
**Runtime `enabled = false` in modules config must remain the real disable mechanism**; every
`#[cfg]`-gated module should degrade to a TOML rule pointing at an external tool.
## Recommended phasing (adopted)
- **v0.1 — declarative core**: every file-type module = rule in a TOML registry mapping
mime/glob → previewer/opener, with per-module `enabled` switch and `prepend_/append_` merge.
Built-ins already dispatch through this table → replacing/reordering needs no recompile.
- **v0.2 — external process protocol**: freeze the script contract —
`argv = (path, width, height, x, y)`, env context, FIFO/socket write-back, `cleaner` hook.
Ship bash + Python reference plugins. This is where 90% of Linux-user extensibility lives.
- **v1.0+ — embedded Lua** (mlua, vendored): only for what scripts cannot do — stateful,
in-process previews/fetchers — registered through the same tables. Evaluate Luau if
sandboxing becomes a requirement. WASM explicitly rejected for v1.
## Outcome
Adopted as [[decisions/ADR-002-extension-architecture|ADR-002]] and [[decisions/ADR-003-file-type-modules|ADR-003]].
## Sources
- yazi docs (plugins overview, configuration), yazi Cargo.tomls (mlua 0.12 lua55 vendored)
- nnn plugins README; lf doc.md; rifle(1); RFC 1524 (mailcap)
- zellij plugin docs + discussion #4139; wezterm config docs; neovim lua.txt; kakoune FAQ
- mlua/rhai/gluon repos; deno blog; Lua sandbox-bypass CVE coverage (2025)
- Cargo book: features.md
+77
View File
@@ -0,0 +1,77 @@
---
type: research
status: analyzed
confidence: high
created: 2026-10-17
question: What exactly makes ranger great, what makes it slow and fragile, and what must a Rust successor keep or fix?
tags: [research, ranger, file-manager]
---
# Ranger deep dive
## Research question
Ranger is the most feature-complete terminal file manager but famously slow in specific
scenarios. We need a precise keep/fix list before writing a single line of viexp.
## Findings
### Project state (Oct 2026)
Python + curses, Miller-columns UI. 17.4k stars, ~900 open issues, last tag v1.9.4 (Nov 2024),
commits still trickling. Maintainers openly state they lack manpower (issue #1717).
### Architecture
- Config = 4 files with **different merge semantics**: `rc.conf` merges, `rifle.conf` replaces,
`scope.sh` replaces (~700-line bash), `commands.py` merges (Python subclassing).
- Preview pipeline = external `scope.sh`; communicates via exit codes:
`0` = stdout preview, `1` = none, `2` = fixed-height text, `5` = image written to cache dir.
- Plugins = Python monkeypatching with full interpreter access ("Plugins have full access to
your computer!"). Ecosystem: ranger-devicons, ranger-zoxide, ranger-fzf-filter, etc.
- Image protocols: w3m, ueberzug, urxvt, kitty, sixel, iterm2, terminology.
### Why it is slow (root causes)
| Cause | Evidence |
|-------|----------|
| Synchronous UI loop — directory listing + `stat` of every file run inline | issue #1173: 100k files = multi-second uninterruptible hang (open since 2018) |
| No async I/O anywhere; threading refactor stalled | issue #1025 literally labeled `wants-async` |
| `statvfs` on **every status-bar refresh** → 4–10 s j/k lag on sshfs/NFS/CryFS/s3fs | issue #1025; vifm stays fast in identical setups |
| Python interpreter startup ~1 s cold | issue #1717 |
| Every preview spawns scope.sh + a chain of external binaries | architecture |
### Pain points (user-verified)
1. ueberzug unmaintained and X11-only; ueberzugpp fixes exist but are bolted on
(ranger #3188, #3158, #2995; ueberzugpp #122 fractional-scaling bug on Sway/Hyprland).
2. Network mounts unusable without manual `setlocal` hacks or `F` (freeze_files).
3. `:flat` broken past ~30k files (#379).
4. No built-in trash — ArchWiki documents `map DD shell gio trash` as the workaround.
5. Config fragmentation: four files, four formats, four merge behaviors.
### What users love (must keep)
Vim keys, Miller columns, `:bulkrename` via `$EDITOR` (the killer feature), rifle-style
ordered opener menu (`r`), preview breadth (20+ types), tabs/bookmarks/marks, `:filter`,
`--choosefile/--choosedir` + cd-on-exit, per-path options (`setlocal`).
## Keep / Fix table
| Keep | Fix |
|------|-----|
| Miller 3-pane layout, vim key vocabulary | Native binary startup (no interpreter) |
| `:bulkrename` via `$EDITOR` | Async dir loading / stat / preview — UI never blocks |
| Rifle-style opener rules + chooser menu | No `statvfs` per frame; poll with timeout+backoff |
| Exit-code preview contract (scope.sh idea) | First-class trash (`gio trash`-compatible) + undo |
| Preview breadth with caching | Native kitty/sixel/ueberzug++ graphics, Wayland-safe |
| `setlocal` per-path options | One config format (TOML) with documented merge semantics |
| `--choosefile/--choosedir`, cd-on-exit | Virtual scrolling for 100k+ entry dirs |
| `F` freeze escape hatch | Automatic freeze on network/fuse mounts |
## Outcome
Direct input to [[decisions/ADR-002-extension-architecture|ADR-002]] (extension model),
ADR-004 (preview protocols) and the roadmap's async requirements (Sprint 8).
## Sources
- https://github.com/ranger/ranger — README, wiki, examples/plugins
- https://wiki.archlinux.org/title/Ranger
- Issues: #1717 (startup), #1173 (100k files), #1025 (network lag), #379 (`:flat`),
#3188 / #3158 / #2995 (ueberzug family)
- https://github.com/seebye/ueberzug (unmaintained notice), https://github.com/jstkdng/ueberzugpp
- Community: r/commandline "Is yazi overhyped?", r/linux FM threads, x-cmd six-FM comparison,
https://mqdir.com/blog/file-management/nnn-vs-yazi
+75
View File
@@ -0,0 +1,75 @@
---
type: research
status: analyzed
confidence: high
created: 2026-10-17
question: Which Rust crates should render previews (images, PDF, DJVU, EPUB, markdown, media) in a terminal, with what licenses and fallbacks?
tags: [research, rust, crates, preview, ratatui]
---
# Rust preview stack (October 2026)
Versions verified against crates.io metadata on 2026-10-17.
## Recommended stack
| Concern | Crate | Why | Notes / license |
|---|---|---|---|
| TUI | **ratatui 0.30** + **crossterm 0.29** | De-facto standard; 0.30 split into `ratatui-core`/`-widgets` facade; yazi & felix use it | MIT; MSRV 1.88 |
| Images in TUI | **ratatui-image 11.1** | kitty, sixel, iTerm2, halfblocks + `Picker` protocol/font detection; lives in the ratatui org | MIT |
| Image decode | **image 0.25** | yazi's choice; jpeg/png/gif/webp/bmp/tiff (+avif feature) | MIT/Apache |
| Resize | **fast_image_resize 6.1** (SIMD) or `image::thumbnail` (triangle filter, yazi-style) | hot loop | MIT/Apache |
| Sixel encode | **quantette 0.6** (+ moxcms, palette) | yazi's actual sixel pipeline | MIT |
| PDF | **pdfium-render 0.9** + pdfium binary | only mature page→image in-process option | pdfium is **Apache-2.0** (b redistribute, ~5 MB); spawn `pdftoppm` as fallback |
| DJVU | **djvu-rs 0.43** (pure Rust, render→image) | first usable pure-Rust DjVu decoder | **6 months old** — keep `ddjvu` fallback |
| EPUB | hand-rolled: **zip** + quick-xml/html-to-text | the `epub` crate is **GPL-3.0 — viral** if statically linked | avoid the trap |
| Markdown | **comrak 0.56** → styled ratatui lines, or **termimad** (ready-made renderer) | termimad = fastest integration (broot uses it) | MIT |
| Syntax highlight | **syntect 5.3** | yazi's choice; regex Sublime syntaxes, no C deps | tree-sitter = heavier, per-language grammars |
| Media metadata | **spawn `ffprobe -print_format json`** | no linking pain; what yazi does | ffmpeg-next ties you to system FFmpeg — avoid |
| Video thumbnail | **spawn `ffmpeg -ss … -vframes 1 -vf scale…`** (+ `attached_pic` cover-art fallback) | yazi's proven recipe | — |
| Audio (optional in-process) | **symphonia 0.6** | pure-Rust metadata/album art | MPL-2.0 |
| MIME | **infer 0.22** (magic bytes) + **xdg-mime 0.4** (extension DB) | fast sniffing + correct `text/*` | `new_mime_guess` unmaintained — avoid |
| FS events | **notify 8.2** | yazi's watcher | CC0 |
| Fuzzy | **nucleo 0.5** | Helix's matcher, library-first | MPL-2.0 |
| Config | **serde + toml 1.1** | yazi's set | solid |
| Errors/logs | **anyhow + thiserror 2.x + tracing** | boring and correct | — |
| Async | **tokio 1.x** with results marshalled to a single-threaded UI actor | yazi's exact architecture; ecosystem is tokio-first | start threads-only, adopt tokio in Sprint 8 |
## Image protocol fallback chain (first match wins)
1. **kitty graphics** (unicode-placeholder mode) — kitty ≥0.28, Ghostty ← *our primary target*
2. **iTerm2 inline** — WezTerm, iTerm2, Warp, VSCode terminal
3. **sixel** — foot, Konsole 22.04+, Windows Terminal ≥1.22, WezTerm, rio
4. **ueberzug++ subprocess** — X11 + Wayland (wlroots compositors: sway/hyprland/niri/wayfire);
the only option for alacritty; not GNOME/KDE compositors
5. **chafa / halfblocks** — works everywhere incl. SSH and tmux
6. text: filename + size + mime
Terminal facts (Oct 2026): kitty has **no sixel by design**; alacritty rejected both protocols;
tmux needs `allow-passthrough on` and never passes kitty-old. The daily terminal here is
**kitty without a multiplexer** → tier 1 covers the primary user; tiers 3–5 cover everyone else.
## Thumbnail caching (yazi-proven)
Decode once at preview size (max ~600×900, triangle filter) → persist rendered pages/frames to
a disk cache dir → keep decoded previews in an in-process LRU → preload neighbors in background.
Re-encoding for a protocol is cheap; re-decoding is not.
## Risk notes
- GPL traps: `epub` crate (static link), ueberzug++ (fine — spawned), pdftoppm (fine — spawned).
- pdfium must be shipped as a binary (~5 MB/platform) or static-linked via pdfium-render `static`.
- djvu-rs is young; config-selectable `ddjvu` fallback required.
- ratatui 0.30 subcrate split may cause ecosystem churn in 2026 — pin carefully.
## Outcome
Feeds ADR-001, ADR-004 and Sprints 5–8 crate choices.
## Sources
- https://github.com/sxyazi/yazi (adapter/Cargo.tomls, preset plugins pdf.lua/video.lua)
- https://yazi-rs.github.io/docs/image-preview, /blog/why-is-yazi-fast/
- crates.io version metadata (ratatui, ratatui-image, image, fast_image_resize, quantette,
pdfium-render, djvu-rs, epub, comrak, termimad, syntect, symphonia, notify, nucleo, infer, xdg-mime)
- https://github.com/ajrcarey/pdfium-render; pdfium LICENSE (Apache-2.0)
- https://www.arewesixelyet.com; https://sw.kovidgoyal.net/kitty/graphics-protocol
- https://github.com/jstkdng/ueberzugpp; https://github.com/matyushkin/djvu-rs
+196
View File
@@ -0,0 +1,196 @@
# viexp Roadmap — Rust + English in 20-hour sprints
Method: Josh Kaufman, **"The First 20 Hours"** — rapid skill acquisition. The project is split
into isolated 20-hour sprints (~45 min/day for a month each). Every sprint: one Rust sub-skill,
one working app increment, one English artifact. The goal of each sprint is to cross the
frustration barrier and ship something that runs — not perfect code.
**Kaufman's rules, adapted**
| Kaufman principle | How this roadmap applies it |
|---|---|
| Deconstruct the skill | The table below: each sprint = one sub-skill + one app increment |
| Learn just enough to self-correct | Per-sprint "Rust focus — the vital 20%": the minimum theory to *read your own compiler errors*. Every error goes to the ErrorJournal with a self-correction cue |
| Remove barriers to practice | Sprint 0 sets up the whole environment: toolchain, linters, fast feedback (`bacon`), Gitea board, CI. Never debug your setup during practice time |
| Fast feedback loops | `cargo check`/bacon on save, unit tests per session, immediate AI code review of each session's diff (commit → review → fix) |
| Quantity over quality | Many small commits, many small drills. Working ugly code beats beautiful unfinished code. No over-engineering before v0.1 |
## Skill deconstruction
**Rust sub-skills** (learned in order): cargo & project layout → types & control flow →
ownership/borrowing → structs, enums, pattern matching → `Option`/`Result` & `?` →
modules & crates → traits & generics → iterators → `Path`/`fs` & error handling (`thiserror`/`anyhow`) →
serde/TOML → lifetimes → trait objects & dynamic dispatch → threads & channels →
async/await & tokio → process spawning → caching & performance.
**App increments** (each demoable): `vls` CLI → 3-pane TUI → navigation & file ops →
config & openers → text previews → image previews → document previews → media & async →
script plugins → power features → performance → release.
**English track**: every commit message, every issue, all docs — English only. One written
artifact per sprint. Daily DevLog (3–5 sentences). See [[workflows/english-workflow|English workflow]].
## Session rhythm (45 min, every day)
1. **5 min** — read yesterday's DevLog + ErrorJournal; `git pull`, `bacon` starts.
2. **30 min** — today's drill or feature slice (one drill = one commit).
3. **10 min** — get the diff reviewed, fix what the reviewer flags, write the DevLog entry.
**Frustration rules**: stuck >20 min on one error → ask for a review instead of grinding;
never end a session with broken code (revert to green); a slow day is a reading day
(Rust Book chapter in English) — log it, no guilt.
## Sprints
Legend: 🎯 milestone. Each sprint gets a `docs/sprints/sprint-NN-*.md` note (Sprint template)
and a Gitea milestone with issues created from its Definition of Done.
---
### Sprint 0 — Setup & deconstruction (≈6 h, short)
- **App deliverable**: empty `viexp` cargo project that compiles, `cargo test` runs one trivial
test, CI (Gitea Actions) is green, `bacon` + `clippy` + `rustfmt` wired, Gitea board ready.
- **Rust focus**: toolchain mental model — what cargo, rustc, crates.io, `Cargo.toml` are;
read your first compiler error on purpose (break a program, fix it).
- **English**: README "Status" section + the vault (this roadmap counts).
- **DoD**: `cargo bacon` shows check+test on save; first commit pushed; Sprint-1 issues exist.
### Sprint 1 — Rust core via `vls` (20 h)
- **App deliverable**: `viexp ls` subcommand — a better `ls`: colored names by type,
human-readable sizes, `--sort name|size|mtime`, `--hidden`, `--long`; ≥10 unit tests.
- **Rust focus (vital 20%)**: variables & types, functions, `if`/`match`, loops, `Vec`/`String`
vs `&str`, structs & enums, `Option`/`Result` + `?`, modules, `#[cfg(test)]`.
Rust Book ch. 1–9 (skim 7/8), rustlings `intro→enums`.
- **English**: README usage section; article "Why build a file manager in Rust" (draft).
- **DoD**: `vls ~/` output beats `ls` for your eyes; clippy clean; tests green.
### Sprint 2 — First TUI (20 h) — the borrow-checker sprint
- **App deliverable**: three-pane Miller layout (parent / current / placeholder preview),
`j/k/gg/G` cursor movement, `:q` quits, resize-safe redraw, no flicker.
- **Rust focus**: **ownership & borrowing in anger** (the state struct owns everything),
references vs clones, lifetimes as they appear in ratatui calls, the event-loop pattern
(poll event → mutate state → redraw). Rust Book ch. 4, 10.3 (skim), ratatui official docs.
- **English**: keymap reference table in docs (precise imperative phrasing).
- **DoD**: navigate a 10k-file directory smoothly; panic-free for 15 min of use.
### Sprint 3 — Navigation & file ops (20 h) 🎯 **M1: personal daily driver**
- **App deliverable**: enter/leave directories, hidden toggle, sort cycling, `Space`/`V`
selection, `yy`/`dd`/`pp` copy/cut/paste, delete → **first-class trash** (freedescompliant)
with undo, `:mkdir :touch :rename`, status bar (path, size, free space — **not** per-frame
`statvfs`; 1 Hz poll).
- **Rust focus**: `Path`/`PathBuf`, `fs::read_dir`/`metadata`, real error handling
(`thiserror` for lib-style internals, `anyhow` at main), iterators (`map/filter/sort_by`
chains), `Display` impls.
- **English**: article "Week one of dogfooding my own file manager".
- **DoD**: use viexp instead of ranger for real work for 3 days; log every pain point as an issue.
### Sprint 4 — Config & openers (20 h)
- **App deliverable**: `viexp.toml` (defaults, theme colors, sort) + `modules.toml` registry
v0 (ADR-003): mime/glob → opener, per-module `enabled`, user rules prepend/append;
opener engine (infer + xdg-mime → rule table → `xdg-open` fallback), `r` "open with" menu,
`:open_with`.
- **Rust focus**: serde derive on enums/structs, `Default` + merge semantics, `HashMap`/
`BTreeMap`, first **trait design** (`Opener`), builder-ish config loading with helpful errors.
- **English**: configuration guide draft (reference-style writing).
- **DoD**: any file opens with your preferred app from config alone; broken TOML produces a
friendly error, not a panic.
### Sprint 5 — Text previews & bulk rename (20 h)
- **App deliverable**: preview pane v1 — syntax-highlighted text (syntect) with line numbers
and scrolling, directory preview, file metadata panel, zip/tar listing, `:bulkrename` via
`$EDITOR` (ranger's killer feature), `n/N` plain search, `:` command history.
- **Rust focus**: lifetimes in real signatures, **trait objects** (`Box<dyn Previewer>` registry),
advanced iterators, caching keyed by `(path, mtime)`.
- **English**: article "How a TUI paints text" (explaining rendering → strongest writing practice).
- **DoD**: preview parity with ranger for text/code/archive files.
### Sprint 6 — Image previews (20 h) 🎯 **M2a**
- **App deliverable**: image module through the registry; **kitty graphics adapter first**
(owner's terminal), chafa/halfblocks floor; thumbnail disk cache + LRU; runtime
disable/replace via `modules.toml` proven on images.
- **Rust focus**: the `image` crate, decode/resize pipeline, `#[cfg(feature)]` modules,
pixel data, spawning helper processes safely.
- **English**: article "How terminals display images" (kitty protocol, sixel, ueberzug++ —
your best future blog post; research notes already hold the material).
- **DoD**: PNG/JPEG/WebP/GIF preview instantly in kitty; `enabled=false` kills it in one line.
### Sprint 7 — Document previews (20 h) 🎯 **M2b: full ranger replacement**
- **App deliverable**: PDF (pdfium-render, Apache-2.0), EPUB (hand-rolled zip+XML — **GPL
trap avoided**), Markdown (comrak/termimad), DJVU (djvu-rs + `ddjvu` fallback config).
All as registry modules, all runtime-disableable.
- **Rust focus**: binary data & byte streams, feature-gated crates, error taxonomy design,
more trait objects; reading crate docs as a skill.
- **English**: module documentation pages (PDF/EPUB/DJVU) in user-guide style.
- **DoD**: your entire document library previews; ranger uninstalled.
### Sprint 8 — Media & async (20 h) 🎯 **M2c**
- **App deliverable**: video thumbnail (spawn `ffmpeg -ss … -vframes 1`), audio metadata +
cover art (`ffprobe` JSON), **background preview pipeline**: std threads + mpsc now,
generation-counter cancellation, large-directory pagination; adopt **tokio** for the
pipeline by sprint end (ADR-005).
- **Rust focus**: `std::thread`, `mpsc`, `Arc`/`Mutex`, then async/await, `tokio::spawn`,
`select!`, cancellation patterns. Rust Book ch. 16 + async book chapters (English only!).
- **English**: article "Async Rust for a skeptic" — explain threads-vs-tokio in your own words.
- **DoD**: 4K-video directory scrolls at full speed; huge directory never blocks input.
### Sprint 9 — Script plugin protocol (20 h) 🎯 **M3a: extensible**
- **App deliverable**: freeze the **external plugin contract** (ADR-002 phase 2): previewer
argv `(path, w, h, x, y)` + env context + FIFO write-back + `cleaner` hook; documented
exit codes & SIGPIPE; opener scripts; `plugins/` dir discovery; two reference plugins
(bash: torrent info; python: git repo summary).
- **Rust focus**: process spawning & pipes, timeouts, safe command construction (no shell
injection), protocol design & semver, integration tests with fixture scripts.
- **English**: **plugin author guide** — the sprint's main artifact; write it as if for strangers.
- **DoD**: a script written only from the guide (no source peeking) works.
### Sprint 10 — Power features (20 h) 🎯 **M3b: feature parity with your ranger config**
- **App deliverable**: tabs, bookmarks + marks, fuzzy finder (nucleo) with **interruptible**
search (broot lesson), `:commands` set (`:find :delete :copy…`), `push` key-simulation,
shell integration `--choosefile/--choosedir/--selectfile`, cd-on-exit wrapper script,
session restore.
- **Rust focus**: state machines (modes), Rc-free architecture review, `macro_rules!` for
keymap tables, API refactoring under tests.
- **English**: article "Vim-style UX patterns in file managers".
- **DoD**: your shell rc sources the viexp wrapper; fuzzy find feels instant.
### Sprint 11 — Performance & robustness (20 h)
- **App deliverable**: `notify`-based live directory updates, virtual scrolling, startup
<50 ms, one-week dogfood with zero panics, `tracing` logs to file, panic hook restores
the terminal, criterion benchmarks committed, crash-fix buffer.
- **Rust focus**: profiling (`cargo flamegraph`, `perf`), benchmarking (criterion), reading
someone else's code (yazi's) for patterns, hardening.
- **English**: article "Profiling a TUI app: making viexp start in 40 ms".
- **DoD**: benchmark numbers in README; a week of daily use logged.
### Sprint 12 — Release & docs (20 h) 🎯 **M4: v0.1.0 public**
- **App deliverable**: `v0.1.0` — cargo-dist release builds, AUR PKGBUILD, Gitea release +
changelog, **user guide** (mdBook in-repo), plugin examples gallery, install docs.
- **Rust focus**: release engineering, MSRV policy, feature matrix testing, rustdoc comments
on the public plugin API.
- **English**: the user guide is the largest English artifact of the project; polish it hard.
- **DoD**: a stranger installs from AUR and writes a plugin using only the docs.
### Backlog (post-1.0, priority order)
Embedded Lua plugins (mlua, ADR-002 phase 3) · sixel/iTerm2/ueberzug++ adapters · DDS-style
inter-instance pub/sub · `NNN_FIFO`-style hover event stream · themes gallery · zoxide jump ·
archive mount/browsing depth.
## Milestones
| Milestone | After sprint | Meaning |
|---|---|---|
| M1 | 3 | daily driver for basic navigation/file ops |
| M2a–c | 6–8 | full ranger replacement (images, docs, media) |
| M3a–b | 9–10 | extensible by scripts, feature parity |
| M4 | 12 | v0.1.0 released publicly |
At 45 min/day this is roughly a 12–13 month journey to v0.1.0 — the point is a working
product **and** durable skills, in that order, every single sprint.
## Cadence & safety nets
- Sprint notes live in `docs/sprints/`; each sprint's DoD becomes Gitea issues (milestone per
sprint) — see [[workflows/gitea-workflow|Gitea workflow]].
- If a sprint overruns by >30%, cut scope from the bottom of its DoD — never silently skip tests.
- One "buffer week" is allowed between sprints; more than one means the sprint was too big —
re-decompose in the retro.
+57
View File
@@ -0,0 +1,57 @@
---
type: sprint
number: 0
status: active
started: 2026-10-17
finished:
hours-planned: 6
hours-logged: 2
tags: [sprint, setup]
---
# Sprint 0 — Setup & deconstruction
## Theme
Remove every barrier to practice: toolchain, fast feedback, project board, CI. After this
sprint, a practice session starts in 60 seconds and never touches configuration.
## App deliverable
`viexp` cargo project skeleton that compiles, one trivial test green, Gitea Actions CI green,
`bacon` watching check+clippy+test on save, Gitea board (project + labels + milestones) ready.
## Rust focus — the vital 20%
- What `rustc` / `cargo` / `crates.io` / `Cargo.toml` / `Cargo.lock` are and who owns which file.
- The compile cycle: `cargo check` (fast) vs `build` vs `run`; `cargo fmt`, `cargo clippy`.
- Reading a compiler error: error code (e.g. E0382), span, help hint — where to look first.
## Practice drills (20–30 min each)
- [ ] D1 — Install/verify: `rustup default stable`, `cargo --version`; run `rustup component
add clippy rustfmt`; install `bacon` (`cargo install --locked bacon`).
- [ ] D2 — `cargo new viexp --bin`, add a `lib.rs` skeleton (src split: `main.rs` calls
`viexp::run()`), one `#[test] fn sanity()` — make it fail, read the output, make it pass.
- [ ] D3 — Break it on purpose: introduce E0382 (move a `String`, then use it), E0308 (wrong
type), E0384 (reassign immutable). Paste each into the ErrorJournal **with a self-correction cue**.
- [ ] D4 — Gitea CI: `.gitea/workflows/ci.yml` running `fmt --check`, `clippy -D warnings`,
`test`. Push and watch it go green.
- [x] D5 — Gitea board: labels, milestone "Sprint 1", issues imported from Sprint-1 DoD.
(Done 2026-10-17: 11 labels, 13 sprint milestones, issues #1–#12; kanban board still needs
one-time manual creation in the web UI — Gitea 1.26.2 has no projects API.)
## English focus
README "Status" section (done), DevLog habit starts today, commit messages from now on:
Conventional Commits, imperative mood.
## Session rhythm (45 min)
5 min review → 30 min drill → 10 min DevLog + ErrorJournal.
## Definition of done
- [ ] `cargo bacon` shows check+test passing on save
- [ ] CI green on `main`
- [ ] ErrorJournal has ≥3 entries with cues
- [ ] Sprint-1 milestone and issues exist on Gitea
- [ ] Retro filled
## Retrospective
### Shipped
### Frustrations
### Hours logged / English notes
+58
View File
@@ -0,0 +1,58 @@
---
type: sprint
number: 1
status: planned
started:
finished:
hours-planned: 20
hours-logged: 0
tags: [sprint, rust-basics]
---
# Sprint 1 — Rust core via `vls`
## Theme
Learn 80% of everyday Rust syntax by writing a real tool: `viexp ls`, a better `ls`.
## App deliverable
`viexp ls [PATH]` with: colored entries by type (dir/symlink/exec/image/archive), human sizes
(`1.4 K`, `2.1 M`), `--sort name|size|mtime`, `--hidden` toggle, `--long` (perms, dates),
`--json` (for tests), ≥10 unit tests, clippy clean.
## Rust focus — the vital 20%
Variables & shadowing, scalar types, functions, `if let`/`match`, loops & labels, `Vec`,
`String` vs `&str`, structs + methods, enums + `match` exhaustiveness, `Option`/`Result`,
the `?` operator, modules & `use`, `#[cfg(test)]` + `#[test]`.
Resources: Rust Book ch. 1–6, 7 (modules), 9 (errors) — **in English only**; rustlings
exercises `intro…functions…if…quiz1…enums…options…errors1-3`.
## Practice drills (20–30 min each — target ≈2 per day in week 1, features in week 2+)
- [ ] D1 — `read_dir` → print bare names; handle the `Result` with `?` and `anyhow` at `main`.
- [ ] D2 — `FileEntry` struct: name, is_dir, is_symlink, size, mtime; build with `metadata()`.
- [ ] D3 — Human size formatting fn + unit tests (B→K→M→G boundary values).
- [ ] D4 — Sorting: implement 3 comparators, `--sort` flag parsing (no clap yet — `std::env`).
- [ ] D5 — Colors: ANSI escape codes by hand (no crate) — learn what crates hide from you.
- [ ] D6 — `--hidden` filter (dotfiles) + tests.
- [ ] D7 — `--long` view: permissions string (`rwxr-x---`), date formatting.
- [ ] D8 — Refactor into `src/entry.rs`, `src/render.rs`, `src/cli.rs` — modules & `pub`.
- [ ] D9 — `--json` output via serde; golden-file test.
- [ ] D10 — Adopt `clap` for flags (derive API) — your first "big" crate.
- [ ] D11 — rustlings: enums + options + errors tracks.
- [ ] D12 — Review week: read your own code, note 3 things clippy taught you.
## English focus
README usage section; draft article "Why build a file manager in Rust" (150–200 words is enough).
## Session rhythm (45 min)
5 min review → 30 min drill → 10 min DevLog + ErrorJournal.
## Definition of done
- [ ] `viexp ls ~/` is nicer than `ls` for your own eyes
- [ ] ≥10 tests green, clippy clean, CI green
- [ ] Article draft published in `docs/articles/`
- [ ] Retro filled; Sprint-2 milestone issues created
## Retrospective
### Shipped
### Frustrations
### Hours logged / English notes
@@ -0,0 +1,27 @@
---
type: task
status: todo
priority: P0
sprint: 0
gitea-issue: "#1"
created: 2026-10-17
due:
tags: [task, ci]
---
# Bootstrap cargo project and CI
## Goal
A compiling `viexp` crate with lib/bin split, one passing test, and green Gitea Actions CI.
## Acceptance criteria
- [ ] `cargo new`-style skeleton: `src/main.rs` + `src/lib.rs` (`main` calls `viexp::run()`)
- [ ] `cargo test` green with one sanity test
- [ ] `.gitea/workflows/ci.yml`: `fmt --check` + `clippy -D warnings` + `test` on push
- [ ] First Conventional Commit pushed to `main`
## Notes
Rust Book ch. 1–2; see [[sprints/sprint-00-setup|Sprint 0]] drill D2/D4.
## Log
- 2026-10-17 — task created.
@@ -0,0 +1,27 @@
---
type: task
status: todo
priority: P0
sprint: 0
gitea-issue: "#3"
created: 2026-10-17
due:
tags: [task, tooling]
---
# Verify Rust toolchain and fast-feedback loop
## Goal
`bacon` runs check + clippy + test on every save, so no practice minute is wasted on manual rebuilds.
## Acceptance criteria
- [ ] `rustup default stable`; `rustc --version` recorded in DevLog
- [ ] `clippy` and `rustfmt` components installed
- [ ] `bacon` installed and configured (check + test jobs)
- [ ] Editor shows inline errors (rust-analyzer) — verify once
## Notes
Command reference: `rustup component add clippy rustfmt`; `cargo install --locked bacon`.
## Log
- 2026-10-17 — task created.
+36
View File
@@ -0,0 +1,36 @@
# English workflow
The project documentation is 100% English on purpose: the repo is an immersion environment.
Rust is learned through English, English through Rust.
## Daily habits (zero extra time)
- **Commit messages in English** — Conventional Commits format forces short, structured sentences.
- **DevLog entry** (10 min, last step of every session): 3–5 sentences about what you did,
in English. Don't translate from Russian in your head — describe the code, not the feelings.
- **Read error messages and docs in English only.** Never switch the compiler or Rust Book to Russian.
## Weekly habits
- **One article per sprint** in `articles/` (comparison, tutorial, retro) — reviewed for
language quality, not only content. Keep sentences short; prefer active voice.
- **Glossary growth**: every new term goes to `learning/glossary.md` with your own example
sentence. Re-using a term in your own sentence is what makes it stick.
- Weekly self-check: read Monday's DevLog aloud — if you can't, the sentence was too complex.
## What gets reviewed
| Artifact | Language focus |
|----------|----------------|
| Commit messages | imperative mood, conciseness |
| DevLog | past simple, describing actions |
| Issues / task notes | acceptance criteria phrasing ("must exist when done") |
| Articles | structure, connectors, technical vocabulary |
| Code comments & rustdoc | explaining "why", not "what" |
## Rules of thumb
1. Simple sentences first; compound later. Clarity beats sophistication.
2. If you needed a dictionary twice for one sentence — rewrite the sentence, not the vocab.
3. Write first, polish after; never let language anxiety stop the log entry.
4. Track words you *meet* vs words you *use* — the second list is the real progress.
+40
View File
@@ -0,0 +1,40 @@
# Gitea workflow
The project is tracked at **https://git.v-port.ru/vaskes/viexp** (Gitea 1.26.2).
CLI access: `tea` (logged in as `vaskes`); API access with the saved token.
## Verified capabilities (2026-10-17)
| Feature | Via API? | Notes |
|---------|----------|-------|
| Labels | ✅ | 11 labels created (`type/*`, `priority/*`, `lang/english`) |
| Milestones | ✅ | 13 sprint milestones with due dates |
| Issues | ✅ | labels by **ID**, not by name (`POST /repos/vaskes/viexp/issues`) |
| Project board (kanban) | ❌ | endpoint missing in Gitea 1.26.2 — create once via web UI |
| Actions CI | ✅ | `.gitea/workflows/ci.yml` (repo has `has_actions: true`) |
## Organization
- **Project board** ("viexp development", created manually in the web UI):
columns `Backlog → In Progress → Review → Done`; sprints are tracked by milestones.
- **Milestones** = sprints: `Sprint 0 — Setup & deconstruction` … `Sprint 12 — Release & docs`
(one 20-hour sprint ≈ one month at ~45 min/day). Due dates are targets, not deadlines.
- **Labels**: `type/bug|feature|docs|research|chore|spike`, `priority/P0…P3`, `lang/english`.
- **Issues** are created from task notes (`docs/tasks/`) and sprint definitions-of-done;
the task note's `gitea-issue` property stores the issue number.
- Bootstrap/repeat script: `scripts/gitea-bootstrap.sh` (labels + milestones + seed issues).
## Rules
1. One issue = one demoable outcome; acceptance criteria copied from the task note.
2. Branch per issue: `sprint-N/slug` or `feat/slug`; squash-merge into `main`.
3. Commit messages: Conventional Commits **in English** (`feat(preview): add kitty graphics support`)
— every commit is a micro English exercise.
4. An issue is closed only when its acceptance criteria are demoable.
5. Sprint retro (from the sprint note) is posted as a comment on the milestone's closing issue.
## Automation notes
- Gitea Actions can run `cargo fmt --check && cargo clippy -- -D warnings && cargo test` on push.
- Everything set up via API/`tea` is documented in `docs/workflows/` so it can be re-created
from scratch (the setup commands are in the sprint-0 task note).
+41
View File
@@ -0,0 +1,41 @@
# Research workflow
How research artifacts are collected, analyzed and turned into decisions — the pipeline that
feeds every architectural choice in viexp.
## Lifecycle
```
question → TMP/Research.md note (status: inbox)
→ sources collected (table filled)
→ findings summarized (status: in-progress)
→ synthesis article in articles/ (status: analyzed)
→ decision in decisions/ (ADR) + tasks/issues
→ archived (status: archived) once superseded
```
1. **Capture.** Any open question gets a `Research` note immediately (title = the question
domain, e.g. `djvu-preview-options`). One question per note — small notes beat monoliths.
2. **Collect.** Every source goes into the sources table with a one-line takeaway.
Screenshots and PDFs go to `attachments/`; paste links, not walls of copied text.
3. **Analyze.** Distill findings into bullets; mark `confidence: low|medium|high`.
Open questions move to follow-up notes, not into the article.
4. **Synthesize.** Cross-note comparisons become `Article` notes (e.g. the file-manager
comparison). The article states recommendations, not just facts.
5. **Decide.** Every recommendation that changes the architecture becomes an ADR
(`Decision` template) — short, dated, linked back to the research notes.
6. **Act.** ADRs spawn `Task` notes → Gitea issues (see [[workflows/gitea-workflow|Gitea workflow]]).
## Weekly review (Friday, 15 min)
- Process all `status: inbox` notes: promote, merge or archive.
- Update ADRs whose status changed during the week.
- Check that every active sprint task has a linked issue and vice versa.
## Obsidian mechanics used
- **Templates** (`TMP/`) keep structure uniform; headings are predictable, so folding works.
- **Properties** (`type`, `status`, `tags`, `sprint`) power the tag pane and search, e.g.
`["type": "research", "status": "inbox"]`.
- **Backlinks / graph** show which decisions depend on which research.
- **Attachments** are centralized in `docs/attachments/` (set in Obsidian settings).
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# viexp Gitea bootstrap — create labels, sprint milestones and seed issues.
# Re-runnable: existing items are detected and reused.
# Auth: reads the token saved by `tea login add` from ~/.config/tea/config.yml.
# NOTE (Gitea 1.26.2): the repo *project board* has no API on this instance —
# create "viexp development" once via the web UI (Issues → Projects).
set -u
TOKEN=$(grep -E '^\s*token:' ~/.config/tea/config.yml | head -1 | sed -E 's/^\s*token:\s*//; s/"//g')
API=https://git.v-port.ru/api/v1
REPO=vaskes/viexp
TZOFF=$(date +%:z)
req() { local m=$1 p=$2 d=${3:-}
if [ -n "$d" ]; then curl -s -X "$m" "$API$p" -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' -d "$d"
else curl -s -X "$m" "$API$p" -H "Authorization: token $TOKEN"; fi
}
echo "== labels =="
declare -a LABELS=(
'type/bug|ee0701|Something broken in shipped code'
'type/feature|84b6eb|New app capability'
'type/docs|d4c5f9|Documentation and vault work'
'type/research|fad8c7|Investigation / spike output expected'
'type/chore|ededed|Tooling, CI, maintenance'
'type/spike|fef2c0|Time-boxed experiment'
'priority/P0|b60205|Blocks the sprint'
'priority/P1|d93f0b|Important this sprint'
'priority/P2|fbca04|Normal'
'priority/P3|0e8a16|Nice to have'
'lang/english|5319e7|English-practice artifact'
)
for L in "${LABELS[@]}"; do
IFS='|' read -r name color desc <<< "$L"
req POST "/repos/$REPO/labels" "{\"name\":\"$name\",\"color\":\"$color\",\"description\":\"$desc\"}" >/dev/null
done
req GET "/repos/$REPO/labels" | jq -r '.[].name' | tr '\n' ' '; echo
echo "== milestones (sprints; see docs/roadmap.md) =="
create_ms() { # title description due
local existing
existing=$(req GET "/repos/$REPO/milestones?state=all" | jq -r --arg t "$1" '.[] | select(.title==$t) | .id')
if [ -n "$existing" ] && [ "$existing" != "null" ]; then echo "$existing"; return; fi
req POST "/repos/$REPO/milestones" "{\"title\":\"$1\",\"description\":\"$2\",\"due_on\":\"$3\"}" | jq -r '.id // empty'
}
S0="Sprint 0 — Setup & deconstruction"
S1="Sprint 1 — Rust core via vls"
create_ms "$S0" "Toolchain, CI, board, vault. ~6h." "2026-10-31T23:59:59$TZOFF" >/dev/null
create_ms "$S1" "viexp ls subcommand: colors, sizes, sorting, tests. 20h." "2026-11-30T23:59:59$TZOFF" >/dev/null
req GET "/repos/$REPO/milestones?state=all" | jq -r '.[] | "\(.id) \(.title)"'
echo "== issues =="
bash "$(dirname "$0")/gitea-issues.sh"
echo "== project board =="
echo "No API on Gitea 1.26.2 — create 'viexp development' board in the web UI (Issues → Projects), columns:"
echo " Backlog → In Progress → Review → Done (sprints tracked by milestones)"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Fix-up: create issues with label IDs; diagnose projects API.
set -u
TOKEN=$(grep -E '^\s*token:' ~/.config/tea/config.yml | head -1 | sed -E 's/^\s*token:\s*//; s/"//g')
API=https://git.v-port.ru/api/v1
REPO=vaskes/viexp
req() { local m=$1 p=$2 d=${3:-}
if [ -n "$d" ]; then curl -s -X "$m" "$API$p" -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' -d "$d"
else curl -s -X "$m" "$API$p" -H "Authorization: token $TOKEN"; fi
}
echo "== raw projects GET (first 200 chars) =="
req GET "/repos/$REPO/projects" | head -c 200; echo; echo
declare -A LMAP MMAP
while IFS=$'\t' read -r id name; do LMAP[$name]=$id; done < <(req GET "/repos/$REPO/labels" | jq -r '.[] | "\(.id)\t\(.name)"')
while IFS=$'\t' read -r id title; do MMAP[$title]=$id; done < <(req GET "/repos/$REPO/milestones?state=all" | jq -r '.[] | "\(.id)\t\(.title)"')
EXISTING=$(req GET "/repos/$REPO/issues?state=all&type=issues")
create_issue() { # title body milestone_title labels_csv -> $1 $2 $3 $4
local exist
exist=$(echo "$EXISTING" | jq -r --arg t "$1" '.[]? | select(.title==$t) | .number')
if [ -n "$exist" ] && [ "$exist" != "null" ]; then echo "#$exist (existing) $1"; return; fi
local ids=() n
IFS=',' read -ra names <<< "$4"
for n in "${names[@]}"; do [ -n "$n" ] && ids+=("${LMAP[$n]}"); done
local ids_json
ids_json=$(printf '%s\n' "${ids[@]}" | jq -R -s 'split("\n") | map(select(length>0) | tonumber) | map(select(. != null))')
local ms=${MMAP[$3]:-}
local body
if [ -n "$ms" ]; then
body=$(jq -n --arg t "$1" --arg b "$2" --argjson ms "$ms" --argjson ids "$ids_json" '{title:$t, body:$b, milestone:$ms, labels:$ids}')
else
body=$(jq -n --arg t "$1" --arg b "$2" --argjson ids "$ids_json" '{title:$t, body:$b, labels:$ids}')
fi
local num
num=$(req POST "/repos/$REPO/issues" "$body" | jq -r '.number // "FAIL \(.message)"')
echo "#$num $1"
}
MS0="Sprint 0 — Setup & deconstruction"
MS1="Sprint 1 — Rust core via vls"
create_issue "Bootstrap cargo project skeleton (lib/bin split + sanity test)" "From docs/tasks/0001. Acceptance: src/main.rs + src/lib.rs (main calls viexp::run()), cargo test green, first conventional commit pushed." "$MS0" "type/chore,priority/P0"
create_issue "Add Gitea Actions CI: fmt --check + clippy -D warnings + test" ".gitea/workflows/ci.yml on push to main; green run required." "$MS0" "type/chore,priority/P0"
create_issue "Install bacon and verify fast-feedback loop" "cargo install --locked bacon; bacon runs check+test on save; rust-analyzer inline errors verified once." "$MS0" "type/chore,priority/P0"
create_issue "ErrorJournal: 3 deliberate compiler errors with self-correction cues" "Break code on purpose (E0382, E0308, E0384), record in docs/learning with cues. Learning artifact." "$MS0" "type/docs,priority/P1,lang/english"
create_issue "vls: read directory and print entries" "fs::read_dir + anyhow at main boundary. Sprint-1 drill D1." "$MS1" "type/feature,priority/P0"
create_issue "vls: FileEntry struct with metadata" "name, is_dir, is_symlink, size, mtime. Drill D2." "$MS1" "type/feature,priority/P0"
create_issue "vls: human-readable sizes + unit tests" "B/K/M/G boundary tests. Drill D3." "$MS1" "type/feature,priority/P1"
create_issue "vls: sorting modes (name|size|mtime) via flags" "std::env parsing first, clap later. Drill D4." "$MS1" "type/feature,priority/P1"
create_issue "vls: ANSI colors by file type (no crate)" "Hand-rolled escape codes. Drill D5." "$MS1" "type/feature,priority/P2"
create_issue "vls: --hidden toggle and --long view" "Dotfile filter; rwx perms string; dates. Drills D6-D7." "$MS1" "type/feature,priority/P2"
create_issue "vls: modularize + --json + clap derive" "entry/render/cli modules, serde golden-file test. Drills D8-D10." "$MS1" "type/chore,priority/P1"
create_issue "Article: Why build a file manager in Rust (draft)" "150-200 words, English, docs/articles/. Sprint-1 English artifact." "$MS1" "type/docs,priority/P2,lang/english"
echo "== done =="