Skip to content

feat: in-process markdown preview when cmux isn't running - #2

Merged
nero408 merged 1 commit into
mainfrom
feat/in-process-preview
May 18, 2026
Merged

feat: in-process markdown preview when cmux isn't running#2
nero408 merged 1 commit into
mainfrom
feat/in-process-preview

Conversation

@nero408

@nero408 nero408 commented May 18, 2026

Copy link
Copy Markdown
Owner

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.rsPreview 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.rsFileWatcher 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.

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]>
@nero408
nero408 merged commit b916cc5 into main May 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant