Inline terminal image rendering — Kitty graphics protocol / iTerm2 inline images.
The pixel-free logic (capability detection, intrinsic dimension sniffing,
cell-box sizing, escape encoding, base64/PNG transcoding) now lives in the
lanterna fork's Java class
com.googlecode.lanterna.terminal.image.TerminalImage. This namespace is the
thin Clojure adapter that keeps vis's map-shaped API ({:images …},
{:w :h}, {:cols :rows}) plus the attachment-aware paste probe, which
reaches back into vis internals and so stays here.
The escape strings are emitted DIRECTLY to the tty AFTER Lanterna's delta refresh (the screen loop owns that), placed over rows the renderer reserved as blanks. Lanterna never sees the graphics bytes, so its cell diff stays intact and the image survives subsequent delta frames.
Inline terminal image rendering — Kitty graphics protocol / iTerm2 inline
images.
The pixel-free logic (capability detection, intrinsic dimension sniffing,
cell-box sizing, escape encoding, base64/PNG transcoding) now lives in the
lanterna fork's Java class
`com.googlecode.lanterna.terminal.image.TerminalImage`. This namespace is the
thin Clojure adapter that keeps vis's map-shaped API (`{:images …}`,
`{:w :h}`, `{:cols :rows}`) plus the attachment-aware paste probe, which
reaches back into vis internals and so stays here.
The escape strings are emitted DIRECTLY to the tty AFTER Lanterna's delta
refresh (the screen loop owns that), placed over rows the renderer
reserved as blanks. Lanterna never sees the graphics bytes, so its cell
diff stays intact and the image survives subsequent delta frames.(attachment-cache-dir)Durable cache dir (~/.vis/cache/tui-attachments) for persisted image
attachments, so a re-rendered history image survives its original (often
OS-temp) source path vanishing. Created on demand.
Durable cache dir (`~/.vis/cache/tui-attachments`) for persisted image attachments, so a re-rendered history image survives its original (often OS-temp) source path vanishing. Created on demand.
(box-pixels cols rows)Long-edge PIXEL ceiling of a cols×rows cell box — what to ask a decoder
for when the result will be drawn into that box.
Decoding 1080p for an 80-column window produces about six times the pixels the cells can show: seconds of extra decode and tens of megabytes of extra escape bytes, all of it thrown away by the terminal's own downscale. The fork reads its LIVE cell metrics, so a HiDPI cell asks for a proportionally sharper picture.
Long-edge PIXEL ceiling of a `cols`×`rows` cell box — what to ask a decoder for when the result will be drawn into that box. Decoding 1080p for an 80-column window produces about six times the pixels the cells can show: seconds of extra decode and tens of megabytes of extra escape bytes, all of it thrown away by the terminal's own downscale. The fork reads its LIVE cell metrics, so a HiDPI cell asks for a proportionally sharper picture.
(cell-pixels)The terminal's current CELL size in pixels, {:w :h}.
The terminal's current CELL size in pixels, `{:w :h}`.
(cell-size {:keys [w h]} max-cols max-rows)Fit an image of {:w :h} px into max-cols × max-rows cells,
aspect-preserving. Returns {:cols :rows} (>= 1).
Fit an image of `{:w :h}` px into `max-cols` × `max-rows` cells,
aspect-preserving. Returns `{:cols :rows}` (>= 1).(detect-capabilities)(detect-capabilities env)Sniff which inline-image protocol the host terminal speaks from env
(defaults to the process environment). Returns {:images :kitty|:iterm2|nil}.
tmux and screen report nil — they don't reliably forward graphics.
Sniff which inline-image protocol the host terminal speaks from `env`
(defaults to the process environment). Returns `{:images :kitty|:iterm2|nil}`.
tmux and screen report `nil` — they don't reliably forward graphics.(encode-iterm2 data {:keys [cols]})iTerm2 \x1b]1337;File= inline-image sequence for data (base64 String or
RAW image bytes, base64'd into the escape without an intermediate String).
iTerm2 `\x1b]1337;File=` inline-image sequence for `data` (base64 String or RAW image bytes, base64'd into the escape without an intermediate String).
(encode-kitty data {:keys [cols rows crop-top crop-bottom img-w img-h]})Kitty graphics \x1b_G transmit+display sequence for data, sized to
cols×rows cells. C=1 keeps the cursor put after placement. When
crop-top/crop-bottom (cell rows scrolled past the band's top/bottom edge)
are positive, only the visible vertical slice is shown at native scale via the
Kitty source rectangle, sized from the transmitted image's img-w×img-h
pixel dimensions.
data is a base64 String OR the RAW PNG bytes. Raw bytes are base64'd one
chunk at a time straight into the escape by the fork, so the 1.33x base64
String never exists — the per-frame path for video playback, where the
payload changes every frame and nothing can be cached.
Kitty graphics `\x1b_G` transmit+display sequence for `data`, sized to `cols`×`rows` cells. `C=1` keeps the cursor put after placement. When `crop-top`/`crop-bottom` (cell rows scrolled past the band's top/bottom edge) are positive, only the visible vertical slice is shown at native scale via the Kitty source rectangle, sized from the transmitted image's `img-w`×`img-h` pixel dimensions. `data` is a base64 String OR the RAW PNG bytes. Raw bytes are base64'd one chunk at a time straight into the escape by the fork, so the 1.33x base64 String never exists — the per-frame path for video playback, where the payload changes every frame and nothing can be cached.
(graphical-terminal?)(graphical-terminal? env)Whether we're in a GRAPHICAL terminal — one that speaks an inline-image
protocol (kitty/Ghostty/WezTerm/Warp → Kitty, iTerm2 → iTerm2) — as opposed
to a plain/non-graphical terminal (or tmux/screen, which mangle graphics
pass-through). Callers branch on this to draw an inline image vs. a text-card
fallback. The 0-arity reuses the memoized capability detection; pass env to
probe an explicit environment (delegates to TerminalImage/isGraphicalTerminal).
Whether we're in a GRAPHICAL terminal — one that speaks an inline-image protocol (kitty/Ghostty/WezTerm/Warp → Kitty, iTerm2 → iTerm2) — as opposed to a plain/non-graphical terminal (or tmux/screen, which mangle graphics pass-through). Callers branch on this to draw an inline image vs. a text-card fallback. The 0-arity reuses the memoized capability detection; pass `env` to probe an explicit environment (delegates to `TerminalImage/isGraphicalTerminal`).
(image-dimensions b mime)Intrinsic {:w :h} pixel size from the leading bytes of an image, or nil.
Intrinsic `{:w :h}` pixel size from the leading bytes of an image, or nil.
(images-protocol):kitty, :iterm2, or nil for the current terminal.
`:kitty`, `:iterm2`, or nil for the current terminal.
(kitty-delete-placement id)Kitty sequence removing image id's placement while KEEPING its uploaded data,
so an image scrolled off screen leaves no ghost yet needs no re-upload if it
scrolls back into view.
Kitty sequence removing image `id`'s placement while KEEPING its uploaded data, so an image scrolled off screen leaves no ghost yet needs no re-upload if it scrolls back into view.
(kitty-free-image id)Kitty sequence deleting image id AND freeing its uploaded data — used when the
transmit cache evicts a long-off-screen image to bound terminal-side memory.
Kitty sequence deleting image `id` AND freeing its uploaded data — used when the transmit cache evicts a long-off-screen image to bound terminal-side memory.
(kitty-place {:keys [id cols rows crop-top crop-bottom img-w img-h]})Kitty a=p placement sequence for an ALREADY-transmitted image id at the
cursor: draw it into a cols×rows cell box, optionally cropped to the visible
vertical slice (crop-top/crop-bottom cell rows over an img-w×img-h px
image) via the protocol's source rectangle — the SAME x/y/w/h math the fork's
crop encodeKitty uses, because it is the same code. Reusing placement id p=1
REPLACES the prior placement, so a scroll moves the picture atomically: no
delete-all, no re-upload, no flash.
Kitty `a=p` placement sequence for an ALREADY-transmitted image `id` at the cursor: draw it into a `cols`×`rows` cell box, optionally cropped to the visible vertical slice (`crop-top`/`crop-bottom` cell rows over an `img-w`×`img-h` px image) via the protocol's source rectangle — the SAME `x/y/w/h` math the fork's crop `encodeKitty` uses, because it is the same code. Reusing placement id `p=1` REPLACES the prior placement, so a scroll moves the picture atomically: no delete-all, no re-upload, no flash.
(kitty-png path mime {:keys [width height] :as box})PNG payload + transmitted pixel dims {:data :w :h} for the Kitty wire. A PNG
file rides through verbatim — transmitted at its intrinsic width×height
(and works in the native image too); anything else is decoded and re-encoded
as a box-fitted PNG via com.blockether.imaging, and a VIDEO becomes its
poster frame.
:data is base64 text for the verbatim path (the fork caches that read) and
RAW PNG bytes for a transcode; every encoder here takes either.
PNG payload + transmitted pixel dims `{:data :w :h}` for the Kitty wire. A PNG
file rides through verbatim — transmitted at its intrinsic `width`×`height`
(and works in the native image too); anything else is decoded and re-encoded
as a box-fitted PNG via `com.blockether.imaging`, and a VIDEO becomes its
poster frame.
`:data` is base64 text for the verbatim path (the fork caches that read) and
RAW PNG bytes for a transcode; every encoder here takes either.(kitty-transmit data id)Kitty a=t transmit-ONLY sequence: upload PNG data (base64 String or RAW
bytes) under client image id id WITHOUT displaying it, chunked into
m=1…m=0 pieces by the fork's own encoder. A later kitty-place draws it
with NO re-upload — the key to flicker-free scrolling (transmit once, then
re-place cheaply).
Kitty `a=t` transmit-ONLY sequence: upload PNG `data` (base64 String or RAW bytes) under client image id `id` WITHOUT displaying it, chunked into `m=1`…`m=0` pieces by the fork's own encoder. A later `kitty-place` draws it with NO re-upload — the key to flicker-free scrolling (transmit once, then re-place cheaply).
(materialize-attachment att)Decode ONE persisted user image attachment (canonical wire map, STRING keys
id/base64/media_type/filename) into a STABLE cache file keyed by its
row id and return the descriptor a vis-image fence needs -
{:path :mime :filename :size :size-label :width :height} - or nil when it is
neither a usable still nor a decodable clip. Idempotent: an already-written
cache file is reused,
never rewritten, so a resumed session re-renders the picture from DB-owned
bytes even after the original source path is gone. Never throws.
Decode ONE persisted user image attachment (canonical wire map, STRING keys
`id`/`base64`/`media_type`/`filename`) into a STABLE cache file keyed by its
row id and return the descriptor a `vis-image` fence needs -
`{:path :mime :filename :size :size-label :width :height}` - or nil when it is
neither a usable still nor a decodable clip. Idempotent: an already-written
cache file is reused,
never rewritten, so a resumed session re-renders the picture from DB-owned
bytes even after the original source path is gone. Never throws.(parse-cell-size-report s)Parse a terminal window-report reply into {:w :h} CELL pixel dimensions, or
nil. The fork recognises the CSI 16 t cell-size reply ESC[6;<h>;<w>t
directly, and otherwise derives the cell from a CSI 14 t text-area-pixels
reply ESC[4;<hpx>;<wpx>t paired with a CSI 18 t text-area-cells reply
ESC[8;<rows>;<cols>t (cell = px / cells) — tolerant of the replies arriving
concatenated in any order and interleaved with whatever else the tty had
queued.
Parse a terminal window-report reply into `{:w :h}` CELL pixel dimensions, or
nil. The fork recognises the `CSI 16 t` cell-size reply `ESC[6;<h>;<w>t`
directly, and otherwise derives the cell from a `CSI 14 t` text-area-pixels
reply `ESC[4;<hpx>;<wpx>t` paired with a `CSI 18 t` text-area-cells reply
`ESC[8;<rows>;<cols>t` (cell = px / cells) — tolerant of the replies arriving
concatenated in any order and interleaved with whatever else the tty had
queued.(probe-dimensions path mime)Read path's head and sniff its {:w :h} pixel dimensions. nil on failure.
A VIDEO is answered by the fork from the MP4 track header (no decode, no
native imaging library), and only then by the imaging index — so a clip can
size its cell box without touching a single picture.
Read `path`'s head and sniff its `{:w :h}` pixel dimensions. nil on failure.
A VIDEO is answered by the fork from the MP4 track header (no decode, no
native imaging library), and only then by the imaging index — so a clip can
size its cell box without touching a single picture.(probe-paste-image text {:keys [workspace-root]})Detect the FIRST image the pasted text points at (a dropped file path).
Returns {:path :mime :filename :size :size-label :width :height} or nil.
A dropped MP4/M4V/MOV resolves too — it renders as its poster frame.
workspace-root anchors relative candidates. Never throws.
Detect the FIRST image the pasted `text` points at (a dropped file path).
Returns `{:path :mime :filename :size :size-label :width :height}` or nil.
A dropped MP4/M4V/MOV resolves too — it renders as its poster frame.
`workspace-root` anchors relative candidates. Never throws.(read-base64 path)Read path and base64-encode its bytes, or nil on failure. Cached by
path + mtime + size so an unchanged file is encoded at most once.
Read `path` and base64-encode its bytes, or nil on failure. Cached by path + mtime + size so an unchanged file is encoded at most once.
(render-sequence path mime box)Full escape sequence that draws the image at path (mime) into a
cols×rows cell box for the current terminal, or nil when the terminal
can't render inline images / the file can't be decoded. box may carry
:crop-top/:crop-bottom (cell rows scrolled past the band edge) plus the
image's intrinsic :width/:height, so a partly-scrolled Kitty image renders
its visible slice at native scale instead of vanishing.
Full escape sequence that draws the image at `path` (`mime`) into a `cols`×`rows` cell box for the current terminal, or nil when the terminal can't render inline images / the file can't be decoded. `box` may carry `:crop-top`/`:crop-bottom` (cell rows scrolled past the band edge) plus the image's intrinsic `:width`/`:height`, so a partly-scrolled Kitty image renders its visible slice at native scale instead of vanishing.
(transcode->png-base64 path box)Decode path (any format com.blockether.imaging reads) and re-encode it as a
PNG base64 string, downscaled so it fits the cols×rows cell box in pixels.
The Kitty protocol's f=100 only accepts PNG, so a JPEG/GIF/BMP drop must
pass through here first. Pure FFM (no java.desktop, works in the native
image); returns nil on any failure so callers fall back to a text card.
Prefer [[transcode->png]] and hand its raw :data to the escape encoders:
this arity exists for callers that genuinely need the base64 text.
Decode `path` (any format `com.blockether.imaging` reads) and re-encode it as a PNG base64 string, downscaled so it fits the `cols`×`rows` cell box in pixels. The Kitty protocol's `f=100` only accepts PNG, so a JPEG/GIF/BMP drop must pass through here first. Pure FFM (no java.desktop, works in the native image); returns nil on any failure so callers fall back to a text card. Prefer [[transcode->png]] and hand its raw `:data` to the escape encoders: this arity exists for callers that genuinely need the base64 text.
(video-mime? mime)True for a media type the fork sizes and posters as a clip.
True for a media type the fork sizes and posters as a clip.
(video-source? path mime)True when path is a clip this namespace can draw — by media type when the
caller knows one, else by sniffing the file's head (a dropped path carries no
mime). Both halves live in the fork; 12 bytes are read, never a decode, so it
is cheap enough to sit on the still-image transcode path.
True when `path` is a clip this namespace can draw — by media type when the caller knows one, else by sniffing the file's head (a dropped path carries no mime). Both halves live in the fork; 12 bytes are read, never a decode, so it is cheap enough to sit on the still-image transcode path.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |