Emacs terminal emulator powered by libghostty-vt — the same VT engine that drives the Ghostty terminal.
Ghostel is inspired by emacs-libvterm: a native dynamic module handles terminal state and rendering, while Elisp manages the shell process, keymap, and buffer.
- Requirements
- Installation
- Building from source
- Shell Integration
- Key Bindings
- Features
- Configuration
- Commands
- Running Tests
- Performance
- Ghostel vs vterm
- Architecture
- License
- Emacs 27.1+ with dynamic module support
- macOS or Linux
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-linux
If you prefer to build from source or need a different platform, you'll also need Zig 0.15.2+ (see Building from source).
(use-package ghostel
:ensure t)(use-package ghostel
:vc (:url "https://github.com/dakra/ghostel"
:lisp-dir "lisp"
:rev :newest))NOTE: :lisp-dir "lisp" is only required on Emacs <31.1
(use-package ghostel
:load-path "/path/to/ghostel")(add-to-list 'load-path "/path/to/ghostel")
(require 'ghostel)Then M-x ghostel to open a terminal.
When the native module is missing, Ghostel will offer to download a
pre-built binary or compile from source (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 binaryC-u M-x ghostel-download-module— choose a specific release tag (leave blank for latest)M-x ghostel-module-compile— build from source viazig build
Building is only needed if you don't want to use the pre-built binaries.
Ghostel vendors a generated vendor/emacs-module.h, so normal builds do not
require local Emacs headers. If you want to override the vendored header, set
EMACS_INCLUDE_DIR to a directory containing emacs-module.h, or set
EMACS_BIN_DIR to an Emacs bin/ directory and Ghostel will look for
../include and ../share/emacs/include.
git clone https://github.com/dakra/ghostel.git
cd ghostel
# Build everything (fetches ghostty automatically via Zig package manager)
zig build -Doptimize=ReleaseFastTo 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.
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=ReleaseFastWhen installed from MELPA, M-x ghostel-module-compile builds the native
module from source using zig build. Zig's package manager fetches the
ghostty dependency automatically.
Alternatively, download a pre-built binary via M-x ghostel-download-module
(or C-u M-x ghostel-download-module to pick a specific release).
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.
Shell integration (directory tracking via OSC 7, prompt navigation via OSC 133, etc.) 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:
Manual shell integration
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:
test "$INSIDE_EMACS" = 'ghostel'; and source "$EMACS_GHOSTEL_PATH/etc/shell/ghostel.fish"| 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 C-t |
Enter copy mode |
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 C-l |
Clear scrollback |
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-c C-q |
Send next key literally (escape hatch) |
| Mouse wheel | Scroll through scrollback |
Keys listed in ghostel-keymap-exceptions (default: C-c, C-x, C-u,
C-h, C-g, M-x, M-o, M-:, C-\) pass through to Emacs.
Enter with C-c C-t. Standard Emacs navigation works.
Normal letter keys exit copy mode and send the key to the terminal.
| Key | Action |
|---|---|
C-SPC |
Set mark |
M-w / C-w |
Copy selection and exit |
C-n / C-p |
Move line (scrolls at edges) |
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 |
C-c C-t |
Exit without copying |
a–z |
Exit and send key to terminal |
Soft-wrapped newlines are automatically stripped from copied text.
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 — even outside copy mode.
- 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), 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 below. 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 (percentage shown in the mode line; 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)
- 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)
- 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). Note: the bundledxterm-ghosttyterminfo intentionally does not advertise theMscapability, so apps don't 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, install upstream Ghostty's terminfo on the same path or overrideTERMINFO. - Bracketed paste — yank from kill ring sends text as a bracketed paste so shells handle it correctly
- Full keyboard input with Ghostty key encoder (respects terminal modes, Kitty keyboard protocol)
- Mouse tracking (press, release, drag) via SGR mouse protocol — TUI apps receive full mouse input
- Focus events gated by DEC mode 1004
- Drag-and-drop (file paths and text)
- 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 (buffer is renamed from the terminal title)
- OSC 51 — 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
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 containersEach 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).
By default, shell integration scripts are not injected for remote sessions. There are two ways to enable it:
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))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:
bash — add to ~/.bashrc on the remote host:
[[ "$INSIDE_EMACS" = 'ghostel' ]] && source ~/.local/share/ghostel/ghostel.bashzsh — add to ~/.zshrc on the remote host:
[[ "$INSIDE_EMACS" = 'ghostel' ]] && source ~/.local/share/ghostel/ghostel.zshfish — add to ~/.config/fish/config.fish on the remote host:
test "$INSIDE_EMACS" = 'ghostel'; and source ~/.local/share/ghostel/ghostel.fishThe integration scripts provide directory tracking (OSC 7), prompt
navigation (OSC 133), and ghostel_cmd for calling Elisp from the shell.
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.
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.
The bundled bash/zsh/fish integration shadows ssh with a function
that:
- Resolves the canonical target via
ssh -G(normalises ssh_config 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'll see two auth prompts the first time. Strongly recommended:
# ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 60sWith 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
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): wrapper skips install for that call to avoid clashing with the user's command. Connects with 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.
If you'd rather not have ghostel touch remote hosts (and don't 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.
Set (setq ghostel-term "xterm-256color") to drop TERM=xterm-ghostty
locally. No advertisement, no terminfo gymnastics, no synchronized
output fast-path either.
- Incremental redraw — only dirty rows are re-rendered
- Timer-based batched updates with adaptive frame rate
- Immediate redraw for interactive typing echo — small PTY output arriving shortly after a keystroke bypasses the timer, eliminating 16–33ms of latency per keypress
- Input coalescing — rapid keystrokes are batched into a single PTY write to reduce syscall overhead
- Cursor position updates even without cell changes
- Theme-aware color palette (syncs with Emacs theme via
ghostel-sync-theme)
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 OSC 51 escape sequences (the same protocol as vterm). 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)"; }
fiGhostel 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 isn't 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.
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. The default handler, ghostel-default-progress,
updates mode-line-process in the terminal buffer:
[42%]— running at 42% done[...]— indeterminate progress[err 73%]— error (shown in theerrorface)[paused 25%]— paused- (cleared) — removed
For a fancier visual indicator during indeterminate progress, swap
ghostel-progress-function for a handler backed by
spinner.el:
(require 'spinner)
(defvar-local my/ghostel-spinner nil)
(defun my/ghostel-progress (state progress)
(pcase state
((or 'set 'indeterminate)
(unless my/ghostel-spinner
(setq my/ghostel-spinner (spinner-create 'progress-bar t))
(spinner-start my/ghostel-spinner))
(when (eq state 'set)
(setq mode-line-process (format " [%d%%]" (or progress 0)))))
((or 'remove 'error 'pause)
(when my/ghostel-spinner
(spinner-stop my/ghostel-spinner)
(setq my/ghostel-spinner nil)))))
(setq ghostel-progress-function #'my/ghostel-progress)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")| Variable | Default | Description |
|---|---|---|
ghostel-module-auto-install |
ask |
What to do when native module is missing (ask, download, compile, nil) |
ghostel-shell |
$SHELL |
Shell program to run |
ghostel-term |
"xterm-ghostty" |
Value of TERM for spawned processes. Default uses the bundled terminfo so apps can detect ghostel's full capability set. Set to "xterm-256color" to fall back (drops TERMINFO and TERM_PROGRAM=ghostty too) |
ghostel-environment |
nil |
Extra env vars for spawned processes (list of "KEY=VALUE" strings). |
ghostel-ssh-install-terminfo |
auto |
Install xterm-ghostty terminfo on remote hosts as needed. auto follows ghostel-tramp-shell-integration. Affects both TRAMP-launched ghostel (push terminfo over the existing TRAMP connection) and outbound ssh from a local buffer (install via tic on first connection, cache in ~/.cache/ghostel/ssh-terminfo-cache). Per-call ssh override: GHOSTEL_SSH_KEEP_TERM=1 |
ghostel-tramp-shells |
(see below) |
Shell to use per TRAMP method (with login-shell detection) |
ghostel-shell-integration |
t |
Auto-inject shell integration |
ghostel-tramp-default-method |
nil |
TRAMP method for new remote paths from OSC 7 (nil uses tramp-default-method) |
ghostel-tramp-shell-integration |
nil |
Auto-inject shell integration for remote TRAMP sessions |
ghostel-buffer-name |
"*ghostel*" |
Default buffer name |
ghostel-max-scrollback |
5MB |
Maximum scrollback size in bytes (materialized into the Emacs buffer; ~5,000 rows on 80-col terminals) |
ghostel-timer-delay |
0.033 |
Base redraw delay in seconds (~30fps) |
ghostel-adaptive-fps |
t |
Adaptive frame rate (shorter delay after idle, stop timer when idle) |
ghostel-immediate-redraw-threshold |
256 |
Max output bytes to trigger immediate redraw (0 to disable) |
ghostel-immediate-redraw-interval |
0.05 |
Max seconds since last keystroke for immediate redraw |
ghostel-input-coalesce-delay |
0.003 |
Seconds to buffer rapid keystrokes before sending (0 to disable) |
ghostel-full-redraw |
nil |
Always do full redraws instead of incremental updates |
ghostel-kill-buffer-on-exit |
t |
Kill buffer when shell exits |
ghostel-eval-cmds |
(see above) |
Whitelisted functions for OSC 51 eval |
ghostel-enable-osc52 |
nil |
Allow apps to set clipboard via OSC 52 |
ghostel-notification-function |
ghostel-default-notify |
Handler for OSC 9 / OSC 777 desktop notifications (nil disables) |
ghostel-progress-function |
ghostel-default-progress |
Handler for OSC 9;4 ConEmu progress reports (nil disables) |
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-ignore-cursor-change |
nil |
Ignore terminal-driven cursor shape/visibility changes |
ghostel-keymap-exceptions |
("C-c" "C-x" ...) |
Keys passed through to Emacs |
ghostel-exit-functions |
nil |
Hook run when the shell process exits |
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
| Command | Description |
|---|---|
M-x ghostel |
Open a new terminal (create new buffer with prefix arg) |
M-x ghostel-project |
Open a terminal in the current project root (create new buffer with prefix arg) |
M-x ghostel-other |
Switch to next terminal or create one |
M-x ghostel-clear |
Clear screen and scrollback |
M-x ghostel-clear-scrollback |
Clear scrollback only |
M-x ghostel-copy-mode |
Enter copy mode |
M-x ghostel-copy-all |
Copy entire scrollback to kill ring |
M-x ghostel-paste |
Paste from kill ring |
M-x ghostel-send-next-key |
Send next key literally |
M-x ghostel-next-prompt |
Jump to next shell prompt |
M-x ghostel-previous-prompt |
Jump to previous shell prompt |
M-x ghostel-next-hyperlink |
Jump to next hyperlink (OSC 8, URL, file ref) |
M-x ghostel-previous-hyperlink |
Jump to 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 color palette after theme change |
M-x ghostel-ssh-clear-terminfo-cache |
Clear outbound-ssh terminfo install cache (force re-probe) |
M-x ghostel-download-module |
Download pre-built native module |
M-x ghostel-module-compile |
Compile native module from source |
For packages that need to inject input into a running ghostel buffer (agent integrations, custom keymaps, Swerty-style bindings, …) 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-separatedBoth 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.
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)ghostel-compile runs a shell command in a ghostel buffer and presents
the result like M-x compile — 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 owned by the ghostel
renderer — 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 |
Prompt for a command and run it (uses compile-command) |
M-x ghostel-recompile |
Re-run the last command in its original directory |
M-x ghostel-compile-global-mode |
Route all compile-style calls through ghostel (opt-in) |
What a run looks like — the buffer text matches M-x compile:
-*- 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
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). The
buffer becomes a regular, read-only Emacs buffer with compile-mode's
coloured error / line-number faces; the buffer never returns to an
interactive ghostel terminal — a recompile discards it and starts
fresh in the original directory. mode-line-process shows
:run while the command is running and :exit [N] afterwards, using
the same faces M-x compile uses.
Keybindings (in ghostel-compile-view-mode):
| 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) |
These standard compile options are honoured:
compile-command/compile-history— shared withM-x compile. The prompt defaults tocompile-command, the chosen command is written back, and the history list iscompile-history, so recent commands round-trip between the two commands.compilation-read-command— when nil,ghostel-compilerunscompile-commandsilently; pass a prefix arg to force the prompt.compilation-ask-about-save— modified buffers are offered for saving before launching.compilation-auto-jump-to-first-error— jumps to the first error after parsing.compilation-finish-functions— runs with(buffer msg)just like withM-x compile.- Output scrolling is always on (terminal behaviour — equivalent to
compilation-scroll-outputnon-nil).
ghostel-recompile runs in the directory the original ghostel-compile
was invoked from, regardless of which buffer you're in when you press g.
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 under the hood — runs in a ghostel
buffer automatically.
(require 'ghostel-compile)
(ghostel-compile-global-mode 1)grep-mode falls through to the stock compilation-start
implementation by default, because its output parsing and
window-management conventions don't fit a live TTY. Extend
ghostel-compile-global-mode-excluded-modes to 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 to switch to after each run (default ghostel-compile-view-mode; set to nil to 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) |
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 can't break the rest.
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's 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 nethackCustomisation:
| 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.
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 checkYou 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 built native module)
emacs --batch -Q -L . -l ert -l test/ghostel-test.el -f ghostel-test-runGhostel includes a benchmark suite comparing throughput against other Emacs
terminal emulators: vterm (native
module), eat (pure Elisp), and Emacs
built-in term.
The primary benchmark streams 5 MB of data through a real process pipe, matching actual terminal usage. 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 | 87 MB/s | 64 MB/s |
| ghostel (no detect) | 86 MB/s | 86 MB/s |
| vterm | 35 MB/s | 27 MB/s |
| eat | 4.7 MB/s | 3.5 MB/s |
| term | 5.7 MB/s | 4.7 MB/s |
Ghostel scans terminal output for URLs and file paths, making them clickable.
The "no detect" row shows throughput with this detection disabled
(ghostel-enable-url-detection / ghostel-enable-file-detection). The other
emulators do not have this feature, so their numbers are comparable to the "no
detect" row.
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 checkThe typing latency benchmark can be run from Elisp:
(require 'ghostel-debug)
M-x ghostel-debug-typing-latency ; interactive measurementBoth 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.
| 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 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 |
| Drag-and-drop | Yes | No |
| Auto module download | Yes | No |
| Scrollback default | ~5,000 | 1,000 |
| PTY throughput (plain ASCII) | 70 MB/s | 34 MB/s |
| Default redraw rate | ~30 fps | ~10 fps |
Terminal engine. libghostty-vt comes from Ghostty, a modern GPU-accelerated terminal, and supports 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 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.
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.
Performance. In PTY throughput benchmarks (5 MB streamed through cat,
both backends configured with ~1,000 lines of scrollback), ghostel is
roughly 2x faster than vterm on plain ASCII data (70 vs 34 MB/s). On
URL-heavy output ghostel pulls further ahead of vterm (56 vs 27 MB/s);
with link detection disabled ghostel reaches 70 MB/s regardless of input.
See the Performance section above for full numbers and how
to run the benchmark suite yourself.
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 design.org.
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/render.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
src/ghostty.zig Re-exports and constants for the ghostty C API
GPL-3.0-or-later