Display images, video, and scaled text directly in terminal Emacs (emacs -nw) using the Kitty graphics protocol or Sixel. Beyond inline images it also does inline mpv video playback, org-mode heading text sizing (OSC 66), and typst math previews.
The right backend is picked automatically at startup, so it works across ~20+ terminals with no configuration, and keeps working inside tmux, where Kitty graphics are wrapped in DCS passthrough and Sixel runs natively on tmux >= 3.4.
Version 1.0.4. Requires Emacs >= 27.1.
Inline video playback (mpv via the Kitty backend, truecolor):
Org heading text sizing (OSC 66, native scaled headings):
Heading rendering was massively reworked and is now stable for reading and navigating files. Editing scaled headings works far better than before (the overlay stays put and a debounced rescan re-renders in place as you type), but for now it is best used to view files rather than to actively edit them: heavy editing re-renders on every debounce, and only one window per buffer renders the scaled headings while others show plain text. Toggle kitty-gfx-org-heading-sizes off when you switch to editing.
shr image scaling (eww, elfeed, mu4e, gnus):
With kitty-gfx-shr-scale set to fit (or a float fraction), inline images in eww/elfeed/mu4e/gnus are scaled into a window-relative box instead of dominating the buffer at full natural size.
PDF and document viewing (doc-view):
PDFs, DVI, PostScript, and EPUB render inline through doc-view, page-centered, with n=/=p to flip pages and +=/-=/=0= to zoom. On the Kitty backend a zoomed page is clipped to the window and panned by scrolling; on Sixel the page renders at fit size. With MuPDF’s mutool on PATH doc-view uses it automatically; Ghostscript also works.
Sixel backend (foot):
Inline web browser (casty, experimental):
kitty-graphics.el renders images directly in terminal Emacs using either the Kitty graphics protocol or Sixel, selected automatically at startup. The backend dispatch tries Kitty first (fast KITTY_PID env check), then probes for Sixel support, so the package works across ~20+ terminals without configuration.
With the Kitty backend, images are transmitted once and positioned via direct placements at overlay screen coordinates after each redisplay. The Sixel backend encodes images via an external encoder and re-emits them on scroll. Both backends integrate with Emacs’s overlay display engine, so images scroll with text, survive buffer switches, work in split windows, and keep rendering inside tmux.
Integrations and capabilities:
- org-mode: inline images via
C-c C-x C-v(bothorg-toggle-inline-imagesandorg-link-previewfor org 10.0+) - org heading text sizing: render headings at scaled sizes via the Kitty text sizing protocol (OSC 66)
- org LaTeX preview: render LaTeX fragments as images via
C-c C-x C-l - typst math preview: render inline
$...$math fragments as images - inline video: play video files inline with mpv (Kitty backend, plus experimental Sixel via
--vo=sixel) - doc-view: PDF/DVI/PS viewing with page navigation and zoom
- image-mode: terminal-aware image file viewing with zoom
- shr/eww: inline images in HTML rendering (eww, mu4e, gnus)
- dired: image preview in a side window, plus inline video preview/playback
- dirvish: native preview dispatcher for image and video files
- inline web browser: drive a real Chromium page inside a buffer via casty (experimental, Kitty only)
- Emacs >= 27.1
- A supported terminal:
- Kitty backend: Kitty >= 0.20.0, WezTerm, or Ghostty
- Sixel backend: foot, Konsole, xterm (with
+sixel), mlterm, mintty, Windows Terminal, VS Code Terminal, and others with Sixel support
- A Sixel encoder on
PATH, required for the Sixel backend.- Strongly recommended:
img2sixelfrom libsixel (Nix:nixpkgs#libsixel). Roughly2.2xsmaller payloads and~10xfaster than ImageMagick, which matters a lot in tmux where every refresh re-emits the full DCS payload. - Fallback: ImageMagick 7 (
magick) or 6 (convert). Works, but slower; on long sessions and in tmux you may notice flicker on redraw. - Override the auto-detect order via
kitty-gfx-sixel-encoder-program. If neither is onPATHthe Sixel backend logsno encoder foundand silently produces no image, so make sure one is installed before launching Emacs.
- Strongly recommended:
- ImageMagick (
magick/identify) is still needed for non-PNG formats on the Kitty backend (JPEG/WebP/SVG/TIFF/BMP -> PNG conversion) and for the dimension probe - For org heading text sizing (OSC 66): Kitty >= 0.40.0, currently the only terminal implementing OSC 66 with scale support
- For inline video: mpv >= 0.36.0 with
--vo=kittysupport (enable withkitty-gfx-enable-video); on the Sixel backend, an mpv built with libsixel (--vo=sixel, experimental) - For typst math preview: the ~typst~ CLI on
PATH - For LaTeX preview: a TeX distribution with
dvipng(e.g.texlive) - For doc-view:
ghostscript(for PDF),dvipng(for DVI); the same tools as GUI Emacs - Launch Emacs with
TERM=xterm-256color(Emacs often can’t find thexterm-kittyterminfo)
TERM=xterm-256color emacsclient -nwThe package works under emacs --daemon with several emacsclient -t
clients attached to different terminals at the same time. Each client’s
graphics are routed to its own tty, and capabilities are detected per
client, so one client in Kitty and another in a Sixel terminal both
render correctly and concurrently. New clients are picked up
automatically; a client’s terminal capabilities are probed the first time
that client is focused.
Inline video (mpv) and the inline browser (casty) are the exception: each streams a single byte stream to one terminal, so playback is bound to the client it was started on. If you display that buffer on a second client, the video/browser keeps rendering only on the launching client and the other client shows the reserved blank area. Closing the launching client stops its playback.
(use-package kitty-graphics
:straight (:local-repo "~/projects/kitty-graphics")
:config
(setq kitty-gfx-enable-video t) ; optional: inline mpv playback
(kitty-graphics-setup))(add-to-list 'load-path "~/projects/kitty-graphics")
(require 'kitty-graphics)
(kitty-graphics-setup)Enable the package with kitty-graphics-setup. It works the same whether you run a plain terminal Emacs or a daemon: under emacs --daemon there is no terminal at startup, so it defers enabling to the first emacsclient -t frame and then detects every later client automatically. No extra daemon configuration is needed.
(kitty-graphics-setup)For a one-off interactive terminal session you can also toggle the global minor mode directly with kitty-graphics-mode.
Then:
- In org-mode: toggle inline images with
C-c C-x C-v - In org-mode: scale headings with
M-x kitty-gfx-org-heading-sizes(or setkitty-gfx-heading-sizes-autoto apply automatically). Requires Kitty >= 0.40.0 - In org-mode: preview LaTeX fragments with
C-c C-x C-l(requires a LaTeX installation anddvipng) - Typst math:
M-x kitty-gfx-typst-previewrenders$...$fragments (region or buffer);kitty-gfx-typst-clear-previewremoves them - Play a video:
M-x kitty-gfx-play-video(needskitty-gfx-enable-videoand mpv). While playing:SPCpause/resume,qstop and go back,?help - Open a PDF:
doc-view-moderenders pages via Kitty (n/pto navigate,+/-/0to zoom) - Open an image file:
image-modedisplays it via Kitty;+/ == zoom in,-zoom out,0reset - In dired: press
Pon an image for a side-window preview;kitty-gfx-dired-play-videoplays the video at point inline - In dirvish: image and video previews work automatically (no extra config)
- In eww/mu4e/gnus: HTML images render inline
| Command | Description |
|---|---|
kitty-graphics-setup | Enable, daemon- and tty-aware |
kitty-graphics-mode | Toggle the global minor mode |
kitty-gfx-display-image | Display an image file at point |
kitty-gfx-remove-images | Remove images in region or buffer |
kitty-gfx-clear-all | Remove all images from all buffers |
| Command | Description |
|---|---|
kitty-gfx-org-heading-sizes | Toggle scaled org heading sizes (OSC 66) |
| Command | Description |
|---|---|
kitty-gfx-typst-preview | Render $...$ math fragments as inline images |
kitty-gfx-typst-clear-preview | Remove typst preview overlays |
| Command | Key | Description |
|---|---|---|
kitty-gfx-play-video | Play a video file inline via mpv | |
kitty-gfx-toggle-video | SPC | Pause/resume playback |
kitty-gfx-stop-video | Stop inline video playback | |
kitty-gfx-stop-video-and-back | q | Stop and switch to previous buffer |
kitty-gfx-video-help | ? | Echo the inline-video keymap |
kitty-gfx-dired-play-video | Play the video at point in dired |
The SPC / q / ? keys are active on the video overlay while a video plays.
| Command | Key | Description |
|---|---|---|
kitty-gfx-image-increase-size | + / = | Zoom in |
kitty-gfx-image-decrease-size | - | Zoom out |
kitty-gfx-image-reset-size | 0 | Reset zoom |
| Command | Key | Description |
|---|---|---|
kitty-gfx-dired-preview | P | Preview image at point in a side window |
kitty-gfx-dired-auto-preview-mode | Auto-preview the file at point on cursor move |
| Command | Description |
|---|---|
kitty-gfx-doctor | Diagnostic report: backend, capabilities, binaries |
kitty-gfx-debug-state | Dump critical state to a debug buffer |
kitty-gfx-debug-overlay-at-point | Dump info about the image overlay at point |
kitty-gfx-run-self-tests | Run batch-safe self-tests |
| Variable | Default | Description |
|---|---|---|
kitty-gfx-preferred-protocol | auto | Graphics protocol: auto, kitty, or sixel |
kitty-gfx-max-width | 120 | Maximum inline image width in columns |
kitty-gfx-max-height | 40 | Maximum inline image height in rows |
kitty-gfx-chunk-size | 4096 | Max base64 chunk size for image transfer |
kitty-gfx-render-delay | 0.016 | Debounce delay before re-rendering (s) |
kitty-gfx-cache-size | 64 | Max images kept in the terminal-side cache |
kitty-gfx-debug | nil | Log debug info to the debug buffer / log |
| Variable | Default | Description |
|---|---|---|
kitty-gfx-async-conversion | t | Convert non-PNG images to PNG in the background, off the main loop |
kitty-gfx-process-timeout | 15.0 | Seconds before a hung magick/identify/ffmpeg/typst call is killed |
kitty-gfx-skip-clean-refresh | t | Skip refreshing when no visible window content changed |
kitty-gfx-base64-cache-bytes | 67108864 | Max bytes of base64-encoded image data cached in memory (64 MiB) |
External conversions (ImageMagick, ffmpeg, typst) run under a watchdog: if one exceeds kitty-gfx-process-timeout it is killed so a corrupt or pathological file cannot freeze Emacs. Sixel encoding has its own bound, kitty-gfx-sixel-encoder-timeout. Set either to nil to disable the watchdog.
| Variable | Default | Description |
|---|---|---|
kitty-gfx-shr-scale | nil | Image sizing for shr backends: nil, a float, or fit |
kitty-gfx-shr-fit-width | 0.6 | Fraction of the window width under fit sizing |
kitty-gfx-shr-fit-height | 20 | Maximum image height in rows under fit sizing |
By default shr images render at natural size, shrinking only to fit kitty-gfx-max-width / kitty-gfx-max-height — which often leaves feed images dominating the buffer. kitty-gfx-shr-scale tames them:
nil— natural size (shrink-to-fit the max dimensions), the default.- a float such as
0.25— render every image at that fraction of its natural width and height, still capped at the max dimensions. fit— dynamically scale each image into a box derived from the live window: at mostkitty-gfx-shr-fit-widthof the window’s width andkitty-gfx-shr-fit-heightrows tall, preserving aspect ratio and never enlarging images that already fit. The box follows the window, so narrowing the window shrinks the images with it.
;; Quarter-size feed images:
(setq kitty-gfx-shr-scale 0.25)
;; Or: dynamic, window-relative sizing (recommended for elfeed/eww):
(setq kitty-gfx-shr-scale 'fit
kitty-gfx-shr-fit-width 0.6
kitty-gfx-shr-fit-height 20)| Variable | Default | Description |
|---|---|---|
kitty-gfx-sixel-encoder-program | nil | Encoder program (auto: img2sixel > magick > convert) |
kitty-gfx-sixel-encoder-args | nil | Extra arguments passed to the encoder |
kitty-gfx-sixel-encoder-timeout | 5.0 | Max seconds to wait for a single encoder run |
kitty-gfx-sixel-dither | nil | Dithering: nil (encoder default), “none”, “fs”, “atkinson” |
kitty-gfx-sixel-colors | 256 | Palette size; lower shrinks the payload, fewer colors |
| Variable | Default | Description |
|---|---|---|
kitty-gfx-tmux-passthrough | t | Wrap Kitty APC sequences in tmux DCS passthrough |
kitty-gfx-kitty-placement-mode | auto | Placement strategy: auto, direct, or placeholder |
kitty-gfx-tmux-allow-sixel | t | Allow the Sixel backend inside tmux >= 3.4 |
| Variable | Default | Description |
|---|---|---|
kitty-gfx-enable-video | nil | Enable inline mpv video (mpv 0.36.0+) |
kitty-gfx-video-file-extensions | mp4 mkv webm mov m4v avi | Extensions handled by inline mpv preview |
kitty-gfx-video-thumbnail-seek | “0.5” | Seconds offset for thumbnail extraction |
kitty-gfx-dired-preview-debounce | 0.3 | Idle seconds before dired auto-preview |
kitty-gfx-dirvish-video-inline-preview | nil | Where to play a video opened with RET in dirvish |
| Variable | Default | Description |
|---|---|---|
kitty-gfx-heading-scales | ((1 . 2.0) (2 . 1.5) (3 . 1.2)) | Org heading level -> visual scale factor |
kitty-gfx-heading-sizes-auto | nil | Apply heading sizes automatically in org buffers |
kitty-gfx-heading-scan-visible-only | t | Only instrument headings near the visible region |
kitty-gfx-heading-conflicting-modes | (org-modern-mode …) | Minor modes disabled while heading sizes are active |
| Variable | Default | Description |
|---|---|---|
kitty-gfx-typst-command | “typst” | Path to the typst executable |
kitty-gfx-typst-ppi | 300 | Pixels-per-inch passed to typst compile |
kitty-gfx-typst-text-size | 11 | Text size in points for rendered math fragments |
kitty-gfx-typst-preamble | nil | Extra typst code prepended to each math fragment |
The image-sizing defaults (kitty-gfx-max-width / kitty-gfx-max-height) govern inline images in org-mode, eww, and dired previews; the shr backends (eww, elfeed, mu4e, gnus) can shrink further via kitty-gfx-shr-scale (see above). Doc-view ignores these and fills the window, with + / - / 0 for zoom.
- On startup, backend dispatch selects a protocol: if
kitty-gfx-preferred-protocolisauto, it checks for Kitty (viaKITTY_PID), then probes Sixel via a DA1 query. The mode-line shows[K],[S], or[?]for the active backend, with a+Tsuffix (e.g.[K+T]) when the Kitty text sizing protocol (OSC 66) is available. - Cell pixel size is queried via
CSI 16 t(XTWINOPS) for accurate image scaling. Falls back to 8x16 if the query times out. - Kitty path: image data is transmitted once via APC escape sequences (
a=t, store only). Direct placements (a=p) position the image at overlay screen coordinates after each redisplay, using unique placement IDs (p=PID). - Sixel path: images are encoded to Sixel via the external encoder and emitted directly at overlay screen coordinates. Since Sixel is stateless, images are re-emitted whenever they scroll to a new position.
- Text sizing: org heading overlays emit OSC 66 (
\e]66;s=SCALE;TEXT\a) at the heading position; the protocol is stateless, so headings are re-emitted on refresh. - Video: mpv runs on a PTY with
--vo=kitty(or--vo=sixelon the Sixel backend); its graphics stream is captured and forwarded to the terminal, while a JSON IPC socket drives pause/resume. Only one video plays at a time, and it auto-pauses when scrolled out of view. - Overlays with a
displayproperty reserve blank space in Emacs buffers. - All output is wrapped in synchronized output (
BSU/ESU,DEC mode 2026) to prevent partial rendering and flicker; position caching skips redundant re-placements, and placements are deleted when overlays scroll out of view.
PNG is sent directly to the terminal. Other formats (JPEG, GIF, WebP, SVG, TIFF, BMP) are converted to PNG via ImageMagick before transmission. Video files (mp4, mkv, webm, mov, m4v, avi) are played via mpv on the Kitty backend, or experimentally on the Sixel backend with an mpv built with libsixel.
- tmux: Kitty graphics now work inside tmux via DCS passthrough (
kitty-gfx-tmux-passthrough), with a Unicode-placeholder placement mode (kitty-gfx-kitty-placement-mode) to survive pane switches. Sixel also works inside tmux >= 3.4 (foot/Konsole/mintty/mlterm/WezTerm as the outer terminal;kitty-gfx-tmux-allow-sixelto opt out). Because tmux’s cell buffer is not pixel-aware, images may stick to old positions after scrolling, an upstream tmux limitation - Inline video on the Sixel backend is experimental: it needs an mpv built with libsixel (
--vo=sixel), every frame is a full DCS payload (expect higher CPU than the Kitty backend), and it is excluded inside tmux like the Kitty path - Under a daemon, inline video and the casty browser are bound to the client they were launched on (a single subprocess streams to one terminal); other clients showing the same buffer see only the reserved blank area. Static images have no such limit and render on every client at once
- Scaled org headings (OSC 66) are best for reading and navigating: while you type, a debounced rescan re-renders the heading in place, and only one canonical window per buffer renders the scaled headings (others showing the same buffer fall back to plain heading text). Toggle
kitty-gfx-org-heading-sizesoff for heavy editing - Animated GIFs display only the first frame (they route through the image pipeline as static images)
- Sixel is limited to 256 colors (vs truecolor on Kitty) and is stateless, so images are re-emitted on every scroll, which may be slower on large images
- The Sixel backend needs a Sixel encoder on
PATH.img2sixel(libsixel) is strongly recommended;magick/convertwork as fallback but are noticeably slower. With no encoder onPATH, the backend silently produces no output and only logsno encoder foundwhenkitty-gfx-debugis non-nil - Without ImageMagick, only PNG files work on the Kitty backend
kitty-gfx-browse embeds a real Chromium page inside an Emacs buffer by driving casty (my fork of sanohiro/casty, MIT) in its embed mode: casty renders the page to PNG frames over the Kitty graphics protocol while Emacs forwards scroll, navigation, clicks, and link hints over an IPC socket. This is experimental and Kitty only (the Kitty terminal itself); it is not reliable on Ghostty or other Kitty-protocol terminals yet.
Requirements: the Kitty terminal, Node >= 18, and a Chromium-based browser (casty auto-installs Chrome Headless Shell on first run, or set kitty-gfx-casty-chrome / the CASTY_CHROME env var to reuse an existing one).
Install: clone the fork next to this repo and install its deps, then point Emacs at it.
git clone git@github.com:cashmeredev/casty.git ~/projects/casty
cd ~/projects/casty && npm install(setq kitty-gfx-enable-browser t
kitty-gfx-casty-program "~/projects/casty/bin/casty.js")Use an existing browser: by default casty downloads and runs its own Chrome Headless Shell under ~~/.casty/browsers/~. To drive a Chromium-based browser you already have installed (Chromium, Chrome, Brave, Helium, …) instead, point kitty-gfx-casty-chrome at its binary; casty launches it headless. Any Chromium-based browser that accepts --headless=new and --remote-debugging-port works.
(setq kitty-gfx-casty-chrome "/usr/bin/chromium")Equivalently, set the CASTY_CHROME environment variable before launching Emacs (CASTY_CHROME=/usr/bin/chromium emacs -nw). This also skips casty’s first-run Chrome download.
Usage: M-x kitty-gfx-browse prompts for a URL and opens it in the *kitty-browser* buffer.
| Key | Action |
|---|---|
j / k | Scroll down / up |
C-f / C-b | Page down / up |
H / L | History back / forward |
r | Reload |
o / : | Open a URL |
f | Link hints (Vimium-style) |
mouse-1 | Click at point |
q | Quit and kill the buffer |
With f, casty labels the clickable elements and you type the label to follow it; escape or C-g cancels. Mouse wheel scrolls. When kitty-gfx-browser-evil-bindings is non-nil (default), the same keys are mirrored into evil normal/motion state.
Customization:
| Variable | Default | Description |
|---|---|---|
kitty-gfx-enable-browser | nil | Enable the inline casty web browser |
kitty-gfx-casty-program | “casty” | Program name or path used to launch casty |
kitty-gfx-casty-chrome | nil | Path to a Chromium-based browser for casty to drive |
kitty-gfx-browser-max-width | 200 | Maximum browser frame width in columns |
kitty-gfx-browser-max-height | 60 | Maximum browser frame height in rows |
kitty-gfx-browser-scroll-step | 300 | Pixels scrolled per j=/=k keypress |
kitty-gfx-browser-evil-bindings | t | Mirror the browser keymap into evil normal/motion state |
GPL-2.0-or-later





