Preview plugin API gaps found while building mermaid.yazi
Hi! I've been building a yazi preview plugin ([passion0102/mermaid.yazi](https://github.com/passion0102/mermaid.yazi)) that previews `.md` / `.mmd` files with inline mermaid diagram rendering. In the process I hit a few API limitations that seem worth surfacing as a single thread so we can discuss direction. Happy to scope down and submit a PR for any of these once we agree.
The plugin is a markdown previewer registered via `prepend_previewers` for `*.md` / `*.mmd`. It composites glow-rendered text on top and a mermaid PNG (fetched from mermaid.ink, or rendered locally by mermaid-cli) at the bottom via `ya.preview_widget` + `ya.image_show`.
---
## 1. Re-emitting `ya.image_show` per peek flickers under continuous scroll
Every `M:peek` call ends with `ya.image_show(url, area)` because the plugin can't tell from inside the isolate whether the same image is already on-screen. Under trackpad / `Shift+J/K` scrolling this fires the image-protocol transfer many times per second and the terminal visibly flickers.
I tried two workarounds:
- **Module-local dedup variable** (a `local _last_image_key` remembering the last `(cache_path, w, h, mode)` and skipping `ya.image_show` when it matches). The dedup never fires — the variable seems to reset between peek invocations, suggesting each peek runs in a fresh Lua state. Confirmed via `ya.dbg` instrumentation: the dedup-skip log line was never written.
- **`/tmp` file-backed dedup**. Works across peeks but leaves stale frames after switching files or modes — got correctly flagged in adversarial review as a must-fix because there's no signal to invalidate.
What would help:
- Either a yazi-side `ya.image_show` that no-ops when called with an identical `(url, area)` payload, OR
- A way for plugins to learn "is this image currently on screen at this area" so we can skip the call safely.
Timing from a real session: `image-show.composed` is ~8 ms per peek while everything else (file read, parse, glow cache hit) is sub-millisecond. So this single call dominates plug-in CPU when scrolling.
## 2. `M:preload` is not invoked for markdown previewers
I implemented `M:preload(job)` to warm caches (glow ANSI output + mermaid images for every block in the file) before the user starts scrolling. It is never called for my plugin — `ya.dbg` recorded zero invocations across long preview sessions.
The built-in `image.lua` does use `M:preload`, and the docs list it as a generic plugin lifecycle hook, so I assume it's currently gated on the image previewer category. Could it be invoked for any `prepend_previewers` match? The use case is the canonical one preload exists for: a fetch / render step that's fine to do asynchronously before the first peek.
## 3. Inline image placement (image at the source-anchored row of a text widget)
The plugin currently renders text full-width with a mermaid image pinned to the bottom of the preview area. The natural shape would be to put the image *where the mermaid block sits in the rendered text*, not at the bottom.
I prototyped this by computing a placeholder line, then calling `ya.image_show(url, ui.Rect{x, y, w, h})` at the computed `y`. Two problems:
- Multiple `image_show` calls per peek flicker more than one call (terminal protocol behavior).
- When the image area shrinks between frames, **stale pixels remain** at the previous `y` position — there's no `ya.image_clear(area)` to wipe them.
An explicit `ya.image_clear(area)` (or equivalent) would unlock the inline-embedded layout that markdown editors typically have.
## 4. Per-session cwd / id for plugin state isolation
Two minor ones, lower priority:
- I use `/tmp/mermaid-yazi-mode-<hash>` files to remember preview mode and zoom step. To avoid collisions across yazi sessions I hash `$PWD` at module load time, but yazi-internal `cd` doesn't update `$PWD`, so the suffix is effectively per-process-start, not per-cwd. A `ya.cwd()` or session id accessible from the preview isolate would let plugins keep this state genuinely scoped.
- Concurrent peeks across multiple yazi processes share the same tmp directory and the same `os.time() + math.random()` seed pattern, so two peeks could pick the same tmp filename in the same second. `ya.session_id()` or even exposing the process id would let us make tmp filenames collision-free.
---
Happy to scope this down to whichever item matches the project's direction and submit a PR. Please let me know if I should split into separate issues, or if any of the above already has a tracking thread I missed.
关闭于 2026-05-19 1 条评论