Image Rendering
May 14, 2026 ยท View on GitHub
Summary
Restish v2 renders image/* responses directly in the terminal when stdout is a
TTY. The best available protocol is chosen automatically based on environment
variables and terminal heuristics. Fallback to Unicode half-block rendering
ensures something useful appears on any colour-capable terminal.
On non-TTY output, raw bytes still flow unchanged so > out.png works exactly
as users expect.
Goals
- make common image responses immediately useful in interactive terminals
- preserve exact raw bytes for non-interactive output
- choose the best available terminal image protocol without complex probing
- keep image rendering inside the core output system rather than making users install a plugin for basic behavior
Non-Goals
- rasterizing every possible media type in the core binary
- probing terminals with a complicated handshake before every request
- forcing plugin formatters to solve core content-type dispatch problems
Why It Lives In Core
Image rendering ships in the core binary for three reasons:
- content-type-aware dispatch requires a core decision anyway
- the required codecs and protocols are lightweight
- it should "just work" for a common API experience
Plugin formatters remain the right hook for:
- additional media families
- richer image codecs
- alternate rendering styles
But the default experience for image/* should not depend on optional
installation.
Dispatch Rules
Image rendering is a content-type-aware dispatch decision.
When:
- stdout is a TTY
- no explicit
-oformat was given, and - no non-default filter is active that changes the logical output away from the original image payload
then image/* responses should route to the registered image formatter
instead of the usual auto structured-value path.
Non-TTY output is unaffected: raw bytes flow through unchanged.
Explicit -o image forces image rendering regardless of content type.
Protocol Selection
The formatter inspects environment variables at render time rather than actively querying the terminal.
Priority order:
$RSH_IMAGE_PROTOCOL- Kitty graphics protocol detection
- iTerm2-style inline image detection
- half-block fallback
This keeps first-byte latency low and avoids fragile terminal probing logic.
Supported Protocols
Kitty Graphics Protocol
Selected when:
$RSH_IMAGE_PROTOCOL=kitty, or$KITTY_WINDOW_IDis set, or$TERM=xterm-kitty
Image data is base64-encoded and sent in chunks using Kitty APC sequences.
iTerm2 Inline Images
Selected when:
$RSH_IMAGE_PROTOCOL=iterm2, or$TERM_PROGRAMindicates a compatible terminal such asiTerm.app,WezTerm, orHyper
The raw payload is base64-encoded and embedded in an OSC 1337 sequence.
Half-Block Fallback
Selected when:
$RSH_IMAGE_PROTOCOL=halfblock, or- no richer protocol is detected
The image is decoded locally, scaled to terminal width, and rendered using the
Unicode upper half block (โ) with ANSI 24-bit foreground/background colors.
This is the universal fallback path when inline image protocols are unavailable.
Image Format Support
For Kitty and iTerm2-style protocols, the formatter can forward common image payloads as-is and let the terminal decode them.
For the half-block path, Restish needs local decode support. The core design expects at least:
- PNG
- JPEG
- GIF
If local decoding fails in the half-block path, the formatter should fall back gracefully rather than pretending it rendered the image successfully.
Width And Scaling
Half-block rendering needs a terminal width estimate.
The design order is:
- terminal size query through the TTY helper
$COLUMNS- default width such as 80 columns
The formatter should avoid upscaling tiny images unnecessarily. Its job is to render usefully, not to invent artificial enlargement.
Interaction With Output Planning
Image rendering is part of the output system, not a separate feature.
Important planner rules:
- TTY + original
image/*payload -> image formatter is eligible - redirected output -> raw bytes win
- filtered or transformed image responses no longer count as "original image payload" for auto-dispatch
This keeps image rendering aligned with the same "raw versus normalized value" distinction used elsewhere in the output model.
Error And Fallback Behavior
If the chosen image protocol path fails, Restish should degrade predictably:
- protocol override asks for a supported path -> try that path
- if local half-block decode fails -> fall back toward raw behavior rather than emitting corrupted bytes to the terminal as if they were an image
- explicit
-o imagewith unusable image data should surface an error rather than silently pretending success
The design goal is graceful fallback, not silent corruption.
Examples
Interactive image rendering:
restish get https://api.example.com/users/42/avatar
Raw file download:
restish get https://api.example.com/users/42/avatar > avatar.png
Force half-block rendering:
RSH_IMAGE_PROTOCOL=halfblock restish get https://api.example.com/users/42/avatar
Alternatives Considered
Implement Via A Hook Plugin Formatter
A plugin formatter can render image bytes, but the core still has to decide when to auto-dispatch based on content type. Since that dispatch logic already lives in the core, basic image rendering belongs there too.
Query The Terminal For Capability
Would be more precise in some cases, but adds timing, raw-mode, and complexity cost that is hard to justify given the fallback path.
Sixel Support In Core
Useful for some terminals, but deferred because it increases protocol and rendering complexity substantially. A future plugin or future core expansion can add it deliberately.
Relationship To Other Designs
- Design 009 defines the normalized/raw output split image rendering depends on.
- Design 017 defines terminal-facing CLI behavior and diagnostics.
- Design 028 defines output-family planning and auto-dispatch interactions.