Standalone images were downscaled to one pixel per cell column and drawn
as half-blocks, so a 72-cell image was a 72px thumbnail. On terminals
that speak iTerm2's inline image protocol the real picture is now
painted over those rows, at 576px for the same box.
The half-blocks stay in the buffer on purpose. They define the layout,
they are the fallback everywhere else, and because each row's content
differs, ratatui's diff always repaints the block when the document
moves, so a stale image can't be left stranded behind a scroll.
- src/graphics.rs holds the protocol: an allowlist (iTerm2, WezTerm,
Rio; never under tmux or screen, which don't pass the sequence
through), the OSC 1337 writer, and a base64 encoder. No new
dependencies -- base64 is ~15 lines and image's png encoder was
already available. LC_TERMINAL carries this over ssh, since ssh
forwards LC_* but not TERM_PROGRAM. It is an allowlist and not a
probe because a terminal that doesn't parse OSC 1337 would dump the
whole base64 payload on screen
- The protocol cannot clip, and an image running past the last row
would scroll the alternate screen, so partly visible images are
cropped to their visible rows and the emitted box shrinks to match.
Art::pixels is sized to an exact multiple of the cell rows, which
makes that crop a plain slice of the pixel buffer
- Encoder settings are load-bearing: Fast + Adaptive is ~4x smaller
than NoFilter on a photo for ~1ms more, while Default compression
costs 140ms and is far too slow to run per scroll step. Alpha is
composited over black up front so the payload is RGB, matching what
the half-blocks show
- The event loop now applies every queued event before drawing again.
Without it, key repeat outruns the payload writes and the half-block
underlay is what you actually see while scrolling. A burst of ten
keys now costs three emissions instead of ten
- Drawn caches each image's placement and encoded bytes: one that
hasn't moved is not rewritten at all, and one that moved without
changing its crop is rewritten without re-encoding. Raw view, the
help overlay and Event::Resize clear the record, since each leaves
the screen repainted without necessarily moving anything
- An image is sized min(80% of the column, source width / IMAGE_CELL_PX),
so one smaller than the column renders at its own size instead of being
stretched across it. A cell is one pixel to the half-blocks but several to
the inline protocol, which is what made the old .min(ow) guard -- correct
when half-blocks were the only renderer -- stop biting. IMAGE_CELL_PX is
deliberately separate from IMAGE_PX_PER_CELL so tuning sharpness doesn't
silently resize small images
SVG and PDF stay caption-only: iTerm2 cannot decode SVG and neither can
the image crate, and PDF would only work by passing original file bytes
through uncropped. Both still open with o.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sujBZJyUQrYWCxCh8bnek