Terminal emulator written in x86_64 Linux assembly. No libc, no runtime, pure syscalls. Speaks X11 wire protocol directly via Unix socket. Single static binary, ~155KB.
No toolkit, no rendering library, no external font engine. The TTF
rasterizer (glyph) is embedded
in-binary via %include. Just your keystrokes, the X11 server, and
the kernel.
Part of the CHasm (CHange to ASM) suite: bare (shell), show (file viewer), glass (terminal emulator).
glass driving every pane in the CHasm desktop, with pseudo-transparency picking up the wallpaper. Every binary on screen is x86_64 assembly — tile holds the layout, strip + asmites (the per-segment programs in chasm-bits) drive the status row, show is rendering syntax-highlighted source in the left and bottom-right panes, bare is the shell.
The build expects glyph checked
out as a sibling directory (../glyph relative to this repo) — it
provides the TTF rasterizer that glass includes at assembly time.
mkdir glass-build && cd glass-build
git clone https://github.com/isene/glyph.git
git clone https://github.com/isene/glass.git
cd glass
make
sudo make installmake install also installs the bundled emoji cache (1170
single-codepoint emoji pre-rendered at 12x13, the default font cell
size) to /usr/local/share/glass/emoji/. First-time emoji rendering
is instant; any not in the bundle are rasterized once via ImageMagick
convert and written to ~/.cache/glass/emoji/.
To rebuild the bundled cache for a different cell size (e.g. if you use a larger font):
make emoji-cache W=20 H=20 # rebuild for 20x20 cells
sudo make install-emojiThe convert command (ImageMagick) and the Noto Color Emoji font are
only needed for rendering emoji NOT in the cache; glass itself has no
runtime dependencies beyond an X11 server.
Create ~/.glassrc for custom colors:
bg = #1a1b26
fg = #c0caf5
cursor = #f7768e
font_size = 13
opacity = 80
cursor_blink = 500
dim_brightness = 30
bg_cycle = #000000,#001a33,#002200,#200033,#330011,#332200,#003333
opacity_cycle = 100,75,50,25,0
font_weight = bold
palette = tokyonight
color1 = #e06c75
font_path = /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf
font_path_fallback = /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf
font_path_fallback2 = /usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf
font_path_fallback names a second face to try for any glyph the main
font has no outline for, which is how CJK, Cyrillic or box-drawing text
renders when the main font stops at Latin. font_path_fallback2 is
tried after it, for symbols the CJK face lacks, like ⋮. A fallback
glyph is centred in one cell. Leave both out and nothing changes.
The default 16-color palette matches kitty's defaults so most modern
TUIs (claude-code, lazygit, lazydocker, helix, etc.) render the way
their authors intended. Use palette = NAME to swap the whole 16-slot
block; bundled names: kitty (default), vga (the historical
glass/console palette), solarized-dark, dracula, gruvbox-dark,
nord, tokyonight, monokai. Layer on top with colorN = #RRGGBB
where N is 0..15 — handy for tweaking a single slot without
redefining the rest. Order in the file matters: a palette = line
overwrites earlier colorN lines, but explicit bg/fg keys are
re-applied after the theme so they're never clobbered.
tint = #RRGGBB paints the whole terminal in one colour: each pixel
keeps its brightness and takes the tint's colour. It reaches the 256
colours, truecolor text and pictures, so an app with its own colours
turns the tint's colour too. Only the background stays as set, and
strong reds stay red, so an error still reads as one. An app can switch
it while it runs with OSC 7701 (crust::style::tint_seq).
OSC 7702;1 dims the window to 30% brightness until a key is pressed
in it (dim_brightness = N in ~/.glassrc sets how much shows, 0 to
100), and OSC 7702;0 lifts the dim (crust::style::dim_seq). deck
sends them when its Claude session goes idle and when it works again,
so the sessions that wait for you stand out.
Available font sizes: 10, 13, 15, 18, 20 (misc-fixed, always present),
22, 24, 28, 32 (xos4-terminus, requires the xfonts-terminus package
on Debian/Ubuntu/Arch/Fedora — much more legible at large sizes).
Colors are hex RGB.
font_weight = bold aliases the regular font to the bold XLFD, so
all text gets the heavier strokes that TTF terminals use by default.
SGR 1 (bold) then has no further effect since terminus has no
extra-bold beyond bold.
cursor_blink is the toggle interval in milliseconds (omit or set
to 0 for a steady cursor).
opacity is a 0..100 percentage (100 = opaque, default). With an
opacity line, glass takes a 32-bit ARGB window when a compositor
(frame, picom, KWin, Mutter, etc.) is detected via _NET_WM_CM_S0.
The background is see-through; text and coloured cells stay solid.
At 100 glass tells the compositor the window is solid all over
(_NET_WM_OPAQUE_REGION), so it costs no blending. Keep the
opacity line if you use Alt+t: a window cannot turn into an ARGB
window after it is made. With no compositor, glass reads
the desktop wallpaper from _XROOTPMAP_ID, samples it strip by
strip behind the window, blends each pixel with the configured
bg using opacity, uploads the blended bitmap as the window's
background pixmap, and switches the renderer to PolyText16 for
default-bg cells so the wallpaper texture shows through. Without
a configured opacity, none of this code runs.
To launch glass and run a specific program inside it instead of the
default shell, use -- (or -e):
glass -- /home/you/bin/kastrup
glass -- /usr/bin/ssh user@host
glass --class Weechat -- /usr/bin/ssh -t user@host tmux a -t wee--class NAME sets WM_CLASS so window managers (like
tile) can route the window via
assign NAME <ws> rules.
Use a full path. glass execve(2)s the command directly — it does
not search $PATH. glass -- ssh foo will silently fall back to the
default shell because the kernel can't find ssh without a path.
which ssh and pass the full result. (PATH-search support is a TODO
for v0.4.)
- X11 wire protocol over Unix domain socket (no Xlib/XCB)
- Xauthority cookie authentication (MIT-MAGIC-COOKIE-1)
- Embedded pure-asm TTF rasterizer (the
glyphengine, included via%include): 4×4 supersampling, gamma LUT for stem darkening, XRender CompositeGlyphs32 with batched per-run uploads. Falls back to bitmap iso10646-1 X core font when nofont_pathis set. - Codepoint substitution table for codepoints DejaVu Sans Mono lacks
(e.g. CC's tree-prefix
⎿→└, tool-call⏵→▶); blank-class codepoints (SPACE, NBSP, U+2007..U+200B etc.) upload as zero-area glyphs to avoid the X server's default-glyph placeholder - Fallback font via
font_path_fallback: a second face for the glyphs the main one has no outline for. DejaVu Sans Mono carries no Japanese at all, so a CJK mail subject drew as nothing; point the key at a wide-coverage face and it draws. Coverage decides, not a range table, so Korean, Cyrillic, Greek and box drawing come along for free. Opened on the first glyph that needs it and not before, so a session that never meets one never touches the file - UTF-8 decoding state machine (2/3/4-byte sequences)
- Per-color-run rendering (each color segment drawn separately)
- 256-color palette with truecolor (24-bit) SGR mapping
- Inverse video (SGR 7) for status bars
- Configurable background, foreground, and cursor colors
- Visual selection rendering (inverted cells during drag)
- Visual bell on BEL (0x07)
- No-flicker rendering (omits ClearArea since cells fully repaint)
- Back-buffer pixmap cleared on resize/init so the bottom strip (window_height % char_height) doesn't show uninitialised pixels
- VT100/xterm escape sequence parser
- Alternate screen buffer (CSI ?1049h/l) for vim, less, man, htop
- Scroll regions (DECSTBM) for vim splits
- DECSET/DECRST modes: cursor visibility, autowrap, mouse tracking, bracketed paste, focus reports
- Cursor shapes: block, underline, bar (CSI q)
- Insert/delete lines and characters (CSI L/M/@/P/X)
- SGR: bold, underline, inverse, 8/16/256, full 24-bit truecolor (no quantization)
- Charset designators ESC ( / ) / * / + consumed (no stray 'B' from ncurses)
- OSC 0/2: dynamic window title
- OSC 11/111: set the background (
#rrggbb) and put it back - CSI private prefixes: ?, >, =
- Reply-aware event parsing (variable-size replies don't misalign event stream)
- Proper X11 keyboard mapping (GetKeyboardMapping from server)
- AltGr (Mod5) support for international keyboard layouts (e.g., Norwegian AltGr+4 → $)
- Latin-1 keysyms (0x00A0-0x00FF) sent as UTF-8 (e.g., AltGr+3 → £)
- Arrow keys, Home/End, Page Up/Down, Delete, function keys
- Ctrl and Shift modifiers
- Mouse reporting (SGR mode 1006) for vim, tmux
- Bracketed paste mode (CSI ?2004h/l)
- Focus reports (CSI ?1004h/l): ESC[I and ESC[O when the window gains or loses focus
- Ctrl+Shift+V pastes CLIPBOARD selection
- Shift+Insert pastes PRIMARY selection (X11 tradition)
- Ctrl+D properly exits glass when bare exits (POLLHUP detection)
- Scrollback buffer (1000 lines, Shift+PageUp/Down) — captures rows
that scroll off the top of a scroll region too, so session history
printed by tools like
claude --resumeis scrollable from inside the alt-screen TUI prompt - Cursor is hidden while viewing scrollback (prevents stray cursor block at the bottom of the historical view)
- Text selection (click and drag, visible inverted highlight); the highlight goes when a program writes new text into the selected cells
- Double-click to select word (alnum +
_-./~+@:%=); a full stop at its end stays out - Triple-click to select the sentence: it ends at
.,!or?before a space, runs on over wrapped rows up to an empty row or a list mark, and is copied as one line (it stays inside its pane: a box line or three blank cells end a row's text) - Quadruple-click to select the whole line
- Selection populates X11 PRIMARY for paste in other apps
- Wrap-aware copy: visually-wrapped lines join into one logical line when selected
- Glass responds to SelectionRequest (TARGETS, UTF8_STRING, STRING)
- URL detection (http/https): hover underlines the URL in a soft
blue (configurable via
url_underline = #RRGGBB), plain click opens with xdg-open (no Ctrl required); Ctrl+click still works; a comma, full stop or other mark right after the URL is left out; a URL glass wraps onto the next rows is one link (up to four rows), and so is an OSC 8 link a program wrapped: hover underlines every row - OSC 8 hyperlinks (
ESC ] 8 ; params ; URI ESC \\); Ctrl+click opens URI - PTY resize on window resize with SIGWINCH
- Initial PTY size from screen dimensions (no 80×24 default)
- TERM=xterm-256color set in child environment
- Pure x86_64 Linux syscalls (no libc)
- Single
.asmsource file (~8000 lines) - Static binary (~58KB, zero dependencies)
- PTY management (posix_openpt, setsid, TIOCSCTTY)
- X11 request batching (single write per frame)
- 8-byte grid cells (UCS-2 char + fg + bg + attrs)
- Circular scrollback buffer
- Drains large QueryFont replies (~786KB for Unicode font) to keep socket aligned
glass connects to the X11 server via a Unix domain socket (/tmp/.X11-unix/X0), authenticates with the Xauthority cookie, and speaks raw X11 wire protocol. No Xlib, no XCB, no toolkit.
It opens a PTY, forks a child shell (bare by default), and enters an event loop polling both the X11 socket and the PTY master. Keyboard events are translated to terminal input via the server's keymap. PTY output is parsed through a VT100 state machine and rendered to an internal grid. The grid is drawn to the X11 window using batched ImageText16 requests with per-color-run optimization.
Selection works via the X11 PRIMARY mechanism: drag-select to claim ownership, other apps' middle-click or Shift+Insert sends a ConvertSelection request to glass which responds with the selection text via ChangeProperty + SelectionNotify.
glass polls the X11 socket and the PTY master with poll(2). The
event loop tests revents for POLLERR | POLLHUP (mask 0x18) on
the X11 fd and exits cleanly when the X server goes away — see
.ev_check_x11 in glass.asm. The PTY side is handled the same way
so that closing bare via Ctrl+D shuts glass down. There is no
spin-loop on a dead socket; this was caught and fixed during early
development against Xephyr (see the inline comment in the same code).
CHasm's premise is "what's the smallest set of dependencies a real desktop tool actually needs?" Linking XCB would mean dynamic linking, libxcb's ~140KB of code, libxau, and a transitive dependency on libc — all to wrap a wire protocol that's a few KB to write yourself. The benefit XCB does give you (XML-generated request encoders) is real when you're consuming the whole protocol; glass only uses ~25 opcodes and the wire format is stable and small enough to write by hand once. So we did. The same logic applies to libfontconfig (we embed glyph for TTF instead) and to the X core text path (PolyText16 + ImageText16 are 4 lines of serialisation each). This is a stylistic choice, not a defect.
| Key | Action |
|---|---|
| Shift+PageUp | Scroll back |
| Shift+PageDown | Scroll forward |
| Ctrl+Shift+V | Paste from CLIPBOARD |
| Shift+Insert | Paste from PRIMARY (selected text) |
| Click on hovered URL | Open URL (hover underlines first) |
| Ctrl+Click | Open URL under cursor (legacy / works anywhere) |
| Ctrl+D | Exit (when shell line is empty) |
| Alt+plus / Alt+= | Step font size up to next preset |
| Alt+minus | Step font size down to previous preset |
| Alt+_ / Alt+0 | Reset font size to the configured default |
| Alt+b | Cycle through bg_cycle colors from ~/.glassrc |
| Alt+t | Cycle through opacity_cycle percentages, or toggle 100↔50 if no cycle is set (compositor → see-through background in an ARGB window; otherwise wallpaper-blend pseudo-transparency). Under a compositor it needs an opacity line in ~/.glassrc |
| Alt+v | Paste from PRIMARY (same as Shift+Insert) |
| Alt+c | Copy all the text in view to CLIPBOARD and PRIMARY |
| Alt+d | Dim the window to 30% brightness (dim_brightness in ~/.glassrc), or lift the dim. Any other key lifts it too |
The eight Alt-key shortcuts are rebindable via ~/.glassrc:
key.font_inc = alt+plus
key.font_dec = alt+minus
key.font_reset = alt+underscore
key.bg_cycle = alt+b
key.opacity = alt+t
key.paste = alt+v
key.copy_screen = alt+c
key.dim = alt+d
Modifiers: alt, ctrl, shift. Keys: any single ASCII character or
the named keys plus, minus, underscore, equal, space. An
empty value disables the binding.
test/run.sh runs every case in test/cases/, each in its own glass on a
scratch frame that draws to plain memory.
It never touches your own display. Each case is a fault that was once
reported, and fails on the glass from before its fix.
It needs frame built beside this repo (or on your PATH), and gcc, libx11-dev, xdotool, xclip and ImageMagick.
- True emoji rendering (XRender extension + bundled cache)
- Opacity/transparency (compositor + wallpaper sampling fallback)
- Configurable key bindings (the five Alt-key shortcuts)
- Pure-asm TTF parser/rasterizer (the glyph engine, embedded via
%include— 4×4 SS + gamma LUT) - Variable-font TTFs (gvar code path SEGVs in library mode; static TTFs work — see
CONFIG-FUTURE.md) - Tab/split support (multiple PTYs)
- Font ligatures
- Kitty keyboard protocol, flags 1 to 16:
CSI > flags upush,CSI < n upop,CSI = flags ; mode uset,CSI ? uanswered, a stack per screen. Flag 2 reports every repeat and release asCSI code ; mods : type u, arrows and F-keys in their own forms. Flag 4 adds the shifted key and flag 16 the text. Flag 8 sends every key as an escape code, modifier keys too. X sends key releases only while flag 2 is on, so a shell sees nothing new. Flag 1 alone keeps the legacy bytes for Ctrl and Alt letters, which is what Claude Code expects. - Image display via kitty graphics protocol (APC
ESC _G ... ESC \): PNG, RGBA and RGB, inline base64 or a shm/file name (t=s,t=f), placements scaled into cells and scrolled with the text,OK/error replies unlessq. One placement per image id.z<0puts an image under the text. It is drawn before the rows, and the cells on it skip their background fill, so glyphs sit on the picture. A game can stream 640x400 frames through/dev/shmat 60 fps. - WM_CLASS for window manager integration
| Tool | Purpose | Binary | Lines |
|---|---|---|---|
| bare | Interactive shell | ~150KB | ~16K |
| show | File viewer | ~40KB | ~3.4K |
| glass | Terminal emulator | ~75KB | ~8K |
All three: pure x86_64 assembly, no libc, no dependencies, direct syscalls.
Debian/Ubuntu: every release ships a dependency-free static amd64
.deb — grab it from the
latest release and:
sudo apt install ./glass_*_amd64.debArch: a PKGBUILD is included in the repo root:
git clone https://github.com/isene/glass.git && cd glass && makepkg -siUnlicense (public domain)
