feat: add Sixel graphics protocol support#18
Merged
Conversation
Images on terminals that don't support Kitty or iTerm2 were falling back to the Unicode half-block renderer, which — let's be honest — produces results that look like someone described the image to a particularly literal ASCII artist. Terminals like foot, mlterm, mintty, and contour all support Sixel, which renders *actual pixels*. The implementation is zero new dependencies. Sixel encoding is done in-house: median-cut colour quantization to 256 colours, 6-row band encoding with RLE compression, the whole DCS-to-ST dance. Images are pre-encoded during pre_render() and cached, with crop caching for smooth scrolling (same pattern as the iTerm2 path). Terminal detection auto-identifies foot, mlterm, mintty, and contour via TERM_PROGRAM / TERM env vars. For everything else, users can force the protocol with MDTERM_IMAGE_PROTOCOL=sixel — because sometimes you just know better than the heuristics. Rendering follows the iTerm2 block-overlay approach: skip image rows in the main pass, then emit one Sixel sequence per visible image region in a second pass. This keeps scrolling smooth and avoids re-emitting colour registers for every terminal row.
The sixel encoder was blending alpha against hardcoded black, which means transparent PNGs look *terrible* on light-themed terminals. The correct thing is to blend against the actual theme background, just like every other sane compositing pipeline does. While at it, sixel_quantize() was accepting a `width` parameter it never used, with a `let _ = width` to shut the compiler up. That's not how you fix unused parameters — you *remove* them. Also unified the iTerm2/Sixel block rendering dispatch in the viewer. Having a 15-line if/else that calls two identical-signature methods with identical arguments is the kind of copy-paste that breeds bugs. render_block_image() dispatches internally now.
The median-cut quantizer in sixel_median_cut could divide by zero if split_off ever produced an empty box — the weighted average fold would happily compute total=0 and then try to divide by it. Not currently reachable through encode_sixel (which bails on empty images), but it was a latent trap waiting for someone to call the function from a different path. Filter out empty boxes before the average computation. The crop path in render_sixel_block had a similar problem: if first_row * cell_h_px exceeded the resized image height, crop_imm would get a y coordinate past the end of the image. Clamp y to height-1 so we always produce a valid crop region. While at it, add a test for alpha blending — a 1×1 half-transparent red pixel against white should produce roughly (100%, 50%, 50%) in the Sixel color register. The kind of thing that should have been tested from the start.
A few lines in the sixel encoder and its tests were too long for rustfmt's liking. This is purely whitespace — no logic changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1
Adds Sixel image protocol support as a middle tier between iTerm2 and the Unicode half-block fallback. Terminals like foot, mlterm, mintty, and contour can now render actual pixel-level images instead of the half-block approximation.
TERM_PROGRAM/TERMenv varsMDTERM_IMAGE_PROTOCOL=sixelfor terminals not auto-detectedProtocol priority
Terminals that support multiple protocols (e.g. WezTerm supports both Kitty and Sixel) will use the most efficient one.
Auto-detected Sixel terminals
TERM_PROGRAM=footorTERM=foot/foot-extraTERM_PROGRAM=mltermorMLTERMenv varTERM_PROGRAM=minttyTERM_PROGRAM=contourTest plan
cargo check— compiles cleancargo clippy— zero warningscargo test— all 117 tests pass (109 existing + 8 new)cargo build --release— release build succeedsMDTERM_IMAGE_PROTOCOL=sixel mdterm README.md)