diff --git a/README.md b/README.md index 5e192f4..53befc1 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,66 @@ set keeps the default. Both files are needed — `theme.toml` points at the `.tmTheme` for syntax highlighting in file previews. +## Sharper previews (optional) + +Not part of the colour scheme, but it belongs in the same corner of +`~/.config/yazi` — the files are under `preview/`. + +```sh +cp preview/yazi.toml ~/.config/yazi/yazi.toml +cp -r preview/plugins/pdf-hidpi.yazi ~/.config/yazi/plugins/ +ya cache clear +``` + +### Why PDFs look mushy by default + +Two bottlenecks stack up: + +1. Yazi calls `pdftoppm` **without `-r`**, so pages render at poppler's 150 DPI + default — an A4 page is 1241x1754 px. +2. `[preview] max_width` / `max_height` default to **600x900**. The page is + squeezed to 600 px wide and then scaled back up to fill the pane. + +On top of that the page is JPEG-compressed at quality 75, which smears glyph +edges. + +`preview/yazi.toml` raises the cap to 1600x2400, and the `pdf-hidpi` plugin is +Yazi's own PDF previewer with two changes: `-r 300` and PNG instead of JPEG. +The page now renders at 2481x3508 and is downscaled into the cap, so the result +is supersampled rather than upscaled — and lossless where the glyphs are. + +**Both the previewer and the preloader must be overridden.** They write the +same cache file, so replacing only one leaves the 150 DPI version in place. +`preview/yazi.toml` does both. + +Yazi has no zoom. What it has is `J` / `K`, which turn PDF pages in the +preview, and whatever key you bound to maximise the preview pane. + +### Videos + +The video preview is **one still frame** by design: `ffmpeg` pulls a keyframe +at 5 % of the runtime and Yazi shows it as an image. Nothing animates, and no +setting changes that. + +What does work: + +| Key | | +|---|---| +| `J` / `K` | scrub the preview through the file, ±5 % per press | +| `Tab` | spot panel — codec, resolution, duration | +| `Enter` / `o` | open in an mpv window | +| `O` | pick an opener — including **Play here (Kitty)** | + +The last one is the `[opener]` block in `preview/yazi.toml`: it runs +`mpv --vo=kitty --vo-kitty-use-shm=yes`, which draws the video into the +terminal pane through the Kitty graphics protocol. Yazi blocks until mpv +exits (`q`). Shared-memory transfer keeps it watchable; without it every frame +goes through the tty as base64 and it crawls. It is still a terminal doing a +framebuffer's job — fine for checking what a clip is, not for watching it. + +Defining `play` replaces Yazi's preset list rather than merging into it, which +is why mpv appears twice there: once windowed, once inline. + ## Keeping the icon table current The `[icon]` block **replaces** Yazi's preset table wholesale. Icons added by @@ -91,6 +151,11 @@ script prints which Gruvbox tone each source colour landed on. `regen-tmtheme.py` does the same for `gruvbox-dark.tmTheme` — edit the palette or the scope rules at the top of the script and re-run it. +Both generators and the `pdf-hidpi` plugin target the **26.9** plugin API. If +you build against Yazi's `main` branch instead of the tag you run, you will hit +renamed functions — `fs.cha()` was `fs.stat()` on main — and the error only +surfaces at preview time. + ## Self-contained Everything needed is in this repository — nothing has to be fetched from a diff --git a/preview/plugins/pdf-hidpi.yazi/main.lua b/preview/plugins/pdf-hidpi.yazi/main.lua new file mode 100644 index 0000000..7b873b0 --- /dev/null +++ b/preview/plugins/pdf-hidpi.yazi/main.lua @@ -0,0 +1,73 @@ +-- pdf-hidpi: like Yazi's built-in `pdf` previewer, but renders at a higher +-- resolution and as PNG instead of JPEG. +-- +-- The built-in one calls `pdftoppm` without `-r`, so pages come out at +-- poppler's 150 DPI default (A4 = 1240x1754) and are then JPEG-compressed. +-- On a large or a portrait pane that is not enough pixels for body text, and +-- JPEG ringing around glyphs makes it worse. +-- +-- Rendering above the target size and letting `image_precache` downscale gives +-- a supersampled, much sharper result. PNG keeps glyph edges clean. +-- +-- Raise DPI for even sharper text at the cost of render time and cache size. + +local DPI = 300 + +local M = {} + +function M:peek(job) + local start, cache = os.clock(), ya.file_cache(job) + if not cache then + return + end + + local ok, err, bound = self:preload(job) + if bound and bound > 0 then + return ya.emit("peek", { bound - 1, only_if = job.file.url, upper_bound = true }) + elseif not ok or err then + return ya.preview_widget(job, err) + end + + ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock())) + + local _, err = ya.image_show(cache, job.area) + ya.preview_widget(job, err) +end + +function M:seek(job) + local h = cx.active.current.hovered + if h and h.url == job.file.url then + local step = ya.clamp(-1, job.units, 1) + ya.emit("peek", { math.max(0, cx.active.preview.skip + step), only_if = job.file.url }) + end +end + +function M:preload(job) + local cache = ya.file_cache(job) + if not cache or fs.cha(cache) then + return true + end + + -- stylua: ignore + local output, err = Command("pdftoppm") + :arg({ + "-f", job.skip + 1, + "-l", job.skip + 1, + "-singlefile", + "-r", DPI, + "-png", + tostring(job.file.path), tostring(cache), + }) + :output() + + if not output then + return true, Err("Failed to start `pdftoppm`, error: %s", err) + elseif not output.status.success then + local pages = job.skip > 0 and tonumber(output.stderr:match("the last page %((%d+)%)")) + return true, Err("Failed to convert PDF to image, stderr: %s", output.stderr), pages + end + + return ya.image_precache(Url(cache .. ".png"), cache) +end + +return M diff --git a/preview/yazi.toml b/preview/yazi.toml new file mode 100644 index 0000000..2fa14ef --- /dev/null +++ b/preview/yazi.toml @@ -0,0 +1,38 @@ +#:schema https://yazi-rs.github.io/schemas/yazi.json + +[preview] +# Yazi caps every rendered preview image at these pixel dimensions before +# showing it. The default 600x900 is well below what the preview pane is worth +# on a large or a portrait screen, so PDFs and images arrive already +# downscaled and then get upscaled again — that is the blur. +max_width = 1600 +max_height = 2400 + +# Applies to the JPEGs the video previewer produces (PDFs go through pdf-hidpi +# below, which renders PNG and ignores this). +image_quality = 90 + +[plugin] +# Replace the built-in PDF previewer with a high-resolution variant. +# Both entries are needed: the preloader fills the same cache file the +# previewer reads, so overriding only one of them leaves the 150 DPI version +# in place. +prepend_previewers = [ + { mime = "application/pdf", run = "pdf-hidpi" }, +] +prepend_preloaders = [ + { mime = "application/pdf", run = "pdf-hidpi" }, +] + +[opener] +# Replaces the preset's `play` list (a list defined here is not merged). +# Audio and video use it via the built-in `[open]` rule for {audio,video}/*. +play = [ + # Enter / o — normal mpv window. + { run = "mpv %s", desc = "Play (mpv)", for = "unix", orphan = true }, + # O, then pick — plays inside this terminal via the Kitty graphics + # protocol. Blocks Yazi until mpv exits; q quits. + { run = "mpv --vo=kitty --vo-kitty-use-shm=yes %s", desc = "Play here (Kitty)", for = "unix", block = true }, + # Yazi's preset also offers mediainfo here; Tab (spot) already shows + # codec, resolution and duration, so it is left out. +]