Skip to content

cashmeredev/kitty-graphics.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

143 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kitty-graphics.el

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.

Demo

Inline video playback (mpv via the Kitty backend, truecolor):

assets/mpv-showcase.gif

Org heading text sizing (OSC 66, native scaled headings):

assets/text-headings.gif

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):

assets/shr-demo.gif

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):

assets/doc-view.gif

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):

assets/foot.gif

Inline web browser (casty, experimental):

assets/casty-demo.gif

Overview

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 (both org-toggle-inline-images and org-link-preview for 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)

Requirements

  • 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: img2sixel from libsixel (Nix: nixpkgs#libsixel). Roughly 2.2x smaller payloads and ~10x faster 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 on PATH the Sixel backend logs no encoder found and silently produces no image, so make sure one is installed before launching Emacs.
  • 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=kitty support (enable with kitty-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 the xterm-kitty terminfo)
TERM=xterm-256color emacsclient -nw

Daemon and multiple clients

The 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.

Installation

straight.el

(use-package kitty-graphics
  :straight (:local-repo "~/projects/kitty-graphics")
  :config
  (setq kitty-gfx-enable-video t)   ; optional: inline mpv playback
  (kitty-graphics-setup))

Manual

(add-to-list 'load-path "~/projects/kitty-graphics")
(require 'kitty-graphics)
(kitty-graphics-setup)

Usage

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 set kitty-gfx-heading-sizes-auto to apply automatically). Requires Kitty >= 0.40.0
  • In org-mode: preview LaTeX fragments with C-c C-x C-l (requires a LaTeX installation and dvipng)
  • Typst math: M-x kitty-gfx-typst-preview renders $...$ fragments (region or buffer); kitty-gfx-typst-clear-preview removes them
  • Play a video: M-x kitty-gfx-play-video (needs kitty-gfx-enable-video and mpv). While playing: SPC pause/resume, q stop and go back, ? help
  • Open a PDF: doc-view-mode renders pages via Kitty (n / p to navigate, + / - / 0 to zoom)
  • Open an image file: image-mode displays it via Kitty; + / == zoom in, - zoom out, 0 reset
  • In dired: press P on an image for a side-window preview; kitty-gfx-dired-play-video plays the video at point inline
  • In dirvish: image and video previews work automatically (no extra config)
  • In eww/mu4e/gnus: HTML images render inline

Commands

Core

CommandDescription
kitty-graphics-setupEnable, daemon- and tty-aware
kitty-graphics-modeToggle the global minor mode
kitty-gfx-display-imageDisplay an image file at point
kitty-gfx-remove-imagesRemove images in region or buffer
kitty-gfx-clear-allRemove all images from all buffers

Org & text sizing

CommandDescription
kitty-gfx-org-heading-sizesToggle scaled org heading sizes (OSC 66)

Typst math

CommandDescription
kitty-gfx-typst-previewRender $...$ math fragments as inline images
kitty-gfx-typst-clear-previewRemove typst preview overlays

Video

CommandKeyDescription
kitty-gfx-play-videoPlay a video file inline via mpv
kitty-gfx-toggle-videoSPCPause/resume playback
kitty-gfx-stop-videoStop inline video playback
kitty-gfx-stop-video-and-backqStop and switch to previous buffer
kitty-gfx-video-help?Echo the inline-video keymap
kitty-gfx-dired-play-videoPlay the video at point in dired

The SPC / q / ? keys are active on the video overlay while a video plays.

Image-mode zoom

CommandKeyDescription
kitty-gfx-image-increase-size+ / =Zoom in
kitty-gfx-image-decrease-size-Zoom out
kitty-gfx-image-reset-size0Reset zoom

Dired / dirvish

CommandKeyDescription
kitty-gfx-dired-previewPPreview image at point in a side window
kitty-gfx-dired-auto-preview-modeAuto-preview the file at point on cursor move

Debugging

CommandDescription
kitty-gfx-doctorDiagnostic report: backend, capabilities, binaries
kitty-gfx-debug-stateDump critical state to a debug buffer
kitty-gfx-debug-overlay-at-pointDump info about the image overlay at point
kitty-gfx-run-self-testsRun batch-safe self-tests

Customization

Core & sizing

VariableDefaultDescription
kitty-gfx-preferred-protocolautoGraphics protocol: auto, kitty, or sixel
kitty-gfx-max-width120Maximum inline image width in columns
kitty-gfx-max-height40Maximum inline image height in rows
kitty-gfx-chunk-size4096Max base64 chunk size for image transfer
kitty-gfx-render-delay0.016Debounce delay before re-rendering (s)
kitty-gfx-cache-size64Max images kept in the terminal-side cache
kitty-gfx-debugnilLog debug info to the debug buffer / log

Performance & conversion

VariableDefaultDescription
kitty-gfx-async-conversiontConvert non-PNG images to PNG in the background, off the main loop
kitty-gfx-process-timeout15.0Seconds before a hung magick/identify/ffmpeg/typst call is killed
kitty-gfx-skip-clean-refreshtSkip refreshing when no visible window content changed
kitty-gfx-base64-cache-bytes67108864Max 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.

shr images (eww, elfeed, mu4e, gnus)

VariableDefaultDescription
kitty-gfx-shr-scalenilImage sizing for shr backends: nil, a float, or fit
kitty-gfx-shr-fit-width0.6Fraction of the window width under fit sizing
kitty-gfx-shr-fit-height20Maximum 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 most kitty-gfx-shr-fit-width of the window’s width and kitty-gfx-shr-fit-height rows 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)

Sixel encoder

VariableDefaultDescription
kitty-gfx-sixel-encoder-programnilEncoder program (auto: img2sixel > magick > convert)
kitty-gfx-sixel-encoder-argsnilExtra arguments passed to the encoder
kitty-gfx-sixel-encoder-timeout5.0Max seconds to wait for a single encoder run
kitty-gfx-sixel-dithernilDithering: nil (encoder default), “none”, “fs”, “atkinson”
kitty-gfx-sixel-colors256Palette size; lower shrinks the payload, fewer colors

tmux

VariableDefaultDescription
kitty-gfx-tmux-passthroughtWrap Kitty APC sequences in tmux DCS passthrough
kitty-gfx-kitty-placement-modeautoPlacement strategy: auto, direct, or placeholder
kitty-gfx-tmux-allow-sixeltAllow the Sixel backend inside tmux >= 3.4

Video

VariableDefaultDescription
kitty-gfx-enable-videonilEnable inline mpv video (mpv 0.36.0+)
kitty-gfx-video-file-extensionsmp4 mkv webm mov m4v aviExtensions handled by inline mpv preview
kitty-gfx-video-thumbnail-seek“0.5”Seconds offset for thumbnail extraction
kitty-gfx-dired-preview-debounce0.3Idle seconds before dired auto-preview
kitty-gfx-dirvish-video-inline-previewnilWhere to play a video opened with RET in dirvish

Headings & text sizing

VariableDefaultDescription
kitty-gfx-heading-scales((1 . 2.0) (2 . 1.5) (3 . 1.2))Org heading level -> visual scale factor
kitty-gfx-heading-sizes-autonilApply heading sizes automatically in org buffers
kitty-gfx-heading-scan-visible-onlytOnly instrument headings near the visible region
kitty-gfx-heading-conflicting-modes(org-modern-mode …)Minor modes disabled while heading sizes are active

Typst

VariableDefaultDescription
kitty-gfx-typst-command“typst”Path to the typst executable
kitty-gfx-typst-ppi300Pixels-per-inch passed to typst compile
kitty-gfx-typst-text-size11Text size in points for rendered math fragments
kitty-gfx-typst-preamblenilExtra 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.

How It Works

  1. On startup, backend dispatch selects a protocol: if kitty-gfx-preferred-protocol is auto, it checks for Kitty (via KITTY_PID), then probes Sixel via a DA1 query. The mode-line shows [K], [S], or [?] for the active backend, with a +T suffix (e.g. [K+T]) when the Kitty text sizing protocol (OSC 66) is available.
  2. Cell pixel size is queried via CSI 16 t (XTWINOPS) for accurate image scaling. Falls back to 8x16 if the query times out.
  3. 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).
  4. 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.
  5. 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.
  6. Video: mpv runs on a PTY with --vo=kitty (or --vo=sixel on 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.
  7. Overlays with a display property reserve blank space in Emacs buffers.
  8. 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.

Supported Formats

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.

Known Limitations

  • 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-sixel to 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-sizes off 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 / convert work as fallback but are noticeably slower. With no encoder on PATH, the backend silently produces no output and only logs no encoder found when kitty-gfx-debug is non-nil
  • Without ImageMagick, only PNG files work on the Kitty backend

Inline web browser (experimental)

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.

KeyAction
j / kScroll down / up
C-f / C-bPage down / up
H / LHistory back / forward
rReload
o / :Open a URL
fLink hints (Vimium-style)
mouse-1Click at point
qQuit 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:

VariableDefaultDescription
kitty-gfx-enable-browsernilEnable the inline casty web browser
kitty-gfx-casty-program“casty”Program name or path used to launch casty
kitty-gfx-casty-chromenilPath to a Chromium-based browser for casty to drive
kitty-gfx-browser-max-width200Maximum browser frame width in columns
kitty-gfx-browser-max-height60Maximum browser frame height in rows
kitty-gfx-browser-scroll-step300Pixels scrolled per j=/=k keypress
kitty-gfx-browser-evil-bindingstMirror the browser keymap into evil normal/motion state

License

GPL-2.0-or-later

About

Display images, video, and scaled text directly in terminal Emacs (emacs -nw) using the Kitty graphics protocol, tmux or Sixel

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors