Images & Compression
bmux supports inline terminal images through three protocols: Sixel, Kitty Graphics, and iTerm2 inline images. Images are automatically intercepted, stored in a per-pane registry, and re-emitted to the host terminal. They also appear in GIF recordings.
Supported Image Protocols
| Protocol | Origin | How It Works |
| Sixel | DEC, 1980s | ASCII-encoded raster data in DCS escape sequences. Widely supported by modern terminals (foot, WezTerm, mlterm, contour, xterm with -ti vt340). |
| Kitty Graphics | kitty terminal | Binary pixel data or PNG in APC escape sequences. Supports transparency, animation, and efficient placement. Used by kitty, WezTerm, Ghostty. |
| iTerm2 | iTerm2 | Base64-encoded image files (PNG, JPEG, GIF) in OSC sequences. Used by iTerm2, WezTerm, mintty. |
How It Works
When a program running inside a bmux pane emits image escape sequences, bmux:
- Intercepts the escape sequences in the PTY output stream
- Stores the image data in a per-pane image registry (with position, dimensions, and raw protocol bytes)
- Re-emits the image to the host terminal using the appropriate protocol for display
- Tracks changes – when images scroll, move, or are removed, the registry updates automatically
Applications can use the implemented image operations without a bmux-specific API. Compatibility depends on the host terminal, protocol operations, image format, and build features; support for a protocol does not imply support for every animation, transport, or placement extension. Test the applications and terminals you intend to use.
Scrollback Image Presentation
Live output scrolling and attachment scrollback are different paths. Currently, inline images follow live output, but the attachment scrollback renderer does not consume captured image windows. Entering scrollback can therefore leave images positioned against live output while text displays history. Service-level image retention tests do not establish support for attachment scrollback rendering.
Target ownership and presentation contract
The pane-runtime plugin owns capture selection, history navigation, generated history-service calls, and assembly of a coherent text/image window. The core attachment runtime schedules neutral work and consumes retained presentation; it must not gain additional pane-history service calls or interpret capture policy.
A displayed historical window binds text and images to the same capture, logical anchor, projection width, and visible height. Navigation resolves this window once. The existing attach-history-images-v2 offset is measured in projected rows, so a text-navigation offset cannot be passed through without proving it identifies the same projected window. If that conversion cannot be performed within the request budget, introduce a versioned anchored-window contract rather than changing v2 semantics or guessing an offset.
Image content must use a bounded, owner-scoped resource mechanism separate from text/cell paint operations. Retained placements reference immutable resource identities and revisions; they do not embed raw terminal protocol bytes in RenderOp. This preserves the graphics-payload exclusion guard. Resource publication validates encoded size, decoded dimensions, and total retained bytes before admission; missing or evicted resources require explicit repair rather than silently displaying an incomplete window.
Placements belong to the producer’s own retained surface and use its geometry, clipping, stacking, and occlusion. They cannot target another owner’s allocation. Runtime content and plugin image placements must converge on the same retained compositor, not a second overlay ordering model. Reuse the neutral TUI image primitives and terminal presenter where compatible with these semantics.
Atomic window and frame transitions
The attachment-side plugin controller publishes complete text/image replacements, including empty image lists. It rejects responses for obsolete capture identities, request generations, dimensions, or attachment targets. Prefetched rows must be trimmed with the corresponding image origin adjustment; cached windows include both components and remain bounded.
While a replacement loads, retain the last coherent displayed window rather than mixing new text with old images. Output arriving while pinned updates live state without moving the captured view. Resizing reprojects both components from the same logical anchor. Leaving scrollback replaces the historical window with current live text and images. Capture invalidation enters an explicit recovery state, never a live-image fallback over historical text. Alternate-screen image state remains isolated from normal-screen history.
Image placement and interaction authority advance only after successful terminal presentation. Failed writes retain the last committed scene and require repair; owner removal, empty replacement, and normal shutdown remove owned host image placements without deleting unrelated terminal images.
Required behavioral proof
An attachment integration test must drive the production controller and presenter: place an image beside identifiable text, scroll it offscreen, enter history, navigate away and back, append output while pinned, resize narrower and wider, and return live. Assert image identity, text-relative position, clipping, and removal at each transition. Include stale/out-of-order responses, unavailable captures, failed presentation, alternate-screen transitions, and image-disabled builds. Protocol-level coverage and a real PTY reproduction complement this test; passing capture-service tests alone is insufficient.
Testing Image Support
Using kitten (Kitty protocol)
If you have kitty or the kitten CLI installed:
kitten icat /path/to/image.png
Using timg (all protocols)
timg supports all three protocols with flag switches:
brew install timg
# Kitty protocol
timg -pk /path/to/image.png
# Sixel protocol
timg -ps /path/to/image.png
# iTerm2 protocol
timg -pi /path/to/image.png
Raw escape sequence test (no dependencies)
You can test each protocol with raw escape sequences. This sends a tiny 1x1 red pixel:
Kitty:
printf '\e_Gf=32,s=1,v=1,a=T;/w8AAP8=\e\\'
Sixel (small red block):
printf '\ePq"1;1;1;1#0;2;100;0;0#0!6~-!6~-!6~-!6~-!6~-!6~\e\\'
GIF Export with Images
Images automatically appear in exported GIF recordings. When you record a session that displays images, bmux captures structured image data alongside the terminal text. During GIF export, images are decoded to pixels and composited onto the rasterized text frame.
To record and export:
# Start a recording
bmux recording start --name image-demo
# Display some images in your session...
# Stop and export
bmux recording stop
bmux recording list # find your recording id/name
bmux recording export --format gif <recording-id-or-name>
Optional auto-export for user-initiated recordings
You can have bmux auto-export a GIF when you run user-initiated recording stop/cut flows (bmux --record, bmux recording stop, bmux recording cut).
[recording]
# Auto-export GIF after user-initiated stop/cut.
auto_export = true
# Optional dedicated output directory for auto-exported GIFs.
# Supports ~, $VAR, and ${VAR} interpolation.
# Relative paths resolve from the directory that contains bmux.toml.
# If unset, bmux writes GIFs next to the recording directory.
auto_export_dir = "recordings/gif"
CLI overrides for a single invocation:
bmux recording stop \
--recordings-dir /tmp/bmux-recordings \
--recording-auto-export \
--recording-auto-export-dir /tmp/bmux-gifs
If auto-export cannot run (for example no display track data is present), bmux still completes the recording command and prints a warning.
Supported decode formats in GIF export:
- Sixel: Full native decode to RGBA pixels
- Kitty: Raw RGB/RGBA pixels and PNG-compressed payloads
- iTerm2: PNG, JPEG, GIF, and BMP via the image crate
Compression
bmux compresses image data and remote connections to reduce bandwidth and improve performance. Compression is enabled by default and works transparently.
Configuration
[behavior.compression]
# Master switch. Set to false to disable all compression.
enabled = true
# Image payload compression algorithm.
# Compresses Sixel, Kitty, and iTerm2 image data before IPC transport.
# Typical reduction: 5-15x for sixel text, 3-20x for raw pixel data.
# Pre-compressed formats (kitty PNG) are automatically skipped.
# Options: auto, none, zstd, lz4
images = "auto"
# Remote connection compression.
# Wraps TLS gateway and Iroh P2P connections in streaming compression.
# Local Unix socket connections are never compressed.
# Options: auto, none, zstd
remote = "auto"
# Compression level for zstd (1-19, ignored for lz4).
# 1 = fastest, 3 = good balance (default), 9+ = diminishing returns.
level = 3
Disabling Compression
# Disable all compression
[behavior.compression]
enabled = false
# Disable only image compression
[behavior.compression]
images = "none"
# Disable only remote connection compression
# (useful when SSH already compresses the tunnel)
[behavior.compression]
remote = "none"
Remote Compression Matching
Both the client and the server gateway must have the same remote setting. If one side compresses and the other doesn’t, the connection will fail. The default auto setting works correctly when both sides use the same bmux version and config.
Image Configuration
[behavior.images]
# Master switch for image protocol support.
enabled = true
# How image decoding is distributed.
# passthrough = forward raw bytes (fastest, default)
# server = decode on server, re-encode for client
# client = send raw bytes, client decodes
decode_mode = "passthrough"
# Maximum image payload size (bytes). Images larger than this are discarded.
max_image_bytes = 10485760 # 10 MiB
# Maximum images per pane. Oldest images are evicted when exceeded.
max_images_per_pane = 100
Troubleshooting
Images not appearing
- Verify your host terminal supports the image protocol being used. Run the raw escape sequence tests above directly in your terminal (outside bmux) to confirm.
- Check that behavior.images.enabled is true (default).
- Ensure the image is within size limits (max_image_bytes).
Images appearing in wrong positions after split/zoom
This is expected during layout transitions. Images are re-fetched from the server with correct positions on the next render cycle.
Performance with many large images
- Reduce max_images_per_pane to limit memory usage per pane.
- Compression is on by default and significantly reduces IPC bandwidth for image-heavy sessions.
- Consider behavior.compression.level = 1 for faster compression if CPU is the bottleneck.
Remote connections failing with compression
If TLS or Iroh connections fail immediately after connecting, the most likely cause is a compression mismatch. Ensure both the client and server gateway have the same behavior.compression.remote setting. Set both to "none" to diagnose.