feat: in-process markdown preview when cmux isn't running - #2
Merged
Conversation
Make cmux optional. If a user doesn't have cmux installed (or passes
`--no-cmux`), mdmux now renders the markdown panel itself inside its own
ratatui surface — a horizontal split with the file tree on the left and
the rendered markdown on the right. For cmux users, nothing changes.
## Why
The previous launch story was "you need cmux running, or the TUI lets
you browse but Enter shows an error overlay." Setting up cmux is a
non-trivial install — gating an open-source release on it shrinks the
audience to "people who already use cmux", which is the wrong direction
for a Day-1 release. Glow is a fine standalone renderer, but spawning
it would either need its own pane (defeating the purpose) or popping
out of the TUI entirely. The right answer is to render inside our pane.
## What changed
### New modules
- `src/preview.rs` — `Preview` struct holds a snapshot of an open file;
`load()` reads from disk with a 1 MiB / 5000-line cap; `render()`
produces a `Text<'static>` via tui-markdown for ratatui. Size cap
protects us from accidentally pointing the binary at a giant log file.
- `src/watcher.rs` — `FileWatcher` wraps `notify` for live-reload. We
watch the parent directory (not the file itself) so editors that save
via rename-then-move don't break the watch. Events drain through a
channel that the event loop checks between key presses; no polling
drain when no preview is open.
### App state
- New `RenderMode` enum (`Cmux` | `InProcess`) on `App`. Decided at
startup based on `cmux ping` success + CLI flags. Defaults to `Cmux`.
- Replaced the demo-only `DemoPreview` / `demo_preview` field with a
general-purpose `preview: Option<Preview>` plus `preview_scroll: u16`.
`--demo` keeps its fake-surface-id behavior via the existing
`demo_mode` flag, but the in-process pane is now first-class.
- New `App::reload_preview`, `preview_scroll_{up,down,to_top,to_bottom}`.
### CLI
- New `--no-cmux` flag: force in-process even when cmux is available.
- Auto-fallback: when `cmux ping` fails we silently switch to
`InProcess` instead of popping the "cmux not running" modal. Status
line communicates which mode we ended up in.
### Rendering
- Dropped the bespoke 80-line markdown renderer in `src/ui.rs`. The
new `Preview::render()` uses `tui-markdown` (which uses
`pulldown-cmark` under the hood), giving us headings, lists,
blockquotes, fenced code blocks with syntect-powered syntax
highlighting, and inline emphasis for free.
- New `draw_preview` widget; the demo-pane code path now serves real
users, not just gif rendering.
### Keys
- `J` / `K` scroll the in-process preview down / up.
- `Ctrl-D` / `Ctrl-U` scroll a half-page.
- Help screen + README updated.
### Event loop
- Watcher integrates via a 200 ms poll timeout *only when InProcess mode
has a file open*. Cmux mode and idle in-process mode block forever as
before. Worst-case CPU on an idle in-process session is 5 wakeups/s
to check the watcher — cheap, and cmux users pay nothing.
## Dependencies
- `tui-markdown = "=0.3.6"` (pinned: 0.3.7+ bumps to the ratatui 0.30
workspace, which is a different `ratatui-core` crate and incompatible
with our 0.29).
- `notify = "8"` (stable 8.2.0).
Binary size: ~1.5 MiB → ~3.8 MiB. The extra mass is syntect's bundled
syntax/theme set, which is what gives us code-block highlighting.
## Tests
- 12 new unit tests (5 preview, 3 watcher, 4 app)
- `cargo test --lib`: **48 passed, 0 failed**
- `cargo clippy --all-targets --all-features -- -D warnings`: clean
- `cargo fmt --all -- --check`: clean
## Known transitive cargo-audit warnings — accepted
All four are `unmaintained` / `unsound` markers on transitive deps with
no exploit path for us:
- `paste 1.x` (ratatui dep, informational)
- `lru 0.12 IterMut` (ratatui internal; we don't call IterMut)
- `bincode 1.x` (syntect dep, 1.x stale but functional)
- `yaml-rust 0.4` (syntect dep, unmaintained)
All clear after upgrading to ratatui 0.30 + tui-markdown 0.3.7, which is
a separate, more invasive change best handled after the launch settles.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Make cmux optional. If a user doesn't have cmux installed (or passes
--no-cmux), mdmux now renders the markdown panel itself inside its own ratatui surface — a horizontal split with the file tree on the left and the rendered markdown on the right. For cmux users, nothing changes.Why
The previous launch story was "you need cmux running, or the TUI lets you browse but Enter shows an error overlay." Setting up cmux is a non-trivial install — gating an open-source release on it shrinks the audience to "people who already use cmux", which is the wrong direction for a Day-1 release. Glow is a fine standalone renderer, but spawning it would either need its own pane (defeating the purpose) or popping out of the TUI entirely. The right answer is to render inside our pane.
What changed
New modules
src/preview.rs—Previewstruct holds a snapshot of an open file;load()reads from disk with a 1 MiB / 5000-line cap;render()produces aText<'static>via tui-markdown for ratatui. Size cap protects us from accidentally pointing the binary at a giant log file.src/watcher.rs—FileWatcherwrapsnotifyfor live-reload. We watch the parent directory (not the file itself) so editors that save via rename-then-move don't break the watch. Events drain through a channel that the event loop checks between key presses; no polling drain when no preview is open.App state
RenderModeenum (Cmux|InProcess) onApp. Decided at startup based oncmux pingsuccess + CLI flags. Defaults toCmux.DemoPreview/demo_previewfield with a general-purposepreview: Option<Preview>pluspreview_scroll: u16.--demokeeps its fake-surface-id behavior via the existingdemo_modeflag, but the in-process pane is now first-class.App::reload_preview,preview_scroll_{up,down,to_top,to_bottom}.CLI
--no-cmuxflag: force in-process even when cmux is available.cmux pingfails we silently switch toInProcessinstead of popping the "cmux not running" modal. Status line communicates which mode we ended up in.Rendering
src/ui.rs. The newPreview::render()usestui-markdown(which usespulldown-cmarkunder the hood), giving us headings, lists, blockquotes, fenced code blocks with syntect-powered syntax highlighting, and inline emphasis for free.draw_previewwidget; the demo-pane code path now serves real users, not just gif rendering.Keys
J/Kscroll the in-process preview down / up.Ctrl-D/Ctrl-Uscroll a half-page.Event loop
Dependencies
tui-markdown = "=0.3.6"(pinned: 0.3.7+ bumps to the ratatui 0.30 workspace, which is a differentratatui-corecrate and incompatible with our 0.29).notify = "8"(stable 8.2.0).Binary size: ~1.5 MiB → ~3.8 MiB. The extra mass is syntect's bundled syntax/theme set, which is what gives us code-block highlighting.
Tests
cargo test --lib: 48 passed, 0 failedcargo clippy --all-targets --all-features -- -D warnings: cleancargo fmt --all -- --check: cleanKnown transitive cargo-audit warnings — accepted
All four are
unmaintained/unsoundmarkers on transitive deps with no exploit path for us:paste 1.x(ratatui dep, informational)lru 0.12 IterMut(ratatui internal; we don't call IterMut)bincode 1.x(syntect dep, 1.x stale but functional)yaml-rust 0.4(syntect dep, unmaintained)All clear after upgrading to ratatui 0.30 + tui-markdown 0.3.7, which is a separate, more invasive change best handled after the launch settles.