feat: add preview tuning — hi-DPI PDF previewer and opener setup
preview/ holds a yazi.toml and a pdf-hidpi plugin that fix unreadable PDF previews. Yazi calls pdftoppm without -r (150 DPI) and then caps the image at 600x900, so pages are downscaled and upscaled again; the plugin renders at 300 DPI as PNG and the cap goes to 1600x2400, turning that into supersampling. Previewer and preloader both have to be overridden — they share the cache file. README documents this plus what video previews can and cannot do: the preview is a single ffmpeg keyframe by design, J/K scrubs it, and the opener offers mpv --vo=kitty with shared-memory transfer for inline playback. Targets the 26.9 plugin API (fs.cha, not main's fs.stat). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6dc8e1b3ae
commit
de59a5a904
3 files changed
+176
No files matched your search
@@ -74,6 +74,66 @@ set keeps the default.
|
|||||||
Both files are needed — `theme.toml` points at the `.tmTheme` for syntax
|
Both files are needed — `theme.toml` points at the `.tmTheme` for syntax
|
||||||
highlighting in file previews.
|
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
|
## Keeping the icon table current
|
||||||
|
|
||||||
The `[icon]` block **replaces** Yazi's preset table wholesale. Icons added by
|
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
|
`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.
|
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
|
## Self-contained
|
||||||
|
|
||||||
Everything needed is in this repository — nothing has to be fetched from a
|
Everything needed is in this repository — nothing has to be fetched from a
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
]
|
||||||
Reference in new issue
Block a user