Skip to content

v0.4.0

@Ivapo Ivapo tagged this 15 Aug 02:23
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
Assets 2
Loading