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
bg_cycle = #000000,#001a33,#002200,#200033,#330011,#332200,#003333
opacity_cycle = 100,75,50,25,0
font_weight = bold
palette = tokyonight
color1 = #e06c75
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.
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). Values
below 100 prefer a 32-bit ARGB visual when a compositor (picom,
compton, KWin, Mutter, etc.) is detected via _NET_WM_CM_S0 — that
gives true per-pixel see-through. 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 - 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
- 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
- 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)
- 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)
- Double-click to select word (alnum +
_-./~+@:%=) - Triple-click to select 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 - 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 → _NET_WM_WINDOW_OPACITY; otherwise wallpaper-blend pseudo-transparency) |
| Alt+v | Paste from PRIMARY (same as Shift+Insert) |
The six 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
Modifiers: alt, ctrl, shift. Keys: any single ASCII character or
the named keys plus, minus, underscore, equal, space. An
empty value disables the binding.
- 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
- Image display via kitty graphics protocol (APC
ESC _G ... ESC \) — rasters now silently consumed - 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)
