1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
# AGENTS.md -- kitty-graphics
## Project Overview
Pure Emacs Lisp package (`kitty-graphics.el`) for displaying images in
terminal Emacs (`emacs -nw`) using either the Kitty graphics protocol or
Sixel, with automatic protocol detection. Also renders org-mode headings
at scaled font sizes using the Kitty text sizing protocol (OSC 66).
Integrates with org-mode, image-mode, shr/eww/mu4e/gnus, dired, and dirvish.
Language: **Emacs Lisp** (`lexical-binding: t`). Single source file (~1876 lines).
Current version: **0.3.0** (see Versioning section).
## Build / Validate
### Byte-compile (primary lint check -- run after every change)
```sh
emacs -Q -batch -f batch-byte-compile kitty-graphics.el
```
**This is the closest thing to CI** -- always run it before considering
a change complete.
### Load-test (verify file evaluates without error)
```sh
emacs -Q -batch -l kitty-graphics.el -f kill-emacs
```
## Testing
No automated test framework. All testing is manual/interactive and
requires a supported terminal (Kitty, WezTerm, Ghostty for Kitty backend;
foot, Konsole, xterm, mlterm, mintty for Sixel backend).
The `tests/` directory contains `test-kitty-gfx.org`, `test-image.png`,
and `test-document.pdf` for manual testing -- use these rather than
creating new test files.
**Important**: Always set `TERM=xterm-256color` when launching terminal
Emacs -- the `xterm-kitty` terminfo is often missing.
### Test with org-mode
```sh
TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
--eval "(kitty-graphics-mode 1)" tests/test-kitty-gfx.org
# Then: C-c C-x C-v (org-toggle-inline-images)
```
### Test typst inline equations
```sh
TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
--eval "(kitty-graphics-mode 1)" tests/test-typst.typ
# Then: M-x kitty-gfx-typst-preview
# To clear: M-x kitty-gfx-typst-clear-preview
# Requires the `typst` CLI on PATH.
```
### Test text sizing (org heading scales)
```sh
TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
--eval "(setq kitty-gfx-heading-sizes-auto t)" \
--eval "(kitty-graphics-mode 1)" tests/test-kitty-gfx.org
# Headings should render at scaled sizes automatically.
# S-TAB to unfold all, then scroll to verify fold/unfold/scroll.
```
### Test image-mode
```sh
TERM=xterm-256color emacs -nw -Q -l kitty-graphics.el \
--eval "(kitty-graphics-mode 1)" tests/test-image.png
```
### Debug logging
Set `kitty-gfx-debug` to `t` to log debug info to `/tmp/kitty-gfx.log`.
### Dev environment
The repo uses a Nix flake (`.envrc` → `use flake`). If you have direnv
and Nix, dependencies are provided automatically. This is optional --
only `emacs` and optionally `imagemagick` are needed.
### Agent workflow
When you need the user to manually test a feature in a Kitty-compatible
terminal, or have any question that requires user action, notify them:
```sh
notify-send "kitty-graphics" "Please test: <description of what to test>"
```
## Code Style
### File structure (mandatory order)
```
;;; file.el --- Description -*- lexical-binding: t; -*-
;;; Commentary:
;;; Code:
(require ...) ; dependencies
(declare-function ...) ; forward declarations for optional deps
(defgroup ...) ; customization group
(defcustom ...) ; user options
(defconst ...) ; constants
(defvar / defvar-local) ; internal state
;; ... implementation sections with ;;;; headers ...
(provide 'feature)
;;; file.el ends here
```
### Formatting
- **Spaces only**, no tabs (`indent-tabs-mode: nil`; enforced by `Local Variables` block)
- `lexical-binding: t` required in the first line
- `;;;;` section headers to separate logical sections
### Naming conventions
| Category | Pattern | Example |
|-----------------------|--------------------------------|----------------------------------|
| Public API | `kitty-gfx-` prefix | `kitty-gfx-display-image` |
| Internal functions | `kitty-gfx--` (double dash) | `kitty-gfx--transmit-image` |
| Buffer-local vars | `defvar-local` + double dash | `kitty-gfx--overlays` |
| Customization vars | `defcustom` + single dash | `kitty-gfx-max-width` |
| Global internal vars | `defvar` + double dash | `kitty-gfx--next-id` |
| Advice functions | `kitty-gfx--MODE-VERB-advice` | `kitty-gfx--org-display-advice` |
| Minor mode | `kitty-graphics-mode` | |
All identifiers use `kebab-case`.
### Requires / Dependencies
- Only `cl-lib` is `require`d directly
- All other dependencies (org, image-mode, shr, dired, dirvish) use
`declare-function` for forward declarations and `with-eval-after-load`
for deferred integration -- **never `require` optional packages at
top level**
### Error handling
- `user-error` for user-facing errors (e.g., unsupported terminal)
- `condition-case` around I/O (terminal queries, files, image processing)
- `ignore-errors` around `send-string-to-terminal` -- terminal writes
must never crash Emacs
- `when`/`unless` guards rather than explicit error branches
- `unwind-protect` for cleanup (see `kitty-gfx--refresh`)
### Advice convention
All advice uses `:around` and guards on both mode and terminal:
```elisp
(defun kitty-gfx--FOO-advice (orig-fn &rest args)
(if (and kitty-graphics-mode (not (display-graphic-p)))
... ;; terminal path
(apply orig-fn args)))
```
New mode integrations **must** follow this pattern. Each mode needs
explicit advice -- there is no generic image display hook.
## Architecture
### Backend dispatch
Protocol-specific code is isolated behind an alist-based backend system:
- `kitty-gfx--backends` -- alist mapping `'kitty` / `'sixel` to operation alists
- `kitty-gfx--active-backend` -- symbol for the detected backend
- `kitty-gfx--backend-fn` -- dispatch helper: `(funcall (kitty-gfx--backend-fn 'place) ...)`
- `kitty-gfx--detect-protocol` -- tries Kitty (env check), then Sixel; sets active backend
Each backend implements: `detect`, `prepare`, `place`, `delete`, `cleanup`, `cleanup-all`.
### Image pipeline (shared)
1. **Prepare**: backend-specific -- Kitty transmits via APC `a=t`; Sixel encodes via ImageMagick to temp-file
2. **Reserve space**: `kitty-gfx--make-overlay` -- overlay with blank `display` property
3. **Place**: backend-specific -- Kitty emits `a=p` with `p=PID`; Sixel reads temp-file and emits DCS
4. **Refresh**: `kitty-gfx--refresh` -- debounced via `run-at-time`; walks windows,
re-places moved overlays, deletes hidden ones
5. **Cache**: `kitty-gfx--image-cache` -- keyed by file path → image ID (integer).
Dimensions are computed fresh each time. LRU eviction via `kitty-gfx--cache-lru`
(max `kitty-gfx-cache-size` entries, default 64).
Sixel also caches encoded data in temp-files under `/tmp/kitty-gfx-sixel-*.six`.
Overlay properties: `kitty-gfx` (bool marker), `kitty-gfx-id` (image ID),
`kitty-gfx-pid` (placement ID), `kitty-gfx-cols`/`kitty-gfx-rows` (cell size),
`kitty-gfx-last-row`/`kitty-gfx-last-col` (cached position, nil if hidden),
`kitty-gfx-file` (source file path, needed by Sixel for re-encoding).
All terminal output is wrapped in synchronized output (BSU/ESU, DEC mode
2026) to prevent flicker.
### Text sizing (OSC 66)
Unlike images, the text sizing protocol is **stateless** — there are no
image IDs or placement IDs. The terminal renders scaled text inline
but any redraw clears it.
1. **Detect**: `kitty-gfx--query-text-sizing-support` — 3 CPR queries
to probe width-only vs scale vs no support. Cached in
`kitty-gfx--text-sizing-support` (`scale`, `width`, or `none`).
2. **Scan**: `kitty-gfx--org-apply-heading-sizes` — walks org headings,
looks up scale from `kitty-gfx-heading-scales` alist, creates overlays.
3. **Overlay**: `kitty-gfx--make-heading-overlay` — overlay **without**
`display` property (Emacs shows the heading line normally; OSC 66 is
painted on top by the refresh cycle). No vertical space reservation.
4. **Place**: `kitty-gfx--place-heading` — emits `\e]66;s=SCALE;TEXT\a`
at cursor position with SGR bold + 24-bit foreground color from
`org-level-N` faces.
5. **Refresh**: `kitty-gfx--refresh-heading-overlay` — **always re-emits**
OSC 66 when visible (stateless protocol). Erases old position on move
via `kitty-gfx--erase-heading` (writes spaces across all rows —
per spec, overwriting any cell in the topmost row of a multicell
character erases the entire block).
Heading overlay properties (in addition to the common ones above):
`kitty-gfx-heading` (bool), `kitty-gfx-heading-text`, `kitty-gfx-heading-scale`,
`kitty-gfx-heading-level`. Heading overlays have `modification-hooks` so
editing the heading text removes the stale overlay for re-creation.
Re-scan after edits is debounced via `kitty-gfx--heading-rescan-timer`
(buffer-local, 0.2s) to prevent redundant scans from rapid typing.
Key design differences from images:
- Heading overlays **preserve their position cache** in
`on-window-change` (images clear it). This lets the refresh cycle
compare old→new position and erase properly. Images can afford to
clear cache because re-placing with the same PID is idempotent.
- In `on-buffer-change`, heading overlays are **defensively erased**
when their buffer becomes hidden (not visible in any window). This
prevents ghost multicell artifacts if the new buffer's content does
not fully overwrite the heading's terminal cells.
- Overlap detection (`kitty-gfx--heading-occupied-rows`) is reset
**per-window** inside `walk-windows`, not globally. This ensures
headings in one window don't block headings at different terminal
rows in another window showing the same buffer.
## Known Issues & Planned Work
- **Fixed**: Overlays remain visible when org headings are collapsed (#1)
- **Limitation**: Does not work inside tmux (neither Kitty passthrough nor Sixel) (#2)
- **Limitation**: Each mode needs explicit `:around` advice integration
- **Limitation**: GIF files are not supported (multi-frame conversion issues,
no animation support in terminal)
- **Limitation**: Sixel is 256 colors (vs truecolor on Kitty), stateless (re-emits on scroll)
- **Done**: LaTeX fragment preview in org-mode (#3)
- **Done**: doc-view / pdf-view-mode integration (#4)
- **Done**: Typst inline equation preview (#5) -- `kitty-gfx-typst-preview`
scans `$...$' fragments in the current buffer or region, compiles each
via the `typst' CLI, and displays the resulting PNG inline. No advice
hooks; bind to a key or invoke via `M-x'.
- **Done**: Text sizing protocol for org heading sizes (OSC 66, Kitty >= 0.40.0)
- **Done**: Sixel backend with auto-detection (v0.3.0)
- **WIP**: Sixel image misplacement in org-mode (reported in foot terminal)
- **Planned**: comint-mime integration for shell buffer images (#5)
- **Planned**: Newsticker (RSS reader) integration (#6)
## Dependencies
- **Required**: Emacs >= 27.1, a supported terminal (Kitty/WezTerm/Ghostty
for Kitty backend; foot/Konsole/xterm/mlterm/mintty for Sixel)
- **Required for Sixel**: ImageMagick (`magick`/`convert`) for encoding
- **Optional for Kitty**: ImageMagick for non-PNG formats and pixel-accurate sizing
- **Optional for typst preview**: `typst` CLI on PATH (used by
`kitty-gfx-typst-preview`)
- **No external Elisp deps** beyond built-in `cl-lib`
## Versioning
Version is declared in the file header (`Version: X.Y.Z` in
`kitty-graphics.el` line 6). This is the **single source of truth**.
- **Patch** (0.2.x): bug fixes, minor improvements
- **Minor** (0.x.0): new features, new mode integrations
- **Major** (x.0.0): breaking changes to public API (`kitty-gfx-` symbols)
Bump the version in the header when preparing a release.
|