Install, use and configure guiltty.

Everything here is also in guiltty's README and example config. How each part works is in the book.

Overview

guiltty is a GPU-accelerated terminal emulator in Elixir and C. Elixir holds the behavior: the parser, the screen, tabs, windows, config. C does the syscalls and the pixels: a PTY NIF, a renderer (Vulkan, or OpenGL) that runs as its own process, and the small guiltty command.

It runs on Linux only. It's been tried on openSUSE Tumbleweed, X11 and an AMD GPU; Wayland, Intel, NVIDIA and other distributions should work but haven't been tried.


Requirements

guiltty is built from source on your machine, so installing it needs Elixir and a C toolchain. The installed copy carries its own Erlang runtime: once it's installed, it runs without Elixir.

What Why
Elixir 1.18+, Erlang/OTP 26+ building guiltty and its release
GCC 15+, make, pkg-config the C is C23 and uses #embed
SDL3 (development files) the window, keyboard and mouse
FreeType, fontconfig, HarfBuzz (dev files) finding fonts, drawing and shaping text
Vulkan loader and headers (dev files) the Vulkan renderer
glslc (from shaderc) compiles the Vulkan shaders to SPIR-V

Your distribution's packages work if they're recent enough; for Elixir and Erlang, a version manager such as asdf gets you exact versions. elixir --version should say Elixir 1.18 or newer, compiled with Erlang/OTP 26 or newer.

Tested Elixir and Erlang/OTP

Elixir Erlang/OTP Status
1.20.4 29 Developed on
1.19.7 28 Builds, tests pass
1.19.7 26 Builds, tests pass
1.18.4 26 Builds, tests pass

Anything older than Elixir 1.18 or Erlang/OTP 26 isn't supported.

GPU and fonts

guiltty needs a GPU driver with OpenGL 3.3, which almost any desktop from the last decade has. For Vulkan it needs a Vulkan 1.3 driver (for AMD and Intel GPUs, Mesa's; only AMD has been tried). Without one, it draws with OpenGL.

Fonts come from fontconfig. guiltty looks best with a Nerd Font (for prompt icons), Noto Color Emoji (for emoji) and a font for Chinese, Japanese and Korean, such as Noto Sans CJK.


Install

1. What to install first

On these distributions, one command installs it all. Only openSUSE has been tried; the other package names come from each distribution's package listings.

shell
sudo zypper install gcc make pkg-config SDL3-devel freetype2-devel \
  fontconfig-devel harfbuzz-devel vulkan-devel shaderc elixir erlang-epmd

The one guiltty is developed on.

2. guiltty

shell
git clone https://github.com/z7ealth/guiltty.git
cd guiltty
./install.sh

install.sh checks for the tools above, builds a release and installs it into ~/.local; it needs no root.

Where What
~/.local/lib/guiltty/ the release
~/.local/bin/guiltty the command
~/.local/share/applications/guiltty.desktop the menu entry
~/.local/share/icons/hicolor/*/apps/guiltty.png the icon

3. Run it

Run guiltty, or find it in your applications menu. If your shell says guiltty: command not found, add ~/.local/bin to your PATH.


Update and uninstall

Update
shell
git pull && ./install.sh

If guiltty is running, the script asks before stopping it, since that closes its tabs (--yes skips the question). Run from a guiltty tab, it finishes after that tab closes, then opens guiltty again; its output goes to ~/.local/state/guiltty/install.log.

Uninstall
shell
./install.sh --uninstall

Your config stays.

Install somewhere else
shell
PREFIX=/opt/guiltty ./install.sh

Windows and tabs

Every window has its own tabs. guiltty (or Ctrl + Shift + N ) opens another window, and closing one never touches the others. guiltty stops after its last window.

Locked tabs

Closing a window ends its tabs, except the ones you lock with Ctrl + Shift + L . They show a 🔒 and keep running, and the next guiltty brings that window back with them, where you left them.

Closing a busy tab

Ctrl + Shift + W on a tab that's running a program (vim, ssh, a build) asks first: the tab bar says what's running, and pressing it again closes the tab. A tab waiting at its prompt closes at once.

Marks

A tab you're not looking at shows ● when it prints something and 🔔 when it rings the bell (a build that ends with ; printf '\a', say), until you switch to it.

Names and order

Ctrl + Shift + R renames the tab: type a name, Enter. The name stays even when the program changes its title; an empty name gives the title back. Ctrl + Shift + PgUp and Ctrl + Shift + PgDn move the tab left and right.

Where a new tab starts

A new window's first tab starts in the folder you ran guiltty in. Other new tabs follow working_directory: by default, where the current tab's shell is. A tab's shell gets its window's display (DISPLAY and the like), so GUI programs started from it open where that window is.


Scrollback, search, selection

Each tab keeps 10,000 rows of scrollback by default (scrollback). Shift + PgUp and Shift + PgDn scroll back and forward, as does the mouse wheel; Shift + Home and Shift + End jump to the oldest row and back to the prompt. Ctrl + Shift + K clears the scrollback; the screen stays.

Search

Ctrl + Shift + F searches the scrollback. Enter finds the next older match, Shift+Enter the next newer one, and Escape ends the search.

Selection and the clipboard

  • Selecting with the mouse copies to the primary selection; a middle click pastes it.
  • Ctrl + Shift + C copies the selection to the clipboard, and Ctrl + Shift + V or Shift + Insert paste it.
  • In programs that use the mouse (vim, htop), hold Shift to select.
  • copy_on_select changes what selecting does: :clipboard also copies to the clipboard, false copies nothing until Ctrl + Shift + C .

Broadcast

Broadcast types into every tab in a group at once: the same change on five servers, say. Open a tab per server, ssh in each, and press Ctrl + Shift + B in each to put it in the group. What you type in one goes to all of them. Each window has its own group.

  • A tab can join only while it's running one of broadcast_programs (by default ssh and mosh-client).
  • If a connection drops, typing goes nowhere until that tab leaves the group, and the tab bar shows which tab was lost. A command meant for your servers doesn't land on your own machine.
  • A local tab (your own shell) joins on purpose with a second Ctrl + Shift + B . It's marked 🏠 and isn't checked.

Keys

These are the defaults, as they ship. Every one can be rebound in the config's keys, and Ctrl + Shift + / shows them as you bound them.

new_window A new window, with its own tabs. Ctrl + Shift + N
new_tab A new tab. Ctrl + Shift + T
close_tab Close the tab. If it's running a program, the tab bar says what, and pressing it again closes it. Ctrl + Shift + W
next_tab The next tab. Ctrl + PgDn Ctrl + Tab
prev_tab The previous tab. Ctrl + PgUp Ctrl + Shift + Tab
move_tab_left Move the tab left. Ctrl + Shift + PgUp
move_tab_right Move the tab right. Ctrl + Shift + PgDn
rename_tab Rename the tab: type a name, Enter. An empty name gives the program's title back. Ctrl + Shift + R
scroll_page_up Scroll back a page. Shift + PgUp
scroll_page_down Scroll forward a page. Shift + PgDn
scroll_top Scroll to the oldest row. Shift + Home
scroll_bottom Scroll back to the prompt. Shift + End
clear_scrollback Drop the tab's scrollback; the screen stays. Ctrl + Shift + K
search Search the scrollback: Enter finds older, Shift+Enter newer, Escape ends. Ctrl + Shift + F
copy Copy the selection to the clipboard. Ctrl + Shift + C
zoom_in Bigger text. Ctrl + = Ctrl + Shift + =
zoom_out Smaller text. Ctrl + -
zoom_reset The configured size again. Ctrl + 0
open_config Open the config file in your editor, creating it from the example first. Ctrl + ,
cheatsheet Every key, as you bound it, on one page. Ctrl + Shift + /
lock_tab Lock the tab, so it outlives its window. Ctrl + Shift + L
broadcast Put the tab in the broadcast group, or take it out. Ctrl + Shift + B
Action Keys What it does
new_window Ctrl + Shift + N A new window, with its own tabs.
new_tab Ctrl + Shift + T A new tab.
close_tab Ctrl + Shift + W Close the tab. If it's running a program, the tab bar says what, and pressing it again closes it.
next_tab Ctrl + PgDn Ctrl + Tab The next tab.
prev_tab Ctrl + PgUp Ctrl + Shift + Tab The previous tab.
move_tab_left Ctrl + Shift + PgUp Move the tab left.
move_tab_right Ctrl + Shift + PgDn Move the tab right.
rename_tab Ctrl + Shift + R Rename the tab: type a name, Enter. An empty name gives the program's title back.
scroll_page_up Shift + PgUp Scroll back a page.
scroll_page_down Shift + PgDn Scroll forward a page.
scroll_top Shift + Home Scroll to the oldest row.
scroll_bottom Shift + End Scroll back to the prompt.
clear_scrollback Ctrl + Shift + K Drop the tab's scrollback; the screen stays.
search Ctrl + Shift + F Search the scrollback: Enter finds older, Shift+Enter newer, Escape ends.
copy Ctrl + Shift + C Copy the selection to the clipboard.
zoom_in Ctrl + = Ctrl + Shift + = Bigger text.
zoom_out Ctrl + - Smaller text.
zoom_reset Ctrl + 0 The configured size again.
open_config Ctrl + , Open the config file in your editor, creating it from the example first.
cheatsheet Ctrl + Shift + / Every key, as you bound it, on one page.
lock_tab Ctrl + Shift + L Lock the tab, so it outlives its window.
broadcast Ctrl + Shift + B Put the tab in the broadcast group, or take it out.

Fixed keys

Paste ( Ctrl + Shift + V , Shift + Insert ), middle-click paste and Shift-to-select aren't in keys and can't be rebound.

Rebinding

Each action takes a list of keys. Names are letters, digits, f1 to f12, tab, enter, page_up, page_down, home, end, insert, delete, or a punctuation character like = or -, with any of the ctrl+, shift+ and alt+ modifiers. Actions you leave out keep their defaults.

config.exs
keys: [
  new_tab: ["ctrl+t"],
  search: ["ctrl+shift+f", "f3"]
]

The config file

The config file is ~/.config/guiltty/config.exs (or the path in GUILTTY_CONFIG). Ctrl + , creates it from the example config, which documents every setting, and opens it in your editor.

It's plain Elixir: the last expression is the config, a keyword list. Settings you leave out keep their defaults.

  • guiltty checks the file every second and applies a saved change to the open windows. A few settings apply only to what opens next; the settings table says which.
  • If the file doesn't evaluate, or a setting is wrong, the previous config stays. The tab bar shows the error in red, with the line (config.exs:4: theme: unknown name ...), and a misspelled setting gets a suggestion.

Settings

font family: any fontconfig name; size: 6 to 96 [family: "monospace", size: 18]
theme a built-in name, or your own colors. See Themes. "tomorrow-night"
cursor style: :block, :bar or :underline. Programs can change the style while they run (vim shows a bar in insert mode); it comes back when they're done. [style: :block, blink: false]
window: padding 0 to 100. Pixels around the text; more on a HiDPI screen. 6
window: columns, rows 10 to 1000, 3 to 1000. The size of a new window, in cells. 80, 24
window: opacity 0.1 to 1.0. The background's. Needs a compositor; applies to windows opened after a change. 1.0
shell [command: "/usr/bin/zsh", args: ["-l"]]. The program each new tab runs. your login shell
working_directory :inherit, :home or a path. :inherit starts a new tab where the current tab's shell is. :inherit
copy_on_select true, :clipboard or false. true copies a selection to the primary selection; :clipboard to the clipboard too; false copies nothing until Ctrl+Shift+C. true
wheel_lines 1 to 50. Rows the mouse wheel scrolls per step. 3
bold_is_bright true or false. Bold text in the 8 basic colors uses their bright versions, as xterm does. true
broadcast_programs a list of program names. What a tab must be running to join the broadcast group. ["ssh", "mosh-client"]
tab_bar :bottom or :top :bottom
tab_style :flat, :powerline, :rounded, :minimal or :separators. How the tabs look. Their colors are in the theme. :flat
ligatures true or false. Join ->, !=, |> and the like into one symbol, when the font has ligatures. true
renderer :vulkan or :gl. Applies to windows opened after a change. GUILTTY_RENDERER wins over it. :vulkan
scrollback 0 to 1,000,000. Rows each tab keeps for scrolling back; applies to new tabs. 10_000
keys action: [key, ...]. Any action you leave out keeps its default. see Keys
Setting Default Accepts
font [family: "monospace", size: 18] family: any fontconfig name; size: 6 to 96
theme "tomorrow-night" a built-in name, or your own colors See Themes.
cursor [style: :block, blink: false] style: :block, :bar or :underline Programs can change the style while they run (vim shows a bar in insert mode); it comes back when they're done.
window: padding 6 0 to 100 Pixels around the text; more on a HiDPI screen.
window: columns, rows 80, 24 10 to 1000, 3 to 1000 The size of a new window, in cells.
window: opacity 1.0 0.1 to 1.0 The background's. Needs a compositor; applies to windows opened after a change.
shell your login shell [command: "/usr/bin/zsh", args: ["-l"]] The program each new tab runs.
working_directory :inherit :inherit, :home or a path :inherit starts a new tab where the current tab's shell is.
copy_on_select true true, :clipboard or false true copies a selection to the primary selection; :clipboard to the clipboard too; false copies nothing until Ctrl+Shift+C.
wheel_lines 3 1 to 50 Rows the mouse wheel scrolls per step.
bold_is_bright true true or false Bold text in the 8 basic colors uses their bright versions, as xterm does.
broadcast_programs ["ssh", "mosh-client"] a list of program names What a tab must be running to join the broadcast group.
tab_bar :bottom :bottom or :top
tab_style :flat :flat, :powerline, :rounded, :minimal or :separators How the tabs look. Their colors are in the theme.
ligatures true true or false Join ->, !=, |> and the like into one symbol, when the font has ligatures.
renderer :vulkan :vulkan or :gl Applies to windows opened after a change. GUILTTY_RENDERER wins over it.
scrollback 10_000 0 to 1,000,000 Rows each tab keeps for scrolling back; applies to new tabs.
keys see Keys action: [key, ...] Any action you leave out keeps its default.

Themes

Pick a built-in theme by name:

  • tomorrow-night Tomorrow Night (the default)
  • catppuccin-mocha Catppuccin Mocha
  • dracula Dracula
  • gruvbox-dark Gruvbox Dark
  • solarized-dark Solarized Dark

Or bring your own colors. Any you leave out come from name: (or from tomorrow-night without one), so you can start from a theme and change single colors.

config.exs
theme: [
  name: "catppuccin-mocha",
  fg: "#c5c8c6",
  bg: "#1d1f21",
  cursor: "#c5c8c6",
  # black, red, green, yellow, blue, magenta, cyan, white, then bright
  ansi: ~w(#1d1f21 #cc6666 #b5bd68 #f0c674 #81a2be #b294bb #8abeb7 #c5c8c6
           #969896 #d54e53 #b9ca4a #e7c547 #7aa6da #c397d8 #70c0b1 #eaeaea),
  # The tab bar's colors. Any you leave out are worked out from the
  # theme, to suit the tab_style.
  tab_bar: "#282a36",
  tab_text: "#6272a4",
  tab_active: "#ff79c6",
  tab_active_text: "#282a36",
  tab_accent: "#ff79c6"
]

Tab bar colors

Color What it colors
tab_bar the bar behind the tabs
tab_text the other tabs' titles
tab_active the active tab's background
tab_active_text the active tab's title
tab_accent the line along the active tab (flat, minimal), or the filled tab's color (the other styles)

Any you leave out are worked out from the theme, to suit the tab style.


Tab styles

tab_style picks how the tab bar is drawn. The previews use Catppuccin Mocha; guiltty works out each style's colors from your theme, unless you set them.

:flat Side by side, the active one lighter, with a line along its edge. The default.

:powerline Colored blocks joined by arrows.

:rounded Pills with round ends, the active one filled.

:minimal No bar: titles on the terminal's background, the line under the active one.

:separators Titles between thin bars, the active one filled.


Fonts and ligatures

font takes a fontconfig family name and a size from 6 to 96: font: [family: "JetBrains Mono", size: 14]. Characters the font doesn't have come from fallback fonts, so emoji, Nerd Font icons and CJK text show if those fonts are installed.

With a font that has ligatures (JetBrains Mono and Fira Code do; Source Code Pro doesn't), ->, !=, |>, => and the like are drawn as one symbol. ligatures: false turns that off.

Powerline arrows and half circles in prompts (U+E0B0, U+E0B2, U+E0B4, U+E0B6) are drawn by guiltty itself, like box drawing: they meet the blocks next to them exactly, and show even without a Nerd Font.


Vulkan or OpenGL

guiltty draws with Vulkan 1.3 by default. If Vulkan can't start (an old GPU, no driver), it switches to OpenGL by itself and says so in the tab bar. Both draw the same picture: a test replays recorded frames through each and compares them pixel for pixel.

  • renderer: :gl always uses OpenGL, starting with the next window, so one window can be Vulkan and the next OpenGL.
  • GUILTTY_RENDERER=gl (or vulkan) does the same for a whole run, whatever the config says. It's read when guiltty starts: guiltty stop, then GUILTTY_RENDERER=gl guiltty.

Example config

The example config as it ships, with every setting at its default except the theme.

config.exs
[
  font: [family: "monospace", size: 18],

  # A built-in theme by name: "catppuccin-mocha", "tomorrow-night" (what
  # guiltty uses without a config), "dracula", "gruvbox-dark" or
  # "solarized-dark".
  theme: "catppuccin-mocha",

  # :block, :bar or :underline. Programs can change it while they run (vim
  # shows a bar in insert mode); it comes back when they're done.
  cursor: [style: :block, blink: false],

  window: [
    # Pixels around the text (more on a HiDPI screen).
    padding: 6,
    # The size of a new window, in cells.
    columns: 80,
    rows: 24,
    # The background's opacity, 0.1 to 1.0. Needs a compositor; applies to
    # windows opened after a change.
    opacity: 1.0
  ],

  # The program each new tab runs. Without a command: your login shell.
  # shell: [command: "/usr/bin/zsh", args: ["-l"]],

  # Where a new tab starts: :inherit (where the current tab's shell is),
  # :home, or a path.
  working_directory: :inherit,

  # Selecting text copies it to the primary selection (middle click pastes
  # it); :clipboard copies it to the clipboard too; false copies nothing
  # until Ctrl+Shift+C.
  copy_on_select: true,

  # Rows the mouse wheel scrolls per step.
  wheel_lines: 3,

  # Bold text in the 8 basic colors uses their bright versions, as xterm
  # does.
  bold_is_bright: true,

  # Broadcast (Ctrl+Shift+B): a tab may join only while it runs one of
  # these. If one of them stops (a dropped connection), typing goes nowhere
  # until that tab leaves the group. A local tab (your own shell) joins on
  # a second Ctrl+Shift+B, and isn't checked.
  broadcast_programs: ["ssh", "mosh-client"],

  # Where the tabs go: :bottom (the default) or :top.
  tab_bar: :bottom,

  # How the tabs look:
  #   :flat        (the default) side by side, the active one lighter, with
  #                a line along its edge
  #   :powerline   colored blocks joined by arrows
  #   :rounded     pills with round ends, the active one filled
  #   :minimal     no bar: titles on the terminal's background, the line
  #                under the active one
  #   :separators  titles between thin bars, the active one filled
  # Their colors are in theme: (tab_bar, tab_active, ... above).
  tab_style: :flat,

  # Join ->, !=, |>, => and the like into one symbol, when the font has
  # ligatures (JetBrains Mono and Fira Code do; Source Code Pro doesn't).
  ligatures: true,

  # How guiltty draws: :vulkan, or :gl (OpenGL). If Vulkan can't start
  # (no driver, no GPU that can show the window), guiltty says so and
  # draws with OpenGL. Applies to windows opened after a change.
  # GUILTTY_RENDERER=gl (or vulkan) in guiltty's environment wins over
  # this, to try the other one for a run: `guiltty stop`, then
  # `GUILTTY_RENDERER=gl guiltty`.
  renderer: :vulkan,

  # Rows of output each tab keeps for scrolling back (new tabs only).
  scrollback: 10_000,

  # Each action takes a list of keys. Names: letters, digits, f1-f12,
  # tab, enter, page_up, page_down, home, end, insert, delete, or a
  # punctuation character like = or -, with any of the ctrl+, shift+ and
  # alt+ modifiers.
  keys: [
    new_window: ["ctrl+shift+n"],
    new_tab: ["ctrl+shift+t"],
    close_tab: ["ctrl+shift+w"],
    next_tab: ["ctrl+page_down", "ctrl+tab"],
    prev_tab: ["ctrl+page_up", "ctrl+shift+tab"],
    move_tab_left: ["ctrl+shift+page_up"],
    move_tab_right: ["ctrl+shift+page_down"],
    rename_tab: ["ctrl+shift+r"],
    scroll_page_up: ["shift+page_up"],
    scroll_page_down: ["shift+page_down"],
    scroll_top: ["shift+home"],
    scroll_bottom: ["shift+end"],
    clear_scrollback: ["ctrl+shift+k"],
    search: ["ctrl+shift+f"],
    copy: ["ctrl+shift+c"],
    zoom_in: ["ctrl+=", "ctrl+shift+="],
    zoom_out: ["ctrl+-"],
    zoom_reset: ["ctrl+0"],
    open_config: ["ctrl+,"],
    cheatsheet: ["ctrl+shift+/"],
    lock_tab: ["ctrl+shift+l"],
    broadcast: ["ctrl+shift+b"]
  ]
]

The guiltty command

guiltty is a small program that asks the running guiltty for a window over a private socket, like emacsclient. If guiltty isn't running, it starts it.

Command What it does
guiltty Opens a window, or brings back a closed one that kept locked tabs. Its first tab starts in the folder you ran it in.
guiltty stop Stops guiltty: every window and every tab, locked ones too.
guiltty pid Prints the running guiltty's process id.
guiltty remote An IEx shell inside the running guiltty.

guiltty's Erlang node listens on 127.0.0.1 only, is named guiltty@127.0.0.1, and takes its cookie from ~/.erlang.cookie. install.sh stops if that file can be read by other users.


Environment variables

Variable What it does
GUILTTY_CONFIG The config file to use instead of ~/.config/guiltty/config.exs.
GUILTTY_RENDERER gl or vulkan, for a whole run, whatever the config says. Read when guiltty starts.
PREFIX For install.sh: where to install instead of ~/.local.

Troubleshooting

guiltty: command not found

Add ~/.local/bin to your PATH.

The tab bar says guiltty is drawing with OpenGL

Vulkan couldn't start: there's no Vulkan 1.3 driver, or no GPU that can show the window. guiltty works the same with OpenGL. To stop it trying Vulkan, set renderer: :gl.

The tab bar shows an error in red

The config file has a mistake, at the line it names. The previous config stays until you fix it and save.

A setting didn't change anything

window: opacity and renderer apply to windows opened after the change, and scrollback to new tabs. Opacity also needs a compositor.

Emoji, icons or CJK text show as boxes

Install fonts that have them: a Nerd Font, Noto Color Emoji and Noto Sans CJK.

I can't select text in vim or htop

They use the mouse; hold Shift to select.

Updating from a guiltty tab

The tab closes when the old guiltty stops; the install carries on and opens guiltty again. Its output is in ~/.local/state/guiltty/install.log.


Building from source

To work on guiltty itself, from a clone:

shell
mix deps.get\nmix guiltty        # a window from the source tree\nmix test\niex -S mix         # the same, with an iex shell alongside

mix compile also builds the C: the NIF and both renderers, each in a normal and a debug build. make bench measures throughput, and AGENTS.md lists every other command.


Learn more

  • The book explains every part, with diagrams, from pseudoterminals to tab styles.
  • CHANGELOG.md lists what each version added.
  • DEVLOG.md is what happened along the way.
  • guiltty is under the MIT License. The installed copy also carries the Erlang runtime and the telemetry library, both under the Apache License 2.0.