gfx-replacer
Runs a program inside a pseudo-terminal and changes the pictures it prints. It recognises images sent with the Sixel, kitty and iTerm2 graphics protocols, checks them against rules in a TOML file, and swaps matches for other images, or replaces a chosen line of text with an image, before the output reaches your terminal.
Where it sits
It is about 5,200 lines of Rust, with end-to-end tests for each protocol, mixed output, passthrough, dry runs and reloading the rules.
A real run
The page's build script generates two small images, prints the red one as a kitty graphics sequence between two lines of text, and runs that through gfx-replacer with this rule, which matches the sequence by its SHA-256:
[[rule]]
id = "swap-square"
priority = 10
[rule.matcher]
kind = "hash"
sha256 = "b8501136716cefb1d66ece01f86b8f7d3d8aa5dfd86b809b486efb9a3b9965e8"
[rule.replacement]
kind = "file"
path = "after.png"

| Graphics seen | 1 |
|---|---|
| Replaced | 1 |
| Bytes original | 299 |
| Bytes replaced | 303 |
The right-hand image is decoded from what gfx-replacer actually wrote. The text before and after it came through unchanged. With --dry-run the same rule still matches and is counted, but the original image is passed through and the output is byte-for-byte the input:
| Graphics seen | 1 |
|---|---|
| Replaced | 0 |
| Passthrough | 1 |
| swap-square | 1 |
Rules
Matching an image
- hash: the SHA-256 of the whole escape sequence;
- dimensions: an exact size or a range;
- regex: a pattern over the escape sequence;
- protocol: Sixel, kitty or iTerm2;
- composite: and, or and not over the others.
Replacing it
- file: an image on disk, re-encoded for the protocol;
- inline: base64 image data in the rule;
- passthrough: leave it alone.
Rules have priorities, the file can be watched and reloaded while the program runs, and --stats prints counts per protocol and per rule on exit.
Replacing text with an image
A text rule matches an exact line, a regular expression, or a block between start and end markers, such as a program's ASCII-art banner. The text is printed as usual; then gfx-replacer moves the cursor back up onto it, clears those lines and draws the image in their place.
[[text_rule]]
id = "banner"
priority = 10
[text_rule.matcher]
kind = "text_exact"
text = "=== build passed ==="
[text_rule.replacement]
kind = "file"
path = "after.png"
What does not work yet
- Keyboard input is not passed to the program. A line sent on standard input never reaches it, so for now it suits programs that only print, such as image viewers and logs, not interactive ones.
- A text rule moves the cursor up by the height of the match, so if more output arrives in the same burst after the matched text, the image lands on the wrong line.
- The hash covers the whole escape sequence, so the same picture sent with different parameters hashes differently.
- The dimensions matcher is limited in this version, and replacements fetched from a URL are not implemented.