ghostel.el - Terminal emulator powered by libghostty
Table of Contents
- 1. Quick Start
- 2. Requirements
- 3. Installation
- 4. Building from source
- 5. Shell integration
- 6. Input modes
- 7. Features
- 7.1. Terminal emulation
- 7.2. Process model
- 7.3. Links and file detection
- 7.4. Clipboard
- 7.5. Input
- 7.6. Password prompt detection
- 7.7. Shell integration features
- 7.8. Rendering
- 7.9. Inline images (Kitty graphics protocol)
- 7.10. Calling Elisp from the shell
- 7.11. Notifications and progress
- 7.12. Color palette
- 8. TRAMP (Remote Terminals)
- 9. Configuration
- 10. Optional integrations
- 11. Commands
- 12. Running tests
- 13. Performance
- 14. Ghostel vs vterm
- 15. Architecture
- 16. Contributing
- 17. Changelog
- 18. License
- 19. Indices
Ghostel is an Emacs terminal emulator powered by libghostty-vt - the same VT engine that drives the Ghostty terminal. A native dynamic module written in Zig handles terminal state, rendering, and local PTY I/O; Elisp manages keymaps, buffers, commands, and remote process integration.
Ghostel is inspired by emacs-libvterm and follows the same two-layer design, but uses Ghostty's modern VT engine instead of libvterm. This brings the Kitty keyboard and graphics protocols, rich underline styles, OSC 8 hyperlinks, OSC 4/10/11 color queries, and synchronized output (DEC 2026) - none of which libvterm supports. See Ghostel vs vterm for a detailed comparison.
The native module is downloaded automatically on first use, so no toolchain is
required for the common case. Open a terminal with M-x ghostel.
Table of Contents
- 1. Quick Start
- 2. Requirements
- 3. Installation
- 4. Building from source
- 5. Shell integration
- 6. Input modes
- 7. Features
- 7.1. Terminal emulation
- 7.2. Process model
- 7.3. Links and file detection
- 7.4. Clipboard
- 7.5. Input
- 7.6. Password prompt detection
- 7.7. Shell integration features
- 7.8. Rendering
- 7.9. Inline images (Kitty graphics protocol)
- 7.10. Calling Elisp from the shell
- 7.11. Notifications and progress
- 7.12. Color palette
- 8. TRAMP (Remote Terminals)
- 9. Configuration
- 10. Optional integrations
- 11. Commands
- 12. Running tests
- 13. Performance
- 14. Ghostel vs vterm
- 15. Architecture
- 16. Contributing
- 17. Changelog
- 18. License
- 19. Indices
1. Quick Start
(use-package ghostel :bind (("C-x m" . ghostel) :map ghostel-semi-char-mode-map ("C-s" . consult-line) ("M-<backspace>" . ghostel-backward-kill-word) ;; ;; I'm used to go up/down the shell history with M-n/p from eshell ;; ;; Simulate this behavior in ghostel by sending C-p and C-n ("M-p" . (lambda () (interactive) (ghostel-send-key "p" "ctrl"))) ("M-n" . (lambda () (interactive) (ghostel-send-key "n" "ctrl"))) :map project-prefix-map ("m" . ghostel-project) ("M" . ghostel-project-list-buffers)) :config (defun ghostel-send-C-k-and-kill () "Send `C-k' to ghostel. Like normal Emacs `C-k'. Kill to end of line and put content in kill-ring." (interactive) (kill-ring-save (point) (line-end-position)) (ghostel-send-key "k" "ctrl")) (add-to-list 'project-switch-commands '(ghostel-project "Ghostel") t) (add-to-list 'project-switch-commands '(ghostel-project-list-buffers "Ghostel buffers") t) (add-to-list 'ghostel-eval-cmds '("magit-status-setup-buffer" magit-status-setup-buffer))) (use-package ghostel-eshell :hook (eshell-load . ghostel-eshell-visual-command-mode)) (use-package ghostel-compile :hook (after-init . ghostel-compile-global-mode)) (use-package ghostel-comint :hook (after-init . ghostel-comint-global-mode))
If you are an evil user you can install the evil-ghostel extension:
(use-package evil-ghostel :after (ghostel evil) :hook (ghostel-mode . evil-ghostel-mode))
1.1. Shell integration at a glance
Directory tracking and prompt navigation are automatically on by default for local bash, zsh, or fish sessions. See shell integration for TRAMP support and more.
To call Emacs functions from your shell you have to add them to the
ghostel-eval-cmds whitelist and then add something like this to your bashrc:
if [[ "$INSIDE_EMACS" = 'ghostel' ]]; then # Open a file in Emacs from the terminal e() { ghostel_cmd find-file-other-window "$@"; } # Open dired in another window dow() { ghostel_cmd dired-other-window "$@"; } # Open magit for the current directory gst() { ghostel_cmd magit-status-setup-buffer "$(pwd)"; } fi
1.2. Input modes at a glance
Ghostel offers five eat.el-style input modes.
The default is semi-char mode, which forwards almost all keys to the terminal
besides a few exceptions (e.g. M-x, C-c).
In char mode, all keys go to the terminal. Press M-RET to exit.
In line mode Ghostel behaves like M-x shell: the buffer is a normal Emacs
buffer and no key is sent to the terminal. Only after you finish typing a line
and press RET is the whole line sent to the terminal at once.
emacs mode and copy mode make the buffer temporarily a normal Emacs buffer that you can use to navigate, look around, and copy text. The difference between the two is that copy mode freezes the terminal, so if you have continuous output nothing "scrolls away" while you try to select something. emacs mode is live, so new output keeps coming in while you scroll and select.
Those read-only modes have ghostel-readonly-fast-exit enabled by default (it defaults to t),
which automatically exits them on most keys that you expect to be sent to the
terminal. This makes for seamless transitions: say you have some output running
and see something you want to copy - you press C-c C-t to enter copy mode,
navigate like in a normal Emacs buffer, and select your text. When you copy
something or type any character you are automatically back in your normal ghostel
terminal session. Some actions also activate copy mode automatically, like
selecting with the mouse, navigating to hyperlinks (C-c C-p), or activating the
mark.
2. Requirements
- Emacs 28.1+ with dynamic module support
- macOS, Linux, or FreeBSD
The native module is automatically downloaded on first use. Pre-built binaries are available for:
aarch64-macos(Apple Silicon)x86_64-macos(Intel Mac)x86_64-linuxaarch64-linuxx86_64-freebsd
If you prefer to build from source or need a different platform, you will also need Zig 0.15.2 - see Building from source.
3. Installation
3.1. MELPA
(use-package ghostel :ensure t)
3.2. use-package with :vc (Emacs 30+)
(use-package ghostel :vc (:url "https://github.com/dakra/ghostel" :lisp-dir "lisp" :rev :newest))
:lisp-dir "lisp" is only required on Emacs < 31.1.
3.3. use-package with :load-path
(use-package ghostel :load-path "/path/to/ghostel/lisp")
3.4. Manual
(add-to-list 'load-path "/path/to/ghostel/lisp") (require 'ghostel)
Then M-x ghostel to open a terminal.
3.5. Native module
When the native module is missing, Ghostel offers to download a pre-built
binary or compile from source. This is controlled by
ghostel-module-auto-install (default ask). You can also trigger these
manually:
M-x ghostel-download-module- download the minimum supported pre-built binary.C-u M-x ghostel-download-module- choose a specific release tag (leave blank for the latest).M-x ghostel-module-compile- build from source viazig build.
By default the module is read from and written to the package directory. If
your package manager rebuilds or reinstalls the tree while Emacs has the module
loaded, point ghostel-module-directory at a stable location outside the
package tree (for example ~/.config/emacs/ghostel/).
4. Building from source
Building is only needed if you do not want the pre-built binaries. Ghostel
vendors a generated vendor/emacs-module.h, so normal builds do not require
local Emacs headers.
git clone https://github.com/dakra/ghostel.git cd ghostel # Build everything (fetches ghostty automatically via the Zig package manager) zig build -Doptimize=ReleaseFast
To override the vendored Emacs header, set EMACS_INCLUDE_DIR to a directory
containing emacs-module.h, or set EMACS_BIN_DIR to an Emacs bin/ directory
- Ghostel then looks for
../includeand../share/emacs/include.
To build against a local ghostty checkout, temporarily point the dependency at your local path:
zig fetch --save=ghostty /path/to/ghostty zig build -Doptimize=ReleaseFast
When installed from MELPA, M-x ghostel-module-compile builds the native module
from source using zig build; the Zig package manager fetches the ghostty
dependency automatically.
4.1. Bundled terminfo
The compiled xterm-ghostty terminfo entry ships pre-built in etc/terminfo/
and is identical to what tic would produce locally - no build step needed, and
the file format is portable across BSD and ncurses systems. Maintainers
regenerate it via make regen-terminfo after bumping libghostty.
5. Shell integration
Shell integration (directory tracking via OSC 7, prompt navigation via OSC 133,
title tracking via OSC 2, and ghostel_cmd for calling Elisp from the shell) is
automatic for bash, zsh, and fish. No changes to your shell configuration
files are needed.
This is controlled by ghostel-shell-integration (default t). Set it to nil
to disable auto-injection and source the scripts manually instead:
# bash - add to ~/.bashrc: [[ "${INSIDE_EMACS%%,*}" = 'ghostel' ]] && source "$EMACS_GHOSTEL_PATH/etc/shell/ghostel.bash"
# zsh - add to ~/.zshrc: [[ "${${INSIDE_EMACS-}%%,*}" = 'ghostel' ]] && source "$EMACS_GHOSTEL_PATH/etc/shell/ghostel.zsh"
# fish - add to ~/.config/fish/config.fish: string match -qr '^ghostel(,|$)' -- "$INSIDE_EMACS"; and source "$EMACS_GHOSTEL_PATH/etc/shell/ghostel.fish"
Remote (TRAMP / outbound ssh) shell integration has its own setup; see TRAMP
(Remote Terminals).
6. Input modes
Ghostel offers five eat.el-style input modes. You enter a ghostel buffer in
semi-char mode; switch modes with the key bindings below and watch
mode-line-process for the current mode indicator.
| Mode | Indicator | Terminal | Buffer | Purpose |
|---|---|---|---|---|
| semi-char | (none) | live | editable | default - type to terminal, C-c reserved |
| char | :Char |
live | editable | TUI apps - all keys go to the terminal |
| Emacs | :Emacs |
live | read-only | search/read while the terminal keeps running |
| copy | :Copy |
frozen | read-only | precise text selection without scroll churn |
| line | :Line |
live | editable | compose input with Emacs keys, send on RET |
6.1. Mode-switch keybindings
Available from every mode except char mode:
| Key | Action |
|---|---|
C-c C-j |
Switch to semi-char mode (universal exit) |
C-c M-d |
Switch to char mode |
C-c C-e |
Switch to Emacs mode |
C-c C-t |
Toggle copy mode |
C-c C-l |
Switch to line mode |
M-RET |
Char mode only: return to semi-char |
6.2. Semi-char mode (default)
Most keys are sent to the terminal. Keys in ghostel-keymap-exceptions
(default: C-c, C-x, C-u, C-h, M-x, M-:, C-\) pass through to Emacs.
| Key | Action |
|---|---|
| Most keys | Sent directly to the terminal |
C-c C-c |
Send interrupt (C-c) |
C-c C-z |
Send suspend (C-z) |
C-c C-d |
Send EOF (C-d) |
C-c C-\ |
Send quit (C-\) |
C-c M-w |
Copy entire scrollback to kill ring |
C-y |
Yank from kill ring (bracketed paste) |
M-y |
Yank-pop (cycle through kill ring) |
C-c C-y |
Paste from kill ring |
C-c M-l |
Clear scrollback |
C-c C-n |
Jump to next hyperlink |
C-c C-p |
Jump to previous hyperlink |
C-c M-n |
Enter Emacs mode and jump to next prompt |
C-c M-p |
Enter Emacs mode and jump to previous prompt |
C-c C-q |
Send next key literally (escape hatch) |
| Mouse wheel | Scroll through scrollback |
6.3. Char mode
Entered with C-c M-d. All keys (including ghostel-keymap-exceptions) are
sent to the terminal. Useful for TUI apps that want to bind C-x, M-x, C-h,
etc. themselves. M-RET (or C-M-m) is the sole escape hatch.
6.4. Emacs mode
Entered with C-c C-e. The terminal keeps running, the buffer is read-only,
and standard Emacs bindings fall through to the global map. isearch-forward,
occur, M-x, C-SPC + M-w, arrow keys, wheel scroll - all work unmodified. The
terminal keeps producing output and the buffer keeps growing, but your point
stays where you navigated it (the delayed-redraw path preserves point in Emacs
mode).
Typed keys do not reach the shell - Emacs mode is a "look but don't touch"
view. Self-insert, RET, TAB, DEL fall through to the read-only buffer and
trigger text-read-only, so a stray keystroke cannot accidentally land at the
prompt. Switch to semi-char mode (C-c C-j) when you want to type to the shell.
C-y is the exception: it pastes via bracketed paste as a deliberate action and
snaps point back to the live cursor.
C-c C-e toggles Emacs mode off again (returning to the mode you came from), and
C-c C-t switches to copy mode to freeze the output.
Use this for searching through scrollback while a build is running, filtering
streaming logs with M-x occur, marking and copying across the visible history,
or running any buffer-based command over the terminal's output without having to
freeze it.
6.5. Copy mode
Entered with C-c C-t. The terminal is frozen - no live output updates the
buffer until you exit. Use this when you want to select text precisely without
the terminal scrolling underneath your cursor. The aggressive copy-mode keymap
exits on self-insert, so typing a letter sends it to the terminal and returns to
semi-char mode (controlled by ghostel-readonly-fast-exit).
C-c C-t toggles copy mode off again, and C-c C-e switches to Emacs mode -
read-only but live, so output resumes - without going through semi-char.
| Key | Action |
|---|---|
C-SPC |
Set mark |
M-w / C-w |
Copy selection and exit |
C-n / C-p |
Move line |
M-v / C-v |
Scroll page up / down |
M-< / M-> |
Jump to top / bottom of buffer |
C-c C-n |
Jump to next hyperlink |
C-c C-p |
Jump to previous hyperlink |
C-c M-n |
Jump to next prompt |
C-c M-p |
Jump to previous prompt |
C-l |
Recenter viewport |
q |
Exit without copying |
a - z |
Exit and send key to terminal |
Soft-wrapped newlines are automatically stripped from copied text.
6.6. Mouse selection
Click-and-drag inside a ghostel buffer creates a region. On release,
ghostel-mouse-drag-or-set-region switches input mode so streaming terminal
output cannot clobber the selection - the target is picked by
ghostel-mouse-drag-input-mode (default copy):
copy- enter copy mode. Redraws pause; the selection is stable regardless of where it sits.emacs- enter Emacs mode. The terminal keeps streaming and the buffer becomes read-only; selections wholly in scrollback survive, selections over rows the live program rewrites can still be lost.nil- stay in semi-char. Same selection-survival guarantees asemacs, butM-wis forwarded to the shell so it cannot copy the region - pick this only if you copy via primary selection or the GUI menu.
A single click inside a window that is already selected sets point and then
switches input mode per ghostel-mouse-drag-input-mode, the same as a drag. A
click that focuses a previously-unselected window only gives that window focus:
point lands at the terminal's input cursor (not the click) and the input mode is
unchanged. A click meant just to focus a window therefore never freezes the buffer. (Set
ghostel-mouse-drag-input-mode to nil to turn the click feature off, so a click sets
point like in any Emacs buffer.) When a TUI has DEC mouse-tracking enabled
(1000/1002/1003 - htop, lazygit, etc.) the click is forwarded to the program and none
of the above applies.
Double- and triple-click select the word and line under the cursor respectively, and protect that region the same way a drag does (from any window, focused or not).
The same protection exists for keyboard selections: when a command activates
the mark in semi-char mode - C-SPC (set-mark-command), an expand-region
variant, C-x h, anything that turns the region on - ghostel switches to the
input mode picked by ghostel-mark-activation-input-mode (copy by default,
emacs, or nil to stay in semi-char). This hooks mark activation rather than
any particular key, so custom bindings like set-mark-or-expand trigger it
too. Note that on a TTY Ctrl+Space is indistinguishable from C-@ (NUL) and
is forwarded to the terminal instead; in char mode Ctrl+Space always reaches
the terminal as NUL (GUI and TTY alike).
6.7. Line mode
Entered with C-c C-l. Line mode buffers the user's input locally in Emacs -
no keystrokes are forwarded to the shell while composing. Full Emacs editing
(M-b, M-DEL, C-y yank, transpose-words, etc.) works on the input region.
Pressing RET sends the whole line to the shell in one write; bash receives it
atomically, echoes and executes it.
The terminal stays live: output keeps streaming and the buffer keeps
re-rendering while you compose. A snapshot/restore step in the delayed-redraw
path captures the in-progress input before each redraw and re-inserts it at the
new prompt-end afterwards, so async output or a fresh prompt arriving mid-edit
does not clobber what you typed. After RET, line mode stays active - the next
prompt is found on the following redraw cycle and the input marker moves there.
Line mode uses the terminal cursor as the input-area boundary, so REPLs without
shell integration (python3, irb, sqlite3, …) work too. When OSC 133 prompt
markers are present on the cursor's row, the prompt prefix is recognised and the
input boundary lands right after it. As a fallback without OSC 133, the prompt
prefix is matched against ghostel-prompt-regexp.
Line mode and fullscreen TUIs (vim, less, htop, …) cannot share the same keystroke stream - the TUI needs every key forwarded raw, while line mode buffers them locally. Ghostel handles this transparently: when an alt-screen TUI starts, line mode drops to semi-char so the TUI gets its keys, with a brief message that it will resume on exit. When the TUI exits, line mode resumes at the new prompt.
Pressing C-c C-l on the alt screen does the right thing for what is running.
Over a raw TUI it arms that same auto-resume, so line mode activates when the
TUI exits; an explicit mode switch (C-c C-j, ghostel-char-mode, etc.) cancels
the arming. At an inner shell prompt - a tmux=/=screen session whose OSC 133
markers reach Ghostel via passthrough - C-c C-l enters line mode at that prompt
directly. When those markers do not pass through, C-u C-c C-l forces entry
anyway.
TAB completes the input via ghostel-line-mode-completion-at-point-functions
(comint filename/command completion by default), and optionally layers bash
programmable completion on top via ghostel-line-mode-use-bash-completion.
| Key | Action |
|---|---|
| (letters) | Edit local input (never sent char-by-char) |
RET |
Send the whole line to the shell, stay in line mode |
TAB |
Complete input at point |
C-c C-c |
Discard input and send SIGINT, stay in line mode |
C-d |
Delete char, or send EOF at empty input |
M-p / M-n |
History ring: previous / next entry |
C-a |
Beginning of input on the prompt row, else beginning-of-line |
C-c C-j |
Exit to semi-char mode (discards input) |
6.8. Scrollback search outside copy mode
The full scrollback is always rendered into the buffer as styled text, so
isearch, consult-line, occur, M-x flush-lines, C-x h to select all, and any
other buffer-based command work across the full history in any mode that has a
read-only buffer (Emacs or copy).
7. Features
7.1. Terminal emulation
- Full VT terminal emulation via libghostty-vt.
- 256-color and RGB (24-bit true color) support.
TERM=xterm-ghosttywith bundled terminfo - apps that consult terminfo for capabilities (Claude Code, neovim, tmux, modern TUIs) discover synchronized output (DEC 2026), the Kitty keyboard protocol, true color, colored underlines, focus reporting, etc., and use their fast paths. Synchronized output in particular eliminates the choppy partial-redraw effect when Claude Code repaints over a large scrollback. OSC 52 (clipboard) is supported but intentionally not advertised in the bundled terminfo (see Clipboard). Override viaghostel-term.- OSC 4 / 10 / 11 color queries - TUI programs can query the current palette,
foreground, and background colors, so tools like
duf,btop,delta, and anything else usingtermenvauto-detect the right light/dark theme from the Emacs face colors. - OSC 9 / OSC 777 - desktop notifications and ConEmu progress reports (see Notifications and Progress).
- Text attributes: bold, italic, faint, underline (single/double/curly/dotted/dashed, with color), strikethrough, inverse.
- Cursor styles: block, bar, underline, hollow block.
- Alternate screen buffer (for TUI apps like htop, vim, etc.).
- Scrollback buffer (configurable, default 5 MB / ~5,000 lines, materialized
into the Emacs buffer so
isearch/consult-linework over history).
7.2. Process model
Local ghostel buffers use a native PTY path by default
(ghostel-use-native-pty). The native reader consumes PTY output on a
background thread, updates libghostty-vt asynchronously, and notifies Emacs
through an event pipe when callbacks or redraws are needed. This keeps large
log streams and full-screen TUI redraws from running through Emacs process
filters byte-for-byte.
Remote TRAMP buffers still use Emacs process machinery so TRAMP can spawn the shell on the remote host and apply its file handlers. The rendering and input APIs are shared by both paths.
7.3. Links and file detection
- OSC 8 hyperlinks - clickable URLs emitted by terminal programs (click or
RETto open). - Plain-text URL detection - automatically linkifies
http://andhttps://URLs even without OSC 8 (toggle withghostel-enable-url-detection). - File path detection - patterns like
/path/to/file.el:42become clickable, opening the file at the given line (toggle withghostel-enable-file-detection; tune the path pattern withghostel-file-detection-path-regex).
7.4. Clipboard
- OSC 52 clipboard - terminal programs can set the Emacs kill ring and system
clipboard (opt-in via
ghostel-enable-osc52, useful for remote SSH sessions). The bundledxterm-ghosttyterminfo intentionally does not advertise theMscapability, so apps do not auto-discover it. This avoids silent clipboard drops whenghostel-enable-osc52is at its defaultnil. If you enable OSC 52 and want apps (neovim, tmux) to auto-detect it, install upstream Ghostty's terminfo on the same path or overrideTERMINFO. - Bracketed paste - yank from the kill ring sends text as a bracketed paste so shells handle it correctly.
7.5. Input
- Full keyboard input with the Ghostty key encoder (respects terminal modes, Kitty keyboard protocol).
- Mouse tracking (press, release, drag) via the SGR mouse protocol - TUI apps receive full mouse input.
- Focus events gated by DEC mode 1004.
- Drag-and-drop (file paths and text).
7.6. Password prompt detection
When sudo, ssh, gpg, passwd, etc. ask for a password, ghostel pops up
read-passwd and sends the answer through the PTY - keystrokes never flow through
Emacs's normal key pipeline, so the password does not land in view-lossage,
the recent-keys ring, or any keyboard-macro recording. This is controlled by
ghostel-detect-password-prompts (default t).
Detection has two layers. The primary signal mirrors libghostty's heuristic: the
slave tty is in canonical mode with echo off, read via a small tcgetattr Zig
binding. This catches local programs that flip !ECHO (sudo, ssh's own prompt,
gpg, …). A cursor-row regex fallback (ghostel-password-prompt-regex,
defaulting to comint-password-prompt-regexp) covers cases the tty signal cannot
see, but runs only when the foreground shell is on a remote host
(ghostel--remote-shell-p, derived from the TRAMP default-directory ghostel
keeps in sync via OSC 7). Gating it on remote-only avoids false positives from
local raw-mode TUIs like vim or less, and structural anchoring keeps shell-typed
lines such as $ echo Password: from triggering. See ghostel-debug-start /
ghostel-debug-password-events-show for diagnostics.
The mode line shows = 🔒Password= while a prompt is open. Wrong-password retries are detected automatically (the cursor moves to the new
prompt row). The wire copy of the
password is clear-string'd immediately after sending, so it does not linger in the
heap.
Detection is extensible via ghostel-password-prompt-functions - a chain of
(ROW) -> string-or-nil sources tried in order. The default reads with
read-passwd; users prepend their own (auth-source / KeePass / pass / etc.) and
the default acts as the fallback. The defcustom docstring includes a
TRAMP-aware auth-source-pick-first-password example.
7.7. Shell integration features
- Automatic injection for bash, zsh, and fish - no shell RC edits needed.
- OSC 7 - directory tracking (
default-directoryfollows the shell's cwd, TRAMP-aware for remote hosts). - OSC 133 - semantic prompt markers, enabling prompt-to-prompt navigation with
C-c M-n/C-c M-p. - OSC 2 - title tracking (the buffer is renamed from the terminal title; see
ghostel-buffer-name-function). - OSC 52;e - call whitelisted Emacs functions from shell scripts (see Calling Elisp from the Shell).
- OSC 52 - clipboard support (opt-in, for remote sessions).
INSIDE_EMACSandEMACS_GHOSTEL_PATHenvironment variables.
7.8. Rendering
- Incremental redraw - only dirty rows are re-rendered.
- Timer-based batched updates with adaptive frame rate.
- Immediate redraw for interactive typing echo - PTY output arriving shortly after a keystroke bypasses the timer, eliminating 16-33ms of latency per keypress.
- Asynchronous local PTY output - local PTY output is parsed by the native reader and Emacs is notified only when callbacks or redraws are needed.
- Cursor position updates even without cell changes.
- Theme-aware color palette (syncs with the Emacs theme via
ghostel-sync-theme).
7.9. Inline images (Kitty graphics protocol)
Ghostel renders inline images using the Kitty graphics protocol via libghostty. It supports both placement modes used by real-world tools:
- Traditional placements -
timg,kitty +kitten icat, and any tool that emits direct kitty graphics commands. - Unicode-placeholder placements (U+10EEEE) - used by
yaziand other modern image previewers to anchor images to the buffer's text grid.
Pixel data is rendered through Emacs's built-in image support: PNG payloads are decoded by a vendored stbimage, and raw RGB/RGBA/Gray/GrayAlpha transmissions are converted to PPM in the native module - no external ImageMagick dependency.
XTWINOPS size queries (CSI 14 / 16 / 18 t) are answered so apps can detect
graphics support and pick image dimensions; without that, timg falls back to
half-block rendering even when TERM_PROGRAM=ghostty.
Cell pixel sizes are reported as physical pixels via ghostel-cell-pixel-scale
(default auto, derived from display DPI). On most displays this approximates
standalone Ghostty's output; for pixel-perfect parity (especially on Linux
Wayland with fractional scaling or non-standard DPI), set an explicit number.
7.9.1. Limitations
- Alpha is dropped, not composited. All formats - raw RGBA, GrayAlpha, and PNG - go through an RGBA→PPM conversion that strips the alpha channel (PNGs are decoded to RGBA by libghostty's PNG hook at transmit time, then follow the same path). Transparent pixels render as whatever the underlying color value happens to be (most decoders emit black). Acceptable for thumbnails and screenshots; not ideal for icons with semi-transparent edges.
- Source-rect cropping is not supported. Atlas-style placements that specify a
sub-region of the source image (
x,y,w,hin the kitty protocol) are refused with an explicit error rather than silently mis-rendering. Full-image placements - what timg, yazi, andkitty +kitten icatuse - are unaffected. - Multiple simultaneous virtual placements share rendering. Unicode-placeholder
placements that coexist in the same buffer are rendered as a single image; the
most recent transmission wins.
yazi's preview pane uses one image at a time, so this has not been a problem in practice. - Non-direct mediums are off by default for safety. Only the inline (base64)
medium is enabled; file / temp-file / shared-memory mediums are opt-in via
ghostel-kitty-graphics-mediums. See its docstring for the privilege-escalation reasoning.
7.10. Calling Elisp from the shell
Shell scripts running inside ghostel can call whitelisted Elisp functions via
the ghostel_cmd helper (provided by the shell integration scripts):
ghostel_cmd find-file "/path/to/file" ghostel_cmd message "Hello from the shell"
This uses an OSC 52 escape sequence with a reserved kind byte
(\e]52;e;<payload>\e\\) - a ghostel-private extension. Only functions listed
in ghostel-eval-cmds are allowed.
Default whitelisted commands: find-file, find-file-other-window, dired,
dired-other-window, message.
Add your own with:
(add-to-list 'ghostel-eval-cmds '("magit-status-setup-buffer" magit-status-setup-buffer))
Example shell aliases (add to your .bashrc / .zshrc):
if [[ "${INSIDE_EMACS%%,*}" = 'ghostel' ]]; then # Open a file in Emacs from the terminal e() { ghostel_cmd find-file-other-window "$@"; } # Open dired in another window, defaulting to the current directory dow() { ghostel_cmd dired-other-window "${1:-$PWD}"; } # Open magit for the current directory gst() { ghostel_cmd magit-status-setup-buffer "$(pwd)"; } fi
7.11. Notifications and progress
Ghostel recognises two notification protocols used by terminal programs:
- OSC 9 (iTerm2 form):
ESC ] 9 ; BODY ST- body only. - OSC 777 (rxvt
notify):ESC ] 777 ; notify ; TITLE ; BODY ST- title + body.
Both route to ghostel-notification-function with (TITLE BODY). The default
handler, ghostel-default-notify, uses the alert package when installed - it picks
a sensible backend per platform (osascript on macOS, libnotify on Linux, Growl,
terminal-notifier, etc.) and is configurable via alert-default-style. Install
it from MELPA with M-x package-install RET alert RET.
When alert is not available, ghostel falls back to message, which only appears
in the echo area. Set ghostel-notification-function to nil to silence
notifications entirely, or to your own function to route them elsewhere.
A custom handler receives the title and body and can route them anywhere:
(setq ghostel-notification-function (lambda (title body) (alert body :title (or title "ghostel") :category 'ghostel)))
ConEmu's OSC 9;4 progress protocol is also recognised: build tools, AI agents
like Claude Code, and other long-running commands emit it to report completion
percentage. Ghostel dispatches these to ghostel-progress-function with (STATE
PROGRESS) where STATE is one of remove, set, error, indeterminate, pause and
PROGRESS is an integer 0-100 or nil.
Two built-in handlers are available:
ghostel-default-progress- plain text inmode-line-process:[42%],[...],[err 73%],[paused 25%], or cleared onremove. Zero dependencies.ghostel-spinner-progress- animatesmode-line-processvia spinner.el duringindeterminate(e.g. while Claude Code is working) and falls back to the same text indicator for the other states.
ghostel-progress-function defaults to ghostel-spinner-progress when spinner.el
is on the load-path at ghostel load time, otherwise to ghostel-default-progress.
Pin a specific handler explicitly:
;; Pin to spinner (errors with a hint if spinner.el isn't installed): (setq ghostel-progress-function #'ghostel-spinner-progress) ;; Or stay on the plain text indicator: (setq ghostel-progress-function #'ghostel-default-progress) ;; Pick a different spinner style - see `spinner-types' in spinner.el: (setq ghostel-spinner-type 'horizontal-moving)
7.12. Color palette
The 16 ANSI colors are defined as Emacs faces inheriting from term-color-*:
ghostel-color-black ghostel-color-bright-black ghostel-color-red ghostel-color-bright-red ghostel-color-green ghostel-color-bright-green ghostel-color-yellow ghostel-color-bright-yellow ghostel-color-blue ghostel-color-bright-blue ghostel-color-magenta ghostel-color-bright-magenta ghostel-color-cyan ghostel-color-bright-cyan ghostel-color-white ghostel-color-bright-white
Themes that customize term-color-* faces automatically apply. Customize
individual faces with M-x customize-face.
Default foreground/background are read from the ghostel-default face, which
inherits from default. Customize it to give ghostel terminals different default
colors than the rest of Emacs (e.g. a dark terminal inside a light Emacs):
(set-face-attribute 'ghostel-default nil
:foreground "#cdd6f4"
:background "#1e1e2e")
Bold text coloring follows ghostel-bold-color (nil = same color as normal text,
bright = use the bright ANSI variant, or a fixed #RRGGBB string), matching
Ghostty 1.2.0's bold-color configuration.
8. TRAMP (Remote Terminals)
When default-directory is a TRAMP path (e.g. /ssh:host:/home/user/), M-x
ghostel spawns a shell on the remote host via TRAMP's process machinery. The
ghostel-tramp-shells variable controls which shell to use per TRAMP method:
;; Default configuration (setq ghostel-tramp-shells '(("ssh" login-shell) ; auto-detect via getent ("scp" login-shell) ("docker" "/bin/sh"))) ; fixed shell for containers
Each entry is (METHOD SHELL [FALLBACK]). SHELL can be a path like "/bin/bash"
or the symbol login-shell to auto-detect the remote user's login shell via
getent passwd. FALLBACK is used when detection fails.
OSC 7 directory tracking is TRAMP-aware: when the shell reports a remote
hostname, default-directory is set to the corresponding TRAMP path, reusing the
existing TRAMP prefix (method, user, multi-hop) when available. When no prefix
exists, the method defaults to tramp-default-method; set
ghostel-tramp-default-method to override it for ghostel specifically (e.g.
"scp", or "rpc" with emacs-tramp-rpc).
8.1. Remote shell integration
By default, shell integration scripts are not injected for remote sessions. There are two ways to enable it.
8.1.1. Option 1: Automatic injection (recommended for convenience)
Set ghostel-tramp-shell-integration to t to have ghostel automatically transfer
integration scripts to the remote host:
(setq ghostel-tramp-shell-integration t)
This creates small temporary files on the remote host (cleaned up when the terminal exits). You can also enable it for specific shells only:
(setq ghostel-tramp-shell-integration '(bash zsh))
8.1.2. Option 2: Manual setup (recommended for permanent remote hosts)
Copy the integration scripts from ghostel's etc/shell/ directory to each remote
host (e.g. ~/.local/share/ghostel/) and source them from your shell
configuration. Optionally co-locate the bundled xterm-ghostty terminfo there
too - the wrapper that launches a TRAMP-spawned remote shell prepends
~/.local/share/ghostel/terminfo to the terminfo search path, so ghostty-aware
apps (Claude Code, neovim, tmux, …) get their fast paths without needing tic
or ~/.terminfo (see Manual install for that alternative). From a local shell:
ssh REMOTE 'mkdir -p ~/.local/share/ghostel/terminfo' scp "$EMACS_GHOSTEL_PATH"/etc/shell/ghostel.{bash,zsh,fish} REMOTE:.local/share/ghostel/ scp -r "$EMACS_GHOSTEL_PATH"/etc/terminfo/{x,78} REMOTE:.local/share/ghostel/terminfo/
($EMACS_GHOSTEL_PATH is set inside ghostel buffers; outside, substitute the
install path of the ghostel package. The terminfo scp is optional - without it,
TRAMP-spawned remote shells fall back to TERM=xterm-256color, which still has
working echo and basic colors but no ghostty-specific fast paths.)
Then add the appropriate gate to the remote shell config:
# bash - ~/.bashrc on the remote host: if [[ "${INSIDE_EMACS%%,*}" = 'ghostel' || "$TERM" = 'xterm-ghostty' ]]; then source ~/.local/share/ghostel/ghostel.bash fi
# zsh - ~/.zshrc on the remote host: if [[ "${${INSIDE_EMACS-}%%,*}" = 'ghostel' || "$TERM" = 'xterm-ghostty' ]]; then source ~/.local/share/ghostel/ghostel.zsh fi
# fish - ~/.config/fish/config.fish on the remote host:
if string match -qr '^ghostel(,|$)' -- "$INSIDE_EMACS"; or test "$TERM" = 'xterm-ghostty'
source ~/.local/share/ghostel/ghostel.fish
end
The two-clause gate covers both ways a remote ghostel shell can be reached:
- TRAMP-launched ghostel (
M-x ghostelfrom a/ssh:host:path) rewritesINSIDE_EMACStoghostel,tramp:VERon the remote. The${INSIDE_EMACS%%,*}prefix match catches it. - Plain
ssh REMOTEfrom a local ghostel buffer cannot propagateINSIDE_EMACSover ssh -SetEnvrequires server-sideAcceptEnvto take effect. Instead, the gate falls back onTERM, which the SSH protocol does propagate natively. Ghostel setsTERM=xterm-ghosttyin the local PTY shell environment (controlled byghostel-term), so anysshspawned from inside the buffer inherits and forwards that value.
False positives - situations where the second clause matches but the session is
not actually ghostel - include any ssh from a non-ghostel ghostty terminal,
nested ssh hops carrying the same TERM through, and anyone who manually exports
TERM=xterm-ghostty. Sourcing the integration in those cases is harmless (OSC 7
/ OSC 133 work in plain ghostty too; ghostel_cmd becomes a no-op without ghostel
on the other end).
If you customize ghostel-term to something other than xterm-ghostty, the
second clause will not match. Drop it and rely on TRAMP-launched ghostel for
remote integration, or replace it with a match against your customized TERM.
The integration scripts provide directory tracking (OSC 7), prompt navigation
(OSC 133), and ghostel_cmd for calling Elisp from the shell.
8.2. Remote xterm-ghostty terminfo
Ghostel sets TERM=xterm-ghostty so apps inside the buffer get the full
capability set (synchronized output, Kitty keyboard, etc.). That same TERM
value gets inherited by anything spawned inside the buffer - including ssh
REMOTE and M-x ghostel from a TRAMP default-directory. Remote hosts without
the xterm-ghostty entry will then print Error opening terminal: xterm-ghostty.
ghostel-ssh-install-terminfo (default auto) handles both cases. auto is
enabled when ghostel-tramp-shell-integration is on, so turning on remote
integration also turns on terminfo install - one switch.
8.2.1. TRAMP-launched ghostel
M-x ghostel from a TRAMP path (/ssh:host:/path/) spawns the shell on the
remote. Ghostel pushes the bundled compiled terminfo to a remote temp dir over
the existing TRAMP connection (no extra ssh round-trip), sets TERMINFO=<that
dir> in the remote shell's env, and cleans up on exit. Both Linux (x/, g/)
and macOS (78/, 67/) layouts are written so any ncurses or BSD libcurses finds
it. Nothing persists on the remote.
8.2.2. Outbound ssh from a local ghostel buffer
The bundled bash/zsh/fish integration shadows ssh with a function that:
- Resolves the canonical target via
ssh -G(normalises sshconfig aliases). - Looks up the target in
~/.cache/ghostel/ssh-terminfo-cache. The cache key includes a hash of the local terminfo, so libghostty bumps automatically invalidate it. Cache hit → connect with the rememberedTERM. - On miss, runs a single setup ssh that probes whether the entry already exists
on the remote, and if not, installs it via
tic -x -into~/.terminfo/. Recordsok(usexterm-ghostty) orskip(usexterm-256color) in the cache. - Runs the user's actual ssh with the resolved
TERM.
The setup ssh is one extra connection per new host. Without ControlMaster you will see two auth prompts the first time. Strongly recommended:
# ~/.ssh/config Host * ControlMaster auto ControlPath ~/.ssh/cm-%r@%h:%p ControlPersist 60s
With this, the setup connection and the real connection share a single auth.
Subsequent connections within ControlPersist are free.
The cache key includes a hash of the local terminfo, so libghostty bumps
automatically invalidate the cache. It does NOT notice when a remote's terminfo
changes out-of-band (system update, manual tic). Run M-x
ghostel-ssh-clear-terminfo-cache to force a re-probe.
Verified working from macOS to Linux remotes. Mixed macOS-to-macOS or BSD
targets inherit tic's native hashed-dir layout (~/.terminfo/<hex>/); infocmp
reads the same path so they pair correctly.
Skip-install heuristics:
ssh HOST cmd(user passes a remote command): the wrapper skips install for that call to avoid clashing with the user's command. Connects with the cachedTERMif known, otherwisexterm-256color. The next interactivessh HOSTtriggers install.ssh -V,ssh -h, etc. (no host resolved): pass through.- No
infocmplocally: pass through.
Per-call escape: prefix with GHOSTEL_SSH_KEEP_TERM=1 to bypass the wrapper
entirely.
8.2.3. Manual install (no auto-machinery)
If you would rather not have ghostel touch remote hosts (and do not want the
auto-cache), set (setq ghostel-ssh-install-terminfo nil) and install the entry
yourself once per host.
Pipe the local entry across:
infocmp -x xterm-ghostty | ssh REMOTE 'mkdir -p ~/.terminfo && tic -x -'
Or copy the bundled compiled binary from the package directory:
ssh REMOTE 'mkdir -p ~/.terminfo/x' scp <package-dir>/etc/terminfo/x/xterm-ghostty REMOTE:~/.terminfo/x/ # Ghostty also looks in 78/ on macOS: ssh REMOTE 'uname' | grep -q Darwin && { ssh REMOTE 'mkdir -p ~/.terminfo/78' scp <package-dir>/etc/terminfo/78/xterm-ghostty REMOTE:~/.terminfo/78/ }
After this, every shell on the remote sees xterm-ghostty and ghostel's outbound
ssh wrapper is unnecessary.
8.2.4. Drop the Ghostty advertisement entirely
Set (setq ghostel-term "xterm-256color") to drop TERM=xterm-ghostty locally.
No advertisement, no terminfo gymnastics, no synchronized output fast-path
either.
9. Configuration
All variables can be customized via M-x customize-group RET ghostel RET. The
tables below group them by area; defaults shown are the out-of-the-box values.
9.1. Process and environment
| Variable | Default | Description |
|---|---|---|
ghostel-shell |
$SHELL (else /bin/sh) |
Shell program to run. A string, or a list of executable + args. |
ghostel-use-native-pty |
t |
Use the native PTY reader for local buffers. Remote TRAMP buffers always use Emacs processes. |
ghostel-term |
"xterm-ghostty" |
Value of TERM. Advertises the bundled terminfo's full capability set. Set to "xterm-256color" to fall back. |
ghostel-environment |
nil |
Extra env vars (list of "KEY=VALUE" strings; bare "KEY" unsets). Honored via dir-locals. |
ghostel-macos-login-shell |
t on macOS |
Wrap the shell via login(1) so it starts as a login shell (matches Terminal.app / Ghostty). |
ghostel-pre-spawn-hook |
nil |
Hook run just before make-process; process-environment is bound for last-minute env tweaks. |
ghostel-buffer-name |
"*ghostel*" |
Default buffer name. |
ghostel-project-buffer-scope |
both |
How project commands scope buffers: default-directory, identity, or both. |
ghostel-buffer-name-function |
ghostel-buffer-name-by-title |
Maps OSC 2 title + default-directory to a buffer name (nil return keeps it); nil disables. |
ghostel-kill-buffer-on-exit |
t |
Kill the buffer when the shell process exits. |
ghostel-query-before-killing |
auto |
Confirm before killing a live buffer / exiting Emacs: t, nil, or auto (only while a command runs). |
ghostel-exit-functions |
nil |
Hook run with (BUFFER EVENT) when the terminal process exits. |
ghostel-command-start-functions |
(ghostel--query-before-killing-on-cmd-start) |
Hook run with (BUFFER) on OSC 133 C (command start). |
ghostel-command-finish-functions |
(ghostel--query-before-killing-on-cmd-finish) |
Hook run with (BUFFER EXIT-STATUS) on OSC 133 D (command finish). |
9.2. Native module
| Variable | Default | Description |
|---|---|---|
ghostel-module-auto-install |
ask |
What to do when the native module is missing: ask, download, compile, or nil. |
ghostel-module-directory |
nil |
Directory holding the native module (nil = package directory). |
ghostel-github-release-url |
.../releases |
Base URL for pre-built module downloads (customize for a fork/mirror). |
9.3. TRAMP and remote
| Variable | Default | Description |
|---|---|---|
ghostel-shell-integration |
t |
Auto-inject shell integration (bash/zsh/fish). |
ghostel-tramp-shells |
(see above) | Shell to use per TRAMP method, with login-shell auto-detection. |
ghostel-tramp-shell-integration |
nil |
Auto-inject shell integration for remote TRAMP sessions (t, nil, or a list of shells). |
ghostel-tramp-default-method |
nil |
TRAMP method for new remote paths from OSC 7 (nil = tramp-default-method). |
ghostel-ssh-install-terminfo |
auto |
Install xterm-ghostty terminfo on remote hosts. auto follows ghostel-tramp-shell-integration. |
9.4. Rendering and performance
| Variable | Default | Description |
|---|---|---|
ghostel-max-scrollback |
5 MB | Max scrollback in bytes (materialized into the buffer; ~5,000 rows at 80 cols). |
ghostel-timer-delay |
0.033 |
Base redraw delay in seconds (~30 fps). |
ghostel-adaptive-fps |
t |
Adaptive frame rate (shorter delay after idle, stop timer when idle). |
ghostel-immediate-redraw-interval |
0.05 |
Max seconds since last keystroke for immediate redraw. |
ghostel-full-redraw |
nil |
Always do full redraws instead of incremental updates. |
ghostel-inhibit-redraw-functions |
nil |
Abnormal hook (each fn called with the buffer); if any returns non-nil the redraw is deferred. Used by add-ons such as ghostel-ime. |
ghostel-cell-pixel-scale |
auto |
Physical:logical pixel ratio for cell-size reporting (auto derives from DPI). |
ghostel-glyph-scale-floor |
0.0 |
Minimum scale for glyphs that don't fit the cell (0.0 preserves grid alignment; 1.0 renders CJK at natural size). |
9.5. Images
| Variable | Default | Description |
|---|---|---|
ghostel-kitty-graphics-storage-limit |
320 MiB | Per-terminal cap on kitty graphics storage. 0 disables kitty graphics entirely. |
ghostel-kitty-graphics-mediums |
nil |
Opt-in image-loading mediums beyond inline base64: subset of (file temp-file shared-mem). |
9.6. Links, clipboard, and detection
| Variable | Default | Description |
|---|---|---|
ghostel-enable-url-detection |
t |
Linkify plain-text URLs in terminal output. |
ghostel-enable-file-detection |
t |
Linkify file:line references in terminal output. |
ghostel-file-detection-path-regex |
(regex) | Path portion of the file:line[:col] detection pattern. |
ghostel-plain-link-detection-delay |
0.1 |
Delay before redraw-triggered link detection runs (0 = immediate). |
ghostel-enable-osc52 |
nil |
Allow apps to set the clipboard via OSC 52. |
ghostel-eval-cmds |
(see above) | Whitelisted functions for OSC 52;e eval. |
9.7. Password prompts
| Variable | Default | Description |
|---|---|---|
ghostel-detect-password-prompts |
t |
Watch for password prompts and pop read-passwd. |
ghostel-password-prompt-functions |
(ghostel--default-password-source) |
Sources tried in order to obtain a password (prepend auth-source, etc.). |
ghostel-password-prompt-regex |
comint-password-prompt-regexp |
Cursor-row regex fallback (remote shells only). |
ghostel-password-prompt-debounce |
0.2 |
Seconds to wait after a rising edge before opening read-passwd. |
9.8. Notifications and progress
| Variable | Default | Description |
|---|---|---|
ghostel-notification-function |
ghostel-default-notify |
Handler for OSC 9 / OSC 777 notifications (nil disables). |
ghostel-progress-function |
spinner if available, else ghostel-default-progress |
Handler for OSC 9;4 progress (nil disables). |
ghostel-spinner-type |
progress-bar |
Spinner style for ghostel-spinner-progress (see spinner-types). |
9.9. Input and interaction
| Variable | Default | Description |
|---|---|---|
ghostel-keymap-exceptions |
("C-c" "C-x" ...) |
Keys passed through to Emacs in semi-char mode. |
ghostel-ignore-cursor-change |
nil |
Ignore terminal-driven cursor shape/visibility changes. |
ghostel-readonly-fast-exit |
t |
In copy/Emacs modes, exit on q, C-g, or any self-insert key. |
ghostel-readonly-fake-cursor |
t |
Draw a hint cursor at the live terminal position in copy/Emacs modes. |
ghostel-mouse-drag-input-mode |
copy |
Mode to enter after a click/drag/multi-click selection: copy, emacs, or nil. |
ghostel-mark-activation-input-mode |
copy |
Mode to enter when a command activates the mark: copy, emacs, or nil. |
ghostel-word-boundary-string |
(see docstring) | Characters that terminate words for double-click selection and word motion (mirrors Ghostty's selection-word-chars). |
ghostel-scroll-on-input |
t |
Jump to the bottom when typing while scrolled into scrollback. |
ghostel-bold-color |
nil |
How bold text is colored: nil, bright, or a #RRGGBB string. |
9.10. Line mode
| Variable | Default | Description |
|---|---|---|
ghostel-prompt-regexp |
(see docstring) | Prompt-prefix regex fallback when OSC 133 markers are absent. |
ghostel-line-mode-history-size |
200 |
Max entries in the line-mode history ring. |
ghostel-line-mode-completion-at-point-functions |
(comint-completion-at-point) |
Capfs activated for line-mode TAB. |
ghostel-line-mode-use-bash-completion |
auto |
Layer bash programmable completion onto TAB (auto, t, nil). |
ghostel-line-mode-bash-completion-prespawn |
nil |
Eagerly start the bash-completion subprocess on line-mode entry. |
10. Optional integrations
These features live in separate files (or, for evil, a separate package) so the core stays lean. Load only what you use.
10.1. Evil-mode
Ghostel includes optional evil-mode support via evil-ghostel.el. It
synchronizes the terminal cursor with Emacs point during evil state transitions
so that normal-mode navigation (hjkl etc.) works correctly.
evil-ghostel is distributed as an independent MELPA package that depends on
ghostel. Install it alongside ghostel:
(use-package evil-ghostel :ensure t :after (ghostel evil) :hook (ghostel-mode . evil-ghostel-mode))
Or from source (Emacs 30+); :lisp-dir points package-vc at this extension's
subdirectory inside the ghostel monorepo:
(use-package evil-ghostel :vc (:url "https://github.com/dakra/ghostel" :lisp-dir "extensions/evil-ghostel" :rev :newest) :after (ghostel evil) :hook (ghostel-mode . evil-ghostel-mode))
When evil-ghostel-mode is active:
- Ghostel starts in insert state (terminal input works normally).
- Pressing ESC enters normal state and snaps point to the terminal cursor.
- Normal-mode navigation (
h,j,k,l,w,b,e,0,$, …) works as expected. - Insert/append (
i,a,I,A) sync the terminal cursor to point before entering insert state. - Delete (
d,dw,dd,D,x,X) yanks text to the kill ring and deletes via the shell. - Change (
c,cw,cc,C,s,S) deletes then enters insert state. - Replace (
r) replaces the character under the cursor. - Paste (
p,P) pastes from the kill ring via bracketed paste. - Undo (
u) sends readline undo (Ctrl+_). - Cursor shape follows evil state (block for normal, bar for insert).
- Alt-screen programs (vim, less, htop) are unaffected.
The initial state and the ESC behavior are configurable via
evil-ghostel-initial-state (default insert) and evil-ghostel-escape (default
auto).
10.2. Compilation mode
ghostel-compile runs a shell command in a ghostel buffer and presents the
result like M-x compile - a compilation-mode-style header, footer, error
highlighting, and next-error navigation - but backed by a real TTY so programs
that probe isatty(3) (coloured output, progress bars, curses tools) behave as
they do in a normal shell.
Each invocation spawns a fresh process via shell-file-name -c COMMAND through a
PTY rendered by ghostel - no interactive shell sits between the command and the
user, so multi-line shell scripts are passed through verbatim and no
shell-integration setup is required. The process sentinel delivers the real
exit status.
ghostel-compile inherits the same TERM=xterm-ghostty and TERMINFO…= env as
M-x ghostel, so build output gets synchronized output, true color, etc. If a
test runner or build tool gets confused by the unfamiliar TERM, set (setq
ghostel-term "xterm-256color").
(require 'ghostel-compile) (global-set-key (kbd "C-c c") #'ghostel-compile)
Commands:
| Command | Description |
|---|---|
M-x ghostel-compile |
Run a command in a read-only ghostel buffer (uses compile-command). |
C-u M-x ghostel-compile |
Prompt for the command and run it in an interactive (writable) buffer. |
M-x ghostel-recompile |
Re-run the last command in its original directory (preserves launch mode). |
M-x ghostel-compile-global-mode |
Route all compile-style calls through ghostel (opt-in). |
A run looks like a M-x compile buffer:
-*- mode: ghostel-compile -*- Compilation started at Wed Apr 15 08:30:11 make -j4 test ...command output (live, with full TTY)... Compilation finished at Wed Apr 15 08:30:19, duration 8.20 s
By default the buffer is read-only and navigable from the start - just like a
M-x compile buffer. g reruns, n / p walk errors (parsed once the run
finishes), RET jumps to the source. Keystrokes do not reach the running
process, so the "compile-mode" UX (read coloured output, kill with C-c C-c) is
available even mid-run.
Pass a prefix arg (C-u M-x ghostel-compile, mirroring C-u M-x compile) to launch
in interactive mode instead - the buffer stays writable for the duration of the
run, so programs like htop and less, test runners that prompt for input, or
anything that wants live keystrokes work. ghostel-recompile (g) preserves
whichever mode the buffer was launched in.
When the command finishes, the live process and ghostel renderer are torn down
and the buffer's major mode is switched to ghostel-compile-view-mode (derived
from compilation-mode). mode-line-process shows :run while the command runs
and :exit [N] afterwards; an interactive run reads :run/i instead of :run.
10.2.1. Live mode switching
Sometimes a command turns out to need input - a read -p, a git push password
prompt, a test runner asking y/n, or you would like to attach to htop mid-run.
Two keys switch the buffer's state without restarting the process:
| Key | Action |
|---|---|
C-c C-j |
Switch to interactive (writable terminal) |
C-c C-e / C-c C-t |
Switch back to read-only / compile-mode-style |
(C-c C-t mirrors ghostel-mode's key for entering copy-mode, so the same muscle
memory works in compile buffers.) Both keys are bound by
ghostel-compile-toggle-mode, a buffer-local minor mode auto-enabled in compile
buffers. Subsequent recompiles preserve whichever state you last switched to.
10.2.2. Keybindings (ghostel-compile-view-mode, also active during a read-only run)
| Key | Action |
|---|---|
g |
Re-run via ghostel-recompile |
n / p |
Move point to next / previous error (no auto-open) |
RET / mouse-2 |
Jump to the source of the error under point |
M-g n / M-g p |
Standard next-error / previous-error |
C-c C-c |
compile-goto-error (same as RET) |
C-c C-k |
kill-compilation - interrupt the running process |
C-c C-j / C-c C-e / C-c C-t |
Switch to interactive / read-only (see above) |
These standard compile options are honoured: compile-command /
compile-history (shared with M-x compile), compilation-read-command,
compilation-ask-about-save, compilation-auto-jump-to-first-error, and
compilation-finish-functions. Output scrolling is always on.
ghostel-recompile runs in the directory the original ghostel-compile was
invoked from, regardless of which buffer you are in when you press g.
10.2.3. Make compile / recompile / project-compile use ghostel
Enable ghostel-compile-global-mode to advise compilation-start so every caller
that goes through it - M-x compile, M-x recompile, M-x project-compile, and any
third-party command that uses compilation-start - runs in a ghostel buffer
automatically.
(require 'ghostel-compile) (ghostel-compile-global-mode 1)
How calls are routed:
- Plain
M-x compile(or any caller passingMODE=nil,compilation-mode, or acompilation-modesubclass) → read-only ghostel buffer. A subclass is honoured: its error-regexp, font-lock keywords, and keymap take effect when the buffer is finalized. C-u M-x compile(i.e.compilation-start COMMAND t, the comint variant) → interactive ghostel buffer instead of stockcomint-mode.grep-modefalls through to the stockcompilation-startimplementation, because its output parsing and window-management conventions do not fit a live TTY. Extendghostel-compile-global-mode-excluded-modesto opt other modes out.
Ghostel-specific customisation:
| Option | Effect |
|---|---|
ghostel-compile-buffer-name |
Buffer name (default *ghostel-compile*). |
ghostel-compile-finished-major-mode |
Major mode after each run (default ghostel-compile-view-mode; nil = stay in ghostel-mode). |
ghostel-compile-finish-functions |
Ghostel-specific finish hook (runs alongside compilation-finish-functions). |
ghostel-compile-global-mode-excluded-modes |
Modes for which the global advice falls through to stock compile (default (grep-mode)). |
ghostel-compile-debug |
Log lifecycle events to *Messages* (default nil). |
10.2.4. Hooks for your own integrations
Outside of a compile buffer, two hooks let you react to any shell command in any ghostel buffer:
ghostel-command-start-functions- called with(BUFFER)when the shell emits OSC 133C(a command starts running).ghostel-command-finish-functions- called with(BUFFER EXIT-STATUS)when the shell emits OSC 133D(a command finishes).
Errors raised by individual hook functions are caught and logged so one bad consumer cannot break the rest.
10.3. Eshell integration
ghostel-eshell-visual-command-mode makes eshell run "visual" commands - programs
in eshell-visual-commands, eshell-visual-subcommands, and
eshell-visual-options (vim, htop, less, top, git log's pager, …) - inside a
dedicated ghostel buffer instead of the default term-mode fallback, so they get
a real terminal emulator.
(require 'ghostel-eshell) (add-hook 'eshell-load-hook #'ghostel-eshell-visual-command-mode)
When the program exits, the buffer stays on [Process exited] so you can read any
remaining output (window point snaps to the end so it is visible without
scrolling). Press q to dismiss the dead buffer. Set
eshell-destroy-buffer-when-process-dies to t to kill the buffer automatically on
exit instead.
To run an ad-hoc command in a ghostel buffer without editing
eshell-visual-commands, use the ghostel eshell built-in:
~ $ ghostel nethack
Add a shorter alias if you like:
(defalias 'eshell/v 'eshell/ghostel) ;; then: ~ $ v nethack
| Option | Effect |
|---|---|
ghostel-eshell-track-title |
When non-nil, let programs rename the visual-command buffer via OSC title escapes. Default nil (keeps *vim* stable). |
The public primitive behind the mode is ghostel-exec BUFFER PROGRAM &optional
ARGS, which launches an arbitrary program in a ghostel buffer with no shell
integration applied. Useful for building your own integrations.
10.4. Comint integration
ghostel-comint-mode replaces comint's built-in ansi-color-process-output with a
stream filter that runs every chunk of process output through libghostty-vt's VT
parser. In M-x shell (and any other comint-derived buffer - REPLs, etc.) output
renders with the same SGR fidelity a real ghostel terminal would give it, plus
OSC 8 hyperlinks and OSC 7 directory tracking.
(require 'ghostel-comint) (add-hook 'shell-mode-hook #'ghostel-comint-mode)
Or, to enable it for every comint-derived buffer at once:
(ghostel-comint-global-mode 1)
What you get over the stock filter (and xterm-color):
| Feature | Stock ansi-color |
xterm-color | ghostel-comint |
|---|---|---|---|
| ANSI 8 / bright / 256 / truecolor | yes | yes | yes |
| Italic, bold, faint, strike-through, overline, inverse | partial | yes | yes |
Curly / double / dotted / dashed underline (\e[4:3m, …) |
- | - | yes |
Underline color (\e[58;...m) |
- | - | yes |
OSC 8 hyperlinks (gh, git, ls --hyperlink=auto) |
- | - | yes |
| OSC 7 working-directory updates | - | - | yes |
| DCS / APC / SS3 sequences consumed cleanly | - | - | yes |
It is still a stream filter - not a full terminal. Cursor-positioning escapes,
alt-screen entry (\e[?1049h), and full-screen redraws are silently dropped:
programs like htop or less will not render correctly under it. Use M-x ghostel
(a real terminal) for those.
CR / BS / TAB pass through unchanged so comint's own comint-carriage-motion
filter continues to handle progress bars, read -s prompts, etc.
For best performance, xterm-color's advice to disable font-locking in shell
buffers applies here too - see the docstring of ghostel-comint-mode.
10.5. Emacs Lisp input methods
Some Emacs Lisp input methods (hangul-input-method is the common example)
commit text by inserting it into the current buffer instead of returning key
events. Inside a ghostel buffer that insert lands in the buffer but is never
sent to the PTY, so the next redraw erases it.
ghostel-ime-mode is an optional minor mode that wraps the buffer-local
input-method-function. When the input method commits by buffer insertion, the
wrapper deletes the transient insert and forwards the committed text to the PTY
as UTF-8, letting the shell echo it back through the normal redraw path. While a
Quail-style composition is in flight it also asks ghostel to defer redraws (via
ghostel-inhibit-redraw-functions) so the renderer does not rewrite the buffer
mid-composition. GUI native preedit handling is unaffected.
(use-package ghostel-ime :hook (ghostel-mode . ghostel-ime-mode))
This generalizes to any Lisp input method that uses quail-overlay (Korean
Hangul, Japanese, Chinese, …).
11. Commands
| Command | Description |
|---|---|
M-x ghostel |
Open a new terminal (create a new buffer with a prefix arg). |
M-x ghostel-project |
Open a terminal in the current project root (new buffer with prefix arg). |
M-x ghostel-other |
Switch to the next terminal or create one. |
M-x ghostel-next |
Cycle to the next ghostel buffer (sorted by name, wraps). |
M-x ghostel-previous |
Cycle to the previous ghostel buffer. |
M-x ghostel-list-buffers |
Pick a ghostel buffer via read-buffer. |
M-x ghostel-project-next |
Cycle to the next ghostel buffer in the current project. |
M-x ghostel-project-previous |
Cycle to the previous ghostel buffer in the current project. |
M-x ghostel-project-list-buffers |
Pick a project-scoped ghostel buffer. |
M-x ghostel-clear |
Clear screen and scrollback. |
M-x ghostel-clear-scrollback |
Clear scrollback only. |
M-x ghostel-semi-char-mode |
Switch to semi-char input mode (default). |
M-x ghostel-char-mode |
Switch to char input mode. |
M-x ghostel-emacs-mode |
Switch to Emacs input mode (read-only, live). |
M-x ghostel-copy-mode |
Enter copy mode (frozen). |
M-x ghostel-line-mode |
Switch to line input mode. |
M-x ghostel-copy-all |
Copy the entire scrollback to the kill ring. |
M-x ghostel-paste |
Paste from the kill ring. |
M-x ghostel-send-next-key |
Send the next key literally. |
M-x ghostel-next-prompt |
Jump to the next shell prompt. |
M-x ghostel-previous-prompt |
Jump to the previous shell prompt. |
M-x ghostel-next-hyperlink |
Jump to the next hyperlink (OSC 8, URL, file ref). |
M-x ghostel-previous-hyperlink |
Jump to the previous hyperlink. |
M-x ghostel-force-redraw |
Force a full terminal redraw. |
M-x ghostel-debug-typing-latency |
Measure per-keystroke typing latency. |
M-x ghostel-sync-theme |
Re-sync the color palette after a theme change. |
M-x ghostel-ssh-clear-terminfo-cache |
Clear the outbound-ssh terminfo install cache (force re-probe). |
M-x ghostel-download-module |
Download the pre-built native module. |
M-x ghostel-module-compile |
Compile the native module from source. |
11.1. Sending input from Lisp
For packages that need to inject input into a running ghostel buffer (agent integrations, custom keymaps, …), two public functions are provided:
(ghostel-send-string "ls -la\n") ; send raw bytes, newline included (ghostel-send-key "return") ; send a named key through the encoder (ghostel-send-key "a" "ctrl") ; C-a - respects the current terminal mode (ghostel-send-key "up" "shift,ctrl") ; modifiers are comma-separated
Both operate on the current buffer; wrap in with-current-buffer when driving
another ghostel buffer. Calling either outside a ghostel buffer signals a
user-error.
11.2. Project integration
ghostel-project opens a terminal in the current project's root directory with a
project-prefixed buffer name. To make it available from project-switch-project
(C-x p p):
(add-to-list 'project-switch-commands '(ghostel-project "Ghostel") t)
12. Running tests
Tests use ERT. The Makefile provides convenient targets:
make test # pure Elisp tests (no native module required) make all # build + test + lint make bench-quick # quick benchmark sanity check
You can also run tests directly:
# Pure Elisp tests (no native module required) emacs --batch -Q -L . -l ert -l test/ghostel-test.el -f ghostel-test-run-elisp # Full test suite (requires the built native module) emacs --batch -Q -L . -l ert -l test/ghostel-test.el -f ghostel-test-run
Tests are split into two groups by ERT tag. Elisp tests (make test) have no
native tag and require no Zig module. Native tests (make test-native) are
tagged native and require the built module. Additional targets: make test-zig
(Zig unit tests), make test-all (everything), and make test-evil (the
evil-ghostel extension).
13. Performance
Ghostel includes a benchmark suite comparing throughput against other Emacs
terminal emulators: vterm (native module), eat (pure Elisp), and Emacs's
built-in term.
The cross-emulator benchmark streams 1 MB of data through a real process pipe and routes it through each backend's production filter, matching actual terminal usage. To keep the comparison fair, every backend - ghostel included - is driven on this common Emacs-process path; ghostel's default local path is the native Zig PTY, which reads asynchronously off the main thread and is faster still (measured separately below under Native vs Emacs PTY). All backends are configured with ~1,000 lines of scrollback (matching vterm's default). Results on Apple M4 Max, Emacs 31.0.50:
| Backend | Plain ASCII | URL-heavy |
|---|---|---|
| ghostel | 81 MB/s | 77 MB/s |
| ghostel (no detect) | 78 MB/s | 75 MB/s |
| vterm | 34 MB/s | 28 MB/s |
| eat | 4.9 MB/s | 3.8 MB/s |
| term | 5.8 MB/s | 4.9 MB/s |
Ghostel scans terminal output for URLs and file paths, making them clickable.
Detection runs on a coalesced timer outside the redraw hot path, so enabling it
costs essentially nothing on streaming throughput - the "no detect" row shows
what you get with ghostel-enable-url-detection and ghostel-enable-file-detection
set to nil. The other emulators do not have this feature.
13.1. Native vs Emacs PTY
The table above drives ghostel on the Emacs-process path so it can be compared
apples-to-apples against vterm/eat/term. For local shells ghostel defaults to
its own native PTY (ghostel-use-native-pty), where a Zig background thread
reads the PTY and feeds libghostty-vt directly, waking Emacs only when the read
would block. This avoids running the per-chunk filter on the main thread, so
bulk output is faster and the UI stays responsive during floods.
bench/run-bench.sh --backends runs the same cat workload through both
backends and reports the ratio. On a sustained multi-MB dump the native path
runs roughly 2x the Emacs path, and the gap widens with size - the remaining
distance to a standalone GPU terminal is Emacs's own buffer-materialization and
redisplay cost, which both backends share.
13.2. Typing latency
Interactive keystrokes are optimized separately from bulk throughput. When you
type a character, the PTY echo is detected and rendered immediately (bypassing
the 33ms redraw timer), so the character appears on screen with minimal delay.
Use M-x ghostel-debug-typing-latency to measure the end-to-end latency on your
system - it reports per-keystroke PTY, render, and total latency with
min/median/p99/max statistics.
Run the benchmarks yourself:
bench/run-bench.sh # full suite (throughput) bench/run-bench.sh --quick # quick sanity check bench/run-bench.sh --backends # native vs Emacs PTY (ghostel only)
14. Ghostel vs vterm
Both ghostel and vterm are native-module terminal emulators for Emacs. Ghostel uses libghostty-vt (Zig) as its VT engine; vterm uses libvterm (C), the same library powering Neovim's built-in terminal.
14.1. Feature comparison
| Feature | ghostel | vterm |
|---|---|---|
| True color (24-bit) | yes | yes |
| OSC 4/10/11 color queries | yes | no |
| Bold / italic / faint | yes | yes |
| Underline styles (5 types) | yes | no |
| Underline color | yes | no |
| Strikethrough | yes | yes |
| Cursor styles | 4 types | 3 types |
| OSC 8 hyperlinks | yes | no |
| Plain-text URL/file detection | yes | no |
| OSC 9 / 777 notifications | yes | no |
| OSC 9;4 progress reports | yes | no |
| Kitty graphics protocol | yes | no |
| Kitty keyboard protocol | yes | no |
| Mouse passthrough (SGR) | yes | no |
| Bracketed paste | yes | yes |
| Alternate screen | yes | yes |
| Shell integration auto-inject | yes | no |
| Prompt navigation (OSC 133) | yes | yes |
| Elisp eval from shell | yes | yes |
| TRAMP remote terminals | yes | yes |
| OSC 52 clipboard | yes | yes |
| Copy mode | yes | yes |
| Char mode (runtime toggle) | yes | no |
| Line mode (local editing) | yes | no |
| Emacs mode (read-only, live) | yes | no |
| Drag-and-drop | yes | no |
| Password prompt detection | yes | no |
| Auto module download | yes | no |
| Scrollback default | ~5,000 | 1,000 |
| PTY throughput (plain ASCII) | 81 MB/s | 34 MB/s |
| Default redraw rate | ~30 fps | ~10 fps |
14.2. Key differences
Terminal engine. libghostty-vt comes from Ghostty, a modern GPU-accelerated terminal, and supports the Kitty keyboard/mouse protocols, rich underline styles, and OSC 8 hyperlinks. libvterm targets VT220/xterm emulation and is more conservative in protocol support.
Mouse handling. Ghostel encodes mouse events (press, release, drag) and passes them through to the terminal via the SGR mouse protocol. TUI apps like htop or lazygit receive full mouse input. vterm intercepts mouse clicks for Emacs point movement and does not forward them to the terminal.
Input modes. Ghostel offers five eat.el-style input modes (semi-char, char,
Emacs, copy, line) selected from a single base keymap; see Input modes. vterm's
default mode is roughly equivalent to ghostel's semi-char, and vterm-copy-mode
lines up with ghostel's copy mode - both freeze incoming output. Three of
ghostel's modes have no vterm equivalent: line mode buffers input locally so
full Emacs editing works on the in-progress line and RET sends it atomically;
Emacs mode keeps the terminal streaming live but locks the buffer read-only;
char mode is a runtime toggle that forwards every key (including C-c, C-x,
M-x) to the terminal - vterm requires editing vterm-keymap-exceptions and
reloading the buffer to get the same effect.
Rendering. Both use text properties (not overlays) and batch consecutive cells with identical styles. Ghostel's engine provides three-level dirty tracking (none / partial / full) with per-row granularity. vterm uses damage-rectangle callbacks and redraws entire invalidated rows. Ghostel defaults to ~30 fps redraw; vterm defaults to ~10 fps.
Shell integration. Ghostel auto-injects shell integration scripts for bash, zsh, and fish - no shell RC changes needed. vterm requires manually sourcing scripts in your shell configuration. Both support Elisp eval from the shell and TRAMP-aware remote directory tracking.
Password prompts. Ghostel detects when the foreground program is reading a
password (sudo, ssh, gpg, …) and prompts via read-passwd, sending the answer
down the PTY without routing keystrokes through Emacs's normal key pipeline.
vterm has no such interception: each character of your password is a regular
keypress, so it ends up in view-lossage, the recent-keys ring, and anything else
that observes the key pipeline. Ghostel's hook also lets you plug in
auth-source to satisfy known prompts without typing - see Password prompt
detection.
Performance. In PTY throughput benchmarks (1 MB streamed, both backends with ~1,000 lines of scrollback), ghostel is roughly 2.4x faster than vterm on plain ASCII data (81 vs 34 MB/s). On URL-heavy output ghostel pulls further ahead (77 vs 28 MB/s). See Performance for full numbers.
Installation. Ghostel can automatically download a pre-built native module or compile from source with Zig. vterm uses CMake with a single C dependency (libvterm) and can auto-compile on first load from Elisp.
For a detailed architectural comparison, see ARCHITECTURE.md.
15. Architecture
Ghostel has a two-layer design: a Zig native module (the terminal engine) and Elisp (Emacs integration).
ghostel.el Elisp: keymap, process management, mode, commands src/module.zig Entry point: emacs_module_init, function registration src/terminal.zig Terminal struct wrapping ghostty handles src/Renderer.zig RenderState -> Emacs buffer with styled text src/input.zig Key and mouse encoding via ghostty encoders src/emacs.zig Zig wrapper for the Emacs module C API
Key data flow:
- Keystroke →
ghostel--encode-key→ libghostty encoder →ghostel--flush-output→ PTY. - PTY output →
ghostel--filter→ghostel--write-input→ libghostty VT parser. - Timer fires →
ghostel--redraw→Renderer.zigreads dirty rows → updates the buffer with propertized text.
The Elisp sources live in lisp/, the Zig sources in src/, vendored upstream
headers in vendor/, the bundled compiled terminfo in etc/terminfo/, and shell
integration assets in etc/shell/. Optional MELPA extensions that depend on
ghostel live under extensions/<package>/ (currently evil-ghostel).
16. Contributing
Bug reports, feature requests, and pull requests are welcome on the GitHub
repository. When reporting a rendering or interactive-behavior issue, a
reproducer (the exact command or escape sequence, plus your Emacs version and
platform) helps a lot. M-x ghostel-debug-info captures diagnostic events that
are useful to attach.
17. Changelog
Notable changes for each release are recorded in the changelog, which follows Keep a Changelog and semantic versioning.
18. License
Ghostel is free software, released under the GNU General Public License version
3 or later (GPL-3.0-or-later). See the LICENSE file for the full text.