# zurm > A GPU-rendered terminal emulator for macOS, written in Go. > > This file contains the complete documentation. ## Installation *How to install zurm on macOS.* ### Installation #### Binary Release Download the latest release from [GitHub Releases](https://github.com/studiowebux/zurm/releases). **`zurm-macos-arm64.dmg`** — .app bundle, drag to `/Applications` and launch normally. **`zurm`** — raw binary, run directly from the terminal. The app is not notarized yet. On first launch macOS Gatekeeper will block it. To allow it: ```bash # For the .app bundle xattr -d com.apple.quarantine zurm.app # For the raw binary xattr -d com.apple.quarantine zurm chmod +x zurm ``` Or: right-click the app, select Open, then Open anyway. #### From Source Requires Go >= 1.26.1. ```bash git clone https://github.com/studiowebux/zurm cd zurm go build -o zurm . ``` #### Usage ```bash ./zurm ./zurm --no-restore # skip session restore, open a single fresh tab ``` ## Configuration *Full reference for zurm's TOML configuration file.* ### Configuration Config file: `~/.config/zurm/config.toml` — created automatically on first launch with all defaults. Hot-reload with `Cmd+,` — no restart needed. #### Full Reference ```toml [font] family = "JetBrains Mono" # font family name (informational) size = 15 # font size in points (clamped to 6–72) # file = "/path/to/Font.ttf" # custom TTF/OTF; overrides embedded JetBrains Mono # fallbacks = [...] # see Optional Fonts page [window] columns = 120 # initial terminal width in character columns (min 1) rows = 35 # initial terminal height in character rows (min 1) padding = 4 # pixels inside each pane edge (min 0) [shell] program = "" # shell binary; empty = read from $SHELL, fallback /bin/zsh args = ["-l"] # arguments passed to the shell [scrollback] lines = 10000 # scrollback buffer size per pane [scroll] wheel_lines_per_tick = 3 # lines scrolled per mouse wheel tick smooth = false # ease-out animation instead of instant jump (wheel only) smooth_factor = 0.25 # animation speed: fraction of distance closed per frame (0.1 slow … 0.5 fast) [performance] tps = 30 # Ebitengine tick rate (Update calls/sec); lower = less idle CPU auto_idle = true # reduce TPS when unfocused to save CPU; false = keep rendering memory_limit_mb = 1536 # soft memory ceiling (MB); GC runs harder near this limit; 0 = unlimited pprof = false # enable net/http/pprof endpoint on localhost for profiling pprof_port = 6060 # port for pprof HTTP server (localhost only) [input] double_click_ms = 300 # max ms between clicks to register as double-click cursor_blink = false # true = blinking cursor at 530 ms; false = steady [tabs] max_width_chars = 24 # maximum tab label width in character cells new_tab_dir = "cwd" # "cwd" = inherit active tab's directory; "home" = $HOME max_open = 10 # max visible (non-parked) tabs; 0 = unlimited [tabs.hover] enabled = true # show minimap popover when hovering a background tab delay_ms = 300 # ms before popover appears width = 320 # popover width in logical pixels height = 200 # popover height in logical pixels [panes] divider_width_pixels = 1 # pixel width of the border between panes [keyboard] left_option_as_meta = true # left Option sends ESC-prefix (word delete, etc.) # right Option still composes macOS characters repeat_delay_ms = 500 # ms before key repeat starts repeat_interval_ms = 50 # ms between repeated key events (~20/sec) [status_bar] enabled = true show_cwd = true # current working directory show_git = true # git branch name show_clock = false # forces a redraw every second show_process = true # foreground process name (polls every 1s); when ssh, shows [SSH] user@host show_commit = true # short commit hash next to branch name show_dirty = true # modified (~N) and staged (+N) file counts show_ahead_behind = true # commits ahead/behind upstream (N^ Nv) branch_prefix = "" # set to " " with a Nerd Font segment_separator = " · " # separator between status bar segments separator_height_px = 1 # height of top border line (0 = hidden) padding_px = 4 # top padding above text [help] enabled = true # all in-app help surfaces context_menu = true # right-click opens context menu (set false for PTY apps that use right-click) close_confirm = true # ask before closing a tab or pane [session] enabled = true # save/restore session on quit/launch restore_on_launch = true # reopen tabs with last CWDs and titles auto_save = false # automatically save session on quit [file_explorer] enabled = true # file explorer sidebar side = "left" # "left" or "right" width_pct = 35 # panel width as percent of screen width [blocks] enabled = false # OSC 133 command blocks; toggle at runtime with Cmd+B (requires shell hooks) show_duration = true # show elapsed execution time show_border = true # draw 4-sided border + bg tint (false = badges + hover buttons only) border_width = 3 # width in pixels of the left accent stripe border_color = "#1C1C2E" # border color when exit status is unknown success_color = "#34D399" # border color for exit code 0 fail_color = "#F87171" # border color for non-zero exit codes bg_color = "" # optional hex background tint (empty = none) bg_alpha = 0.0 # opacity of background tint (0.0-1.0) max_history = 1000 # max completed blocks retained per pane (0 = unlimited) [bell] style = "visual" # "visual" = screen flash; "none" = disabled sound = true # play system beep sound (NSBeep) duration_ms = 150 # flash duration in ms color = "#F59E0B" # hex color for the visual flash [vault] enabled = false # encrypted local command history + ghost suggestions ghost_text = true # show inline ghost suggestions while typing; false = keep history without suggestions history_path = "" # path to zsh history file; empty = ~/.zsh_history vault_path = "" # encrypted vault file path; empty = ~/.config/zurm/vault.enc ignore_prefix = " " # commands starting with a space are never stored (type " ssh ..." to hide it) suggestion_color = "#555570" # ghost text color for inline suggestions max_entries = 0 # cap on stored commands; 0 = unlimited sync_interval = 0 # seconds between zsh history re-imports; 0 = import once at startup # Encryption key: ~/.config/zurm/vault.key (auto-generated, 32-byte AES-256 key, mode 0600) # Right arrow accepts the ghost suggestion; space-prefix keeps commands private. [server] address = "" # Unix socket path of zurm-server; empty = ~/.config/zurm/server.sock binary = "" # path to zurm-server binary; empty = look next to zurm binary, then PATH # Use Cmd+Shift+B to open a server-backed tab on demand. zurm-server is # auto-started when needed — no manual launch or enabled flag required. [theme] name = "" # theme filename without .toml (e.g. "dark"); empty = no theme # themes live in ~/.config/zurm/themes/ [colors] background = "#0F0F18" foreground = "#E8E8F0" cursor = "#A855F7" border = "#1C1C2E" separator = "#555570" # shared separator line color for tab bar and status bar # 16-color ANSI palette black = "#555570" red = "#F87171" green = "#34D399" yellow = "#F59E0B" blue = "#7C3AED" magenta = "#C084FC" cyan = "#67E8F9" white = "#8888A8" bright_black = "#555570" bright_red = "#F87171" bright_green = "#34D399" bright_yellow = "#F59E0B" bright_blue = "#A855F7" bright_magenta = "#C084FC" bright_cyan = "#67E8F9" bright_white = "#E8E8F0" # Markdown viewer colors (Cmd+M reader mode) md_bold = "#FFFFFF" # bold text md_heading = "#FFFFFF" # heading text (# ## ###) md_code = "#A0D080" # inline code and code block text md_code_border = "#606060" # border around code blocks md_table_border = "#505050" # table grid lines md_match_bg = "#808000" # search match highlight background md_match_current_bg = "#FFCC00" # current search match highlight md_badge_bg = "#FFCC00" # badge/label background (e.g. line count) md_badge_fg = "#000000" # badge/label text color ``` #### Shell Hooks Command blocks (OSC 133) require shell hooks. Install via the command palette: `Cmd+P` then search "Install shell hooks". #### Themes External TOML theme files live in `~/.config/zurm/themes/`. Set `[theme] name` to the filename without `.toml`. A theme overrides any `[colors]` values it defines. #### Keybindings All keybindings are discoverable in-app via `Cmd+/` (help overlay) or `Cmd+P` (command palette). ## Optional Fonts *How to set up fallback fonts for CJK, emoji, and Nerd Font symbols.* ### Optional Fonts zurm ships with JetBrains Mono embedded. For CJK characters, emoji, Nerd Font symbols, and braille, add fallback fonts to the `[font]` section in your config. #### Setup Add the `fallbacks` list to `~/.config/zurm/config.toml`: ```toml [font] fallbacks = [ "/Users/you/Library/Fonts/NotoSansMonoCJKsc-Regular.otf", "/Users/you/Library/Fonts/NotoEmoji-Regular.ttf", "/Users/you/Library/Fonts/SymbolsNerdFontMono-Regular.ttf", "/Users/you/Library/Fonts/NotoSansSymbols2-Regular.ttf", ] ``` Fonts are tried in order when a glyph is missing from the primary font. The first font that contains the glyph is used. #### Download Run these commands once to install the recommended fallback fonts: ```bash # CJK (~16 MB) — Chinese, Japanese, Korean characters curl -sL "https://github.com/googlefonts/noto-cjk/raw/main/Sans/Mono/NotoSansMonoCJKsc-Regular.otf" \ -o ~/Library/Fonts/NotoSansMonoCJKsc-Regular.otf # Monochrome emoji (~2 MB) curl -sL "https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf" \ -o ~/Library/Fonts/NotoEmoji-Regular.ttf # Nerd Font symbols (~2.4 MB) — powerline glyphs, devicons curl -sL "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/NerdFontsSymbolsOnly.tar.xz" \ | tar xJ -C ~/Library/Fonts/ SymbolsNerdFontMono-Regular.ttf # Braille + extra symbols (~1.2 MB) curl -sL "https://github.com/google/fonts/raw/main/ofl/notosanssymbols2/NotoSansSymbols2-Regular.ttf" \ -o ~/Library/Fonts/NotoSansSymbols2-Regular.ttf ``` #### What Each Font Covers | Font | Glyphs | Size | |------|--------|------| | Noto Sans Mono CJK SC | Chinese, Japanese, Korean ideographs | ~16 MB | | Noto Emoji | Monochrome emoji outlines | ~2 MB | | Symbols Nerd Font Mono | Powerline, devicons, file type icons | ~2.4 MB | | Noto Sans Symbols 2 | Braille, mathematical symbols, extra Unicode | ~1.2 MB | #### Limitations Color emoji (Apple Color Emoji, Twemoji) are not supported. Ebitengine does not render color font formats. Monochrome emoji via Noto Emoji work fine. See [Emoji Limitations](/reference/emoji-limitations.html) for technical details. ## Server Mode (Mode B) ### Server Mode (Mode B) zurm can delegate PTY sessions to a background daemon (`zurm-server`) so they survive GUI restarts. This is opt-in per pane. Local panes are never affected. #### Prerequisites Build both binaries: ```bash make build make build-server ``` Place `zurm-server` next to `zurm` or anywhere on your PATH. #### Creating server panes | Method | Description | |--------|-------------| | Cmd+Shift+B | New tab with a server-backed pane | | Cmd+Shift+H | Split current pane horizontally; new half is server-backed | | Cmd+Shift+V | Split current pane vertically; new half is server-backed | | Cmd+P | Command palette: "New Server Tab", "Split Horizontal (Server)", "Split Vertical (Server)" | On first use, zurm auto-starts `zurm-server` in the background. No manual launch required. #### Identifying server panes Server-backed panes show `[SERVER]` in cyan in the status bar when focused. #### Session persistence 1. Create a server pane (Cmd+Shift+B) 2. Run commands, start processes 3. Quit zurm (Cmd+Q) 4. Relaunch zurm Server panes reconnect automatically. Recent output is replayed from a 64KB ring buffer so you see context immediately. If the server or session is gone, the pane falls back to a local shell. #### Attaching to existing sessions ##### From inside zurm Cmd+P, type "attach" to find "Attach to Server Session". This lists all live sessions on the server. Pick one to open it in a new tab. ##### From the command line ```bash # List active sessions zurm -ls # Output: # ID PID SIZE DIR # 7ff2aced69fc24a5 90626 120x35 /Users/you/projects # Attach by full ID zurm -a 7ff2aced69fc24a5 # Attach by short prefix (Docker-style matching) zurm -a 7ff ``` Short prefix matching works as long as the prefix is unambiguous. If multiple sessions match, zurm reports which ones matched. #### Server lifecycle - Auto-starts when a server pane is created and the server is not running - Runs as a detached background process (survives zurm close) - Logs to `~/.config/zurm/server.log` - Sessions are removed automatically when the shell exits - To stop the server: `pkill zurm-server` #### Configuration ```toml [server] address = "" # Unix socket path; empty = ~/.config/zurm/server.sock binary = "" # zurm-server binary path; empty = next to zurm, then PATH ``` #### Resource usage Each live session uses approximately 64KB (output replay buffer) plus a PTY file descriptor. Sessions are cleaned up when the shell exits. The server has no Ebitengine dependency and minimal memory footprint. ## Features Guide *Detailed walkthrough of zurm's features and how to use them.* ### Features Guide This guide explains zurm's features beyond the basics. All keybindings are discoverable in-app via `Cmd+/` (help overlay) or `Cmd+P` (command palette). #### Tabs Create tabs with `Cmd+T`. Each tab has its own pane layout tree. Switch between tabs with `Cmd+1` through `Cmd+9`, or `Cmd+Shift+[` / `Cmd+Shift+]` to cycle. **Rename a tab:** `Cmd+Shift+R` opens inline rename. Press Enter to confirm, Escape to cancel. **Tab notes:** `Cmd+Shift+N` lets you attach a short note to any tab. Tabs with notes show a `*` suffix in the tab bar. **Move tabs:** `Cmd+Shift+←/→` reorders tabs, or drag them with the mouse. **Activity indicator:** A purple dot appears on background tabs when their terminal produces new output. This helps you notice when a long-running command completes. #### Pin Tabs Pin frequently-used tabs to keyboard slots for instant switching. 1. Press `Cmd+G` to enter pin mode 2. Press `Shift+A` through `Shift+L` to assign the active tab to that slot 3. Press `A` through `L` to jump to a pinned tab 4. Press the same `Shift+` key again to unpin Pinned tabs show a `·X` badge (where X is the slot letter) in the tab bar, and `[X]` in the tab switcher/search. #### Tab Parking Park tabs you want to keep running but don't need in the tab bar right now. Parked tabs are hidden from the tab bar — their PTY stays alive, shell state is preserved. **Park a tab:** `Cmd+Shift+K` parks the active tab. You cannot park the last visible tab. **Find parked tabs:** `Cmd+J` (tab search) shows all tabs — visible and parked. Parked entries show a `[P]` badge and dimmed title. Select one to unpark and activate it. **Pin slot jump:** If a parked tab has a pin slot, jumping to that slot (`Cmd+G` then the letter) unparks it automatically. **Tab bar badge:** When parked tabs exist, a `[N↓]` count badge appears at the right edge of the tab bar. **Session restore:** Parked tabs are saved and restored across sessions. **Max open tabs:** The `max_open` config key caps the number of visible tabs (default 10, `0` = unlimited). When the limit is reached, `Cmd+T` shows a status flash — park or close a tab first. #### Tab Switcher & Search **Tab switcher** (`Cmd+Shift+T`): Shows visible tabs with pin badges. Arrow keys to select, Enter to switch. **Tab search** (`Cmd+J`): Type to search all tabs (visible + parked) by title or working directory. Results ranked by match position. Parked tabs show a `[P]` badge — selecting one unparks and activates it. #### Panes Split the current tab into multiple panes: - `Cmd+D` — horizontal split (side by side) - `Cmd+Shift+D` — vertical split (top and bottom) **Navigate panes:** `Cmd+Arrow` moves focus to an adjacent pane. `Cmd+[` / `Cmd+]` cycles through panes. **Resize:** `Cmd+Opt+Arrow` adjusts the split ratio, or drag the divider with the mouse. **Zoom:** `Cmd+Z` maximizes the focused pane (hides all others). Press again to restore. **Rename a pane:** Double-click the pane header label. **Close:** `Cmd+W` closes the focused pane. If it's the last pane in a tab, the tab closes. **Detach/move panes:** Use the command palette (`Cmd+P`) to detach a pane to a new tab, or move it to the next/previous tab. #### Focus History `Cmd+;` navigates back through your focus history — the last 50 panes you focused. Useful when you split multiple panes and want to quickly return to where you were. #### In-Buffer Search `Cmd+F` opens the search bar at the bottom of the terminal. Type to search the scrollback buffer. - `Enter` / `↓` — next match - `Shift+Enter` / `↑` — previous match - `Escape` — close search The current match is highlighted in yellow, other matches in blue. The match counter shows `N of M`. #### File Explorer `Cmd+E` toggles the file explorer sidebar. | Key | Action | |-----|--------| | `↑` / `↓` | Navigate entries | | `Enter` / `→` | Expand directory or open file in viewer | | `←` | Collapse directory | | `n` | New file | | `Shift+N` | New directory | | `r` | Rename | | `d` | Delete (with confirmation) | | `c` | Copy path to clipboard | | `o` | Open in Finder | | `/` | Search within explorer | | `Esc` | Close explorer | Configure side and width in `[file_explorer]` config section. #### Command Blocks (OSC 133) When shell hooks are installed, zurm detects command boundaries and renders visual blocks around each command and its output. **Setup:** `Cmd+P` → search "Install shell hooks" → Enter. This adds OSC 133 sequences to your shell prompt. **Toggle:** `Cmd+B` enables/disables block rendering at runtime. **Features when enabled:** - Left accent stripe colored by exit status (green = success, red = failure) - Elapsed execution time badge (e.g., "1.2s") - Hover any block to see copy buttons: `cmd` (command only), `out` (output only), `all` (both) - Optional background tint (`bg_color` + `bg_alpha` in config) Configure in `[blocks]` config section. #### Command Palette `Cmd+P` opens the command palette — a searchable list of all available actions. Type to filter, arrow keys to select, Enter to execute. The palette includes all tab, pane, search, recording, and configuration commands. #### Markdown Reader Mode `Cmd+Shift+M` opens the markdown viewer, which renders `.md` files with: - Headings, bold, italic, strikethrough - Code blocks with syntax-aware coloring - Tables with border rendering - Inline code highlighting - Search within the document (`/` to search, `n`/`N` to navigate matches) Colors are customizable via the `md_*` fields in `[colors]` config section. #### llms.txt Browser `Cmd+L` opens the llms.txt browser — a text-mode web browser for reading `llms.txt` files (AI-readable documentation). | Key | Action | |-----|--------| | `f` | Enter hint mode (follow links) | | `a`-`z` | Select a link badge | | `Backspace` / `H` | Navigate back | | `L` | Navigate forward | | `Cmd+Enter` | Send current content to the terminal pane | #### Vault (Command History) Encrypted local command history with ghost text suggestions. **Enable:** Set `vault.enabled = true` in config. **How it works:** 1. zurm imports your shell history on startup 2. As you type, a ghost suggestion appears inline (dimmed text) 3. Press `→` (right arrow) to accept the suggestion 4. Commands prefixed with a space are never stored (privacy) **Encryption:** Commands are stored in `~/.config/zurm/vault.enc` using AES-256. The key is auto-generated at `~/.config/zurm/vault.key` (mode 0600). Configure in `[vault]` config section. #### Recording **Screenshot:** `Cmd+Shift+S` captures the current terminal as a PNG file. **Screen recording:** `Cmd+Shift+.` starts/stops recording. Output is saved as MP4. Files are saved to the current working directory. #### Tab Hover Minimap Hover over a background tab in the tab bar to see a minimap preview of its content. The preview appears after a configurable delay. Configure in `[tabs.hover]`: `enabled`, `delay_ms`, `width`, `height`. #### Stats Overlay `Cmd+I` toggles a small stats panel in the top-right corner showing: - **TPS / FPS** — Ebiten tick rate and frame rate - **Goroutines** — number of active Go goroutines - **Heap** — allocated / system heap memory - **GC** — garbage collection count and last pause duration - **Tabs / Panes** — count of open tabs and panes - **Buffer** — focused pane dimensions and scrollback depth #### Server Mode (Mode B) zurm-server provides persistent PTY sessions that survive app restarts. **Create a server tab:** `Cmd+Shift+B` **Split with server pane:** `Cmd+Shift+H` (horizontal) or `Cmd+Shift+V` (vertical) **Attach to existing session:** `Cmd+P` → "Attach to Server Session" Server sessions persist even when zurm is closed. Reconnecting replays the last 64KB of output. Configure in `[server]` config section. See the [Server Mode guide](../getting-started/04-server-mode.md) for setup details. #### URL Clicking `Cmd+Click` on a URL in the terminal opens it in your default browser. URLs are detected automatically as you hover (highlighted with an underline). #### Bell When a terminal sends a bell character (BEL / `\a`), zurm can: - Flash the pane border in a configurable color (`style = "visual"`) - Play the system beep sound (`sound = true`) A bell icon appears briefly on the affected tab. Configure in `[bell]` config section. #### Hot Reload Press `Cmd+,` to reload `~/.config/zurm/config.toml` without restarting. Colors, status bar settings, and most UI options update immediately. #### Session Persistence When `session.enabled = true`, zurm saves your tab layout (CWDs, titles, pin slots, notes, pane splits) on quit and restores it on launch. Configure in `[session]` config section. ## Themes *How to use and create custom color themes for zurm.* ### Themes zurm supports external TOML theme files for full color customization. #### Built-in Themes Two themes ship with zurm and are written to `~/.config/zurm/themes/` on first launch: - **dark** — purple-accent dark palette (default) - **light** — high-contrast light palette Activate a theme in your config: ```toml [theme] name = "dark" # or "light", or any custom theme name ``` Set `name = ""` to disable themes and use `[colors]` directly. #### Creating a Custom Theme 1. Create a TOML file in `~/.config/zurm/themes/`: ```bash touch ~/.config/zurm/themes/my-theme.toml ``` 2. Add a `[colors]` section with any colors you want to override: ```toml # ~/.config/zurm/themes/my-theme.toml # Only include colors you want to change — unset colors fall back to defaults. [colors] background = "#1A1B26" foreground = "#C0CAF5" cursor = "#F7768E" border = "#292E42" separator = "#3B4261" black = "#15161E" red = "#F7768E" green = "#9ECE6A" yellow = "#E0AF68" blue = "#7AA2F7" magenta = "#BB9AF7" cyan = "#7DCFFF" white = "#A9B1D6" bright_black = "#414868" bright_red = "#F7768E" bright_green = "#9ECE6A" bright_yellow = "#E0AF68" bright_blue = "#7AA2F7" bright_magenta = "#BB9AF7" bright_cyan = "#7DCFFF" bright_white = "#C0CAF5" # Optional: customize markdown viewer colors md_bold = "#C0CAF5" md_heading = "#7AA2F7" md_code = "#9ECE6A" md_code_border = "#414868" md_table_border = "#3B4261" md_match_bg = "#E0AF68" md_match_current_bg = "#F7768E" md_badge_bg = "#E0AF68" md_badge_fg = "#1A1B26" ``` 3. Activate it: ```toml [theme] name = "my-theme" ``` 4. Hot-reload with `Cmd+,`. #### Theme + Config Interaction When a theme is active, colors are resolved in this priority: 1. **User-explicit `[colors]` in config.toml** — highest priority. If you uncomment a color in your config, it always wins. 2. **Theme file** — colors defined in the theme file. 3. **Built-in defaults** — colors not set by either source. This means you can use a theme as a base and override individual colors: ```toml [theme] name = "dark" [colors] # Override just the cursor color from the dark theme cursor = "#FF0000" ``` #### Complete Color Reference All available color fields for themes and `[colors]` config: ##### Terminal Colors | Field | Description | Default | |-------|-------------|---------| | `background` | Terminal background | `#0F0F18` | | `foreground` | Terminal foreground text | `#E8E8F0` | | `cursor` | Cursor color | `#A855F7` | | `border` | UI borders (panes, panels) | `#1C1C2E` | | `separator` | Tab bar and status bar separator line | `#555570` | ##### 16-Color ANSI Palette | Field | Default | |-------|---------| | `black` | `#555570` | | `red` | `#F87171` | | `green` | `#34D399` | | `yellow` | `#F59E0B` | | `blue` | `#7C3AED` | | `magenta` | `#C084FC` | | `cyan` | `#67E8F9` | | `white` | `#8888A8` | | `bright_black` | `#555570` | | `bright_red` | `#F87171` | | `bright_green` | `#34D399` | | `bright_yellow` | `#F59E0B` | | `bright_blue` | `#A855F7` | | `bright_magenta` | `#C084FC` | | `bright_cyan` | `#67E8F9` | | `bright_white` | `#E8E8F0` | ##### Markdown Viewer Colors | Field | Description | Default | |-------|-------------|---------| | `md_bold` | Bold text | `#FFFFFF` | | `md_heading` | Heading text (# ## ###) | `#FFFFFF` | | `md_code` | Code blocks and inline code | `#A0D080` | | `md_code_border` | Border around code blocks | `#606060` | | `md_table_border` | Table grid lines | `#505050` | | `md_match_bg` | Search match highlight | `#808000` | | `md_match_current_bg` | Current search match | `#FFCC00` | | `md_badge_bg` | Badge background (line count) | `#FFCC00` | | `md_badge_fg` | Badge text color | `#000000` | #### Tips - All colors are `#RRGGBB` hex format - You don't need to include every color in a theme — only the ones you want to change - Copy `~/.config/zurm/themes/dark.toml` as a starting point for custom themes - Use `Cmd+,` to preview changes instantly ## Architecture *High-level architecture overview for contributors.* ### Architecture zurm is a GPU-accelerated terminal emulator for macOS, built on [Ebiten](https://ebitengine.org/) (OpenGL game engine) and Go's `os/exec` for PTY management. #### System Overview ``` ┌──────────────────────────────────────────────────────────┐ │ Ebiten Game Loop │ │ │ │ Update() Draw() Layout() │ │ ─ input routing ─ Renderer.DrawAll ─ DPI scaling │ │ ─ channel drains ─ layer compositing │ │ ─ state updates ─ cache skip logic │ └──────┬─────────────────────┬─────────────────────────────┘ │ │ ▼ ▼ ┌─────────────┐ ┌──────────────────┐ │ Terminal │ │ Renderer │ │ ─ Buffer │◄────│ ─ offscreen │ RLock │ ─ Parser │ │ ─ blocksLayer │ │ ─ Cursor │ │ ─ modalLayer │ └──────┬───────┘ └──────────────────┘ │ Lock ▼ ┌─────────────┐ │ PtyBackend │ ◄── Strategy Pattern │ (interface)│ ├─────────────┤ │ PTYManager │ Mode A: local PTY (os/exec + pty) │ ServerBack. │ Mode B: zurm-server over HTTP └─────────────┘ ``` #### Data Flow ``` PTY output → PtyBackend.StartReader() goroutine → Parser.Feed() [under write lock] → ScreenBuffer mutation (cells, cursor, scrollback) → RenderGen++ (atomic) Game.Draw() → Renderer.DrawAll(DrawState) → per-pane cache check (skip if gen unchanged) → ScreenBuffer.RLock → read cells → RUnlock → draw to offscreen → composite layers → GPU ``` #### Package Responsibilities | Package | Responsibility | |---------|---------------| | `main` (root) | Game struct, Update/Draw/Layout, decomposed into 19 focused files (see below) | | `terminal/` | VT500 parser, screen buffer (cells + scrollback), cursor state, PTY backend interface | | `renderer/` | GPU rendering via Ebiten — decomposed into 17+ files (see below) | | `pane/` | Binary tree layout, pane view (`NewPane` DI constructor), factory for terminal creation | | `tab/` | Tab management, layout node ownership, activity detection | | `config/` | TOML parsing, defaults, hot-reload, theme merge | | `session/` | Session save/restore (JSON) — tab CWDs, titles, layout tree | | `zserver/` | Persistent PTY server — session management, ring buffer replay, subscriber pattern | | `fileexplorer/` | File tree domain logic — pure functions, no rendering dependency | | `vault/` | Encrypted command history, ghost text suggestions, AES-256 encryption | | `markdown/` | Markdown AST parser — headings, code blocks, tables, bold/italic | | `help/` | Keybinding definitions, command palette entries | | `recorder/` | Screenshot (PNG) and screen recording | ##### Main Package File Structure The root `main` package was decomposed from a single `main.go` into focused files: | File | Responsibility | |------|---------------| | `main.go` | Game struct, Update/Draw/Layout, init, entry point | | `game_input.go` | Keyboard routing, overlay priority cascade | | `game_mouse.go` | Mouse dispatch, selection, divider drag, URL click | | `game_drain.go` | PTY drains, polling, clipboard, paste | | `game_lifecycle.go` | Focus, resize, suspend, dropped files | | `game_tabs.go` | Tab create/close/switch/pin/search/switcher | | `game_panes.go` | Pane splits, focus, resize, zoom | | `game_overlays.go` | Menus, palette, confirm, overlay handlers | | `game_search.go` | SearchController + search input | | `game_explorer.go` | File explorer input handlers | | `game_viewer.go` | Markdown viewer, llms.txt, URL input | | `game_server.go` | Server session management | | `game_misc.go` | Rename, notes, session, config reload, shell hooks | | `tab_manager.go` | TabManager — owns tab slice, active index, focus history, drag/pin state | | `palette_controller.go` | PaletteController — filter state, selection | | `explorer_controller.go` | ExplorerController — tree state, expand/collapse | | `mouse_state.go` | SelectionDragger + DividerDragHandler value types | | `key_repeat.go` | Shared KeyRepeatHandler for auto-repeat across controllers | | `status_poller.go` | Background git status + foreground process polling | ##### Renderer Package File Structure The `renderer/` package was decomposed into focused sub-renderers: | File | Responsibility | |------|---------------| | `renderer.go` | Renderer struct, DrawAll orchestration, layer compositing, cache logic | | `pane_render.go` | DrawPane, drawPaneTo, drawDividers | | `tabbar.go` | drawTabBar | | `tabhover.go` | Tab hover thumbnails, cache keys | | `tabswitcher.go` | Tab switcher overlay | | `tabsearch.go` | Tab search overlay | | `statusbar.go` | Status bar rendering | | `overlay.go` | Help overlay, markdown viewer overlay | | `palette.go` | Command palette rendering | | `search.go` | In-buffer search bar | | `fileexplorer.go` | File explorer panel | | `menu.go` | Context menu rendering | | `blocks.go` | OSC 133 command block decorations | | `stats.go` | Debug stats overlay | | `font.go` | Font renderer, glyph drawing, display width | | `renderconfig.go` | RenderConfig — decouples renderer from config package | | `state.go` | All 15 UI state types consolidated (DrawState, SearchState, etc.) | | `helptypes.go` | OverlayMenuItem, OverlayKeyBinding — decouples from help package | | `mdtypes.go` | MdSpanStyle, MdSpan, MdStyledLine — decouples from markdown package | | `uicolors.go` | UIColors derivation, color helpers | #### Key Design Patterns ##### Composite — Pane Layout Tree `LayoutNode` is a binary tree. Each node is a `Leaf` (holds a `Pane`), `HSplit` (left | right), or `VSplit` (top / bottom). `Ratio` controls the split proportion. ``` HSplit (0.5) / \ Leaf(pane1) VSplit(0.6) / \ Leaf(pane2) Leaf(pane3) ``` `ComputeRects()` recursively assigns pixel rectangles. `Leaves()` returns all panes in DFS order. Pane creation uses DI constructors in `pane/factory.go`: `NewLocal()` and `NewServer()` build terminals with the correct backend, while `NewPane()` wraps a terminal into a `Pane` view without importing `config`. ##### Strategy — PTY Backend `PtyBackend` interface abstracts local vs. remote terminal I/O: ```go type PtyBackend interface { Write(p []byte) (int, error) Resize(cols, rows int) error Dead() <-chan struct{} Close() Pid() int ForegroundPgid() (int, error) StartReader(parser *Parser, buf *ScreenBuffer, paused *atomic.Bool) } ``` - **PTYManager** (Mode A) — local shell via `os/exec` + `creack/pty` - **ServerBackend** (Mode B) — connects to zurm-server over HTTP, PTY persists across app restarts ##### State Machine — VT500 Parser The parser has 8 states dispatched via method calls: | State | Trigger | Handles | |-------|---------|---------| | `Ground` | default | printable characters, C0 controls | | `Escape` | ESC | escape sequence start | | `EscapeInterm` | ESC + intermediate | two-byte escape sequences | | `CSIEntry` | ESC [ | control sequence introducer | | `CSIParam` | digits/semicolons | CSI parameter collection | | `CSIInterm` | intermediate in CSI | CSI with intermediate bytes | | `OSC` | ESC ] | operating system commands (title, CWD, shell integration) | | `DCS` | ESC P | device control strings (ignored) | `Parser.Feed()` processes raw PTY bytes and mutates the `ScreenBuffer` under the caller's write lock. ##### Observer — Server Session Subscriptions `zserver.Session` uses a pub-sub pattern for multi-client output: - PTY reader writes output to all subscriber channels - `subscribe()` returns a buffered `chan []byte` - Slow clients drop packets (non-blocking send with `select/default`) - `ringBuf` (64KB circular buffer) stores recent output for replay on reconnect ##### Snapshot — Lock Minimization The renderer minimizes lock duration by taking snapshots: 1. `DrawState` is built in `Game.Draw()` with current state 2. `blockSnap` copies block data under `RLock`, then renders without holding the lock 3. `paneCacheEntry` tracks `renderGen` — unchanged panes skip `DrawPane` entirely #### Concurrency Model ``` Main Thread (Ebiten) ├── Game.Update() — input handling, state mutations ├── Game.Draw() — renderer calls (single-threaded GPU) │ PTY Reader Goroutines (one per pane) ├── Read PTY bytes ├── Lock ScreenBuffer ├── Parser.Feed() — mutates cells, cursor, scrollback ├── Unlock ScreenBuffer │ Async Goroutines ├── Git status polling (background, result via channel) ├── Clipboard operations (paste via channel) ├── Screenshot encoding (PNG in background) └── CWD/foreground process detection ``` **Synchronization:** - `sync.RWMutex` on `ScreenBuffer` — parser writes, renderer reads - `atomic.Bool` for `paused` flag — spin-wait during resize (avoids race between resize and parse) - `atomic.Uint64` for `renderGen` — activity detection without locking - `atomic.Bool` for `osc7Active`, `osc133Active` — feature detection flags - Buffered channels for async results (clipboard, git, screenshots) #### Rendering Pipeline Three composited layers: 1. **offscreen** — pane cell grids, tab bar, status bar, pane labels 2. **blocksLayer** — OSC 133 command block decorations (borders, duration badges, copy buttons) 3. **modalLayer** — overlays (search bar, palette, file explorer, help, confirm dialogs) Final composite: `offscreen` → `blocksLayer` (alpha blend) → `modalLayer` (alpha blend) → screen. **Performance optimizations:** - Per-pane render cache skips unchanged panes (tracks renderGen, viewOffset, cursor position) - `layoutDirty` flag triggers full offscreen clear only when layout changes - `overlayBg` (1×1 backdrop image) created once, scaled to screen size - Auto-idle reduces TPS when window is unfocused - `allKeys` computed once at package init (avoids per-frame allocation) #### Session Persistence ``` ~/.config/zurm/session.json ├── version: 1 ├── activeTab: int └── tabs[] ├── cwd, title, pinnedSlot, note └── layout (recursive PaneLayout tree) ├── kind: "leaf" | "hsplit" | "vsplit" ├── ratio: float64 ├── cwd, customName, serverSessionID └── left, right (children) ``` Saved on quit (if `session.enabled`), restored on launch (if `restore_on_launch`). #### Configuration - `config.Load()` reads `~/.config/zurm/config.toml`, starts from `Defaults`, overlays user values - `config.LoadWithMeta()` re-reads and returns TOML MetaData for theme merge - Theme merge: `MergeColorsWithMeta()` uses TOML metadata to determine user-explicit vs. theme-default colors - Hot-reload via `Cmd+,` — calls `LoadWithMeta()`, applies theme, updates renderer colors, clears pane cache ## FAQ *Frequently asked questions about zurm.* ### FAQ #### How do I customize colors? Two options: 1. **Use a theme:** Set `[theme] name = "dark"` (or `"light"`, or a custom theme name) in `~/.config/zurm/config.toml`. See the [Themes guide](getting-started/06-themes.md). 2. **Edit colors directly:** Uncomment and modify values in the `[colors]` section of your config. Colors set here override the active theme. Hot-reload with `Cmd+,` to preview changes instantly. #### Why don't emojis render in color? Ebitengine (the GPU engine zurm uses) does not support color font rendering. Monochrome emoji outlines are available via the font fallback chain. See [Emoji Limitations](reference/emoji-limitations.md) for details and setup instructions. #### How do I change the font? Edit `~/.config/zurm/config.toml`: ```toml [font] family = "JetBrains Mono" size = 15 file = "/Users/you/Library/Fonts/MyFont-Regular.ttf" # custom TTF/OTF ``` The `file` field overrides the embedded JetBrains Mono. See the [Optional Fonts guide](getting-started/03-optional-fonts.md) for fallback font setup. #### Can I use zurm with tmux? Yes, tmux works inside zurm. However, zurm has built-in tabs, panes, and splits — so tmux is redundant for most use cases. If you use tmux for session persistence, consider zurm's [Server Mode](getting-started/04-server-mode.md) instead. #### How do I install shell hooks? Open the command palette (`Cmd+P`), search "Install shell hooks", and press Enter. This adds OSC 133 sequences to your shell prompt, enabling command blocks (`Cmd+B`), command duration display, and exit status indicators. #### What do the colored dots on tabs mean? A purple dot on a background tab indicates **activity** — the terminal in that tab produced new output since you last looked at it. The dot disappears when you switch to that tab. A bell icon appears briefly when a terminal sends a BEL character. #### How do I back up my config? Your config and data live in `~/.config/zurm/`: ``` ~/.config/zurm/ ├── config.toml # main configuration ├── themes/ # theme files (dark.toml, light.toml, custom) ├── session.json # saved tab/pane layout ├── vault.enc # encrypted command history (if vault enabled) └── vault.key # vault encryption key ``` Back up this entire directory to preserve your settings. #### Does zurm support ligatures? No. Ligature rendering requires complex text shaping (OpenType GSUB table processing) which is not currently implemented. Each glyph is rendered independently in the cell grid. #### How do I use server mode? `Cmd+Shift+B` creates a server-backed tab. The terminal session persists even when zurm is closed. See the [Server Mode guide](getting-started/04-server-mode.md) for full setup instructions. #### How do I report a bug? Open an issue at [github.com/studiowebux/zurm/issues](https://github.com/studiowebux/zurm/issues). Include: - zurm version (`zurm --version`) - macOS version - Steps to reproduce - Terminal output or screenshots if applicable #### Where is the config file? `~/.config/zurm/config.toml` — created automatically on first launch with all defaults and comments. #### How do I reset to default config? Delete or rename `~/.config/zurm/config.toml` and relaunch zurm. A fresh config with all defaults will be written. ## zurm *A GPU-rendered terminal emulator for macOS, written in Go.* ### zurm A GPU-rendered terminal emulator for macOS, written in Go with Ebitengine. ## Emoji Limitations *Technical details on emoji rendering support and limitations.* ### Emoji Rendering Limitations #### Summary Full-color emoji rendering is not supported in zurm due to Ebitengine limitations. However, **monochrome emoji outlines** are now available via the font fallback chain using Noto Emoji. #### What Works - **Monochrome emoji** via `NotoEmoji-Regular.ttf` as a fallback font — renders black-and-white outlines - **CJK characters** via Noto Sans Mono CJK fallback - **Nerd Font symbols** (powerline, devicons) via Symbols Nerd Font fallback - Multiple fallbacks can be chained in config.toml `[font] fallbacks` #### What Doesn't Work - **Color emoji** (multi-colored glyphs like 😀🎉🔥) — Ebitengine cannot render color fonts - **Apple Color Emoji** — TrueType Collection format not supported by Go font libraries - **ZWJ sequences** (👨‍👩‍👧‍👦, 🏳️‍🌈) — require complex text shaping not available in Go - **Skin tone modifiers** (👋🏽) — modifier codepoints not combined - **Flag sequences** (🇨🇦, 🇺🇸) — regional indicator pairs not rendered as flags #### Technical Background 1. **Ebiten Limitation**: The Ebiten game engine doesn't support color fonts ([Issue #2649](https://github.com/hajimehoshi/ebiten/issues/2649)) 2. **Go Font Library**: The `golang.org/x/image/font` package returns `ErrColoredGlyph` when encountering color glyphs 3. **No Native Bridge**: Pure Go programs cannot easily access platform-native text rendering (CoreText on macOS, DirectWrite on Windows) #### Workarounds - Use **Nerd Font** symbols for icons in your prompt — they render perfectly as single-codepoint glyphs - Use monochrome emoji (outlined/black-and-white) via NotoEmoji fallback font - For programs that display emoji (e.g., `npm`, `yarn`), the outlines are readable even without color #### Setup Add fallback fonts to `~/.config/zurm/config.toml`: ```toml [font] fallbacks = [ "~/Library/Fonts/NotoSansMonoCJKsc-Regular.otf", # CJK "~/Library/Fonts/NotoEmoji-Regular.ttf", # monochrome emoji "~/Library/Fonts/SymbolsNerdFontMono-Regular.ttf", # powerline + devicons ] ``` Download commands are in the config template comments. #### References - [Ebiten Issue #2649 - Emoji Support](https://github.com/hajimehoshi/ebiten/issues/2649) - [go-text/typesetting Issue #191 - Color Emoji](https://github.com/go-text/typesetting/issues/191) - [OpenType Spec - SVG Table](https://docs.microsoft.com/en-us/typography/opentype/spec/svg)