Next: , Previous: , Up: API  


1.25 (heks-linux wezterm)

Renders ~/.config/wezterm/wezterm.lua.

WezTerm is configured in Lua, so this module generates a Lua file: the settings, the application list and the key bindings are Scheme data rendered by serialize-wezterm-value and serialize-wezterm-key, wrapped by a fixed block of Lua (wezterm-lua-runtime) that turns them into a configuration. Keeping the mechanism in one visible place is the point of the split: the parts a user changes are data, and the part that implements the policy never has to be re-read.

The key bindings are the Emacs ones. A terminal can only choose *not* to claim a chord if it cannot make a binding conditional on what is running: once it maps C-y, the pager and Emacs never see it again. WezTerm hands every action a callback, and the callback can ask the pane which process is in front of it, so every chord below is *guarded*: when the foreground process is one of wezterm-yield-apps - Emacs above all, but also less and every full-screen program - the chord is re-sent to that application untouched and the terminal stays out of the way. C-x is the clearest example: at a shell prompt it is the prefix of the window commands, and inside Emacs it is Emacs’s own C-x.

Because every chord is guarded, WezTerm can claim the Emacs page keys. C-v and M-v page the scrollback forward and back, and inside Emacs or the pager they are handed over untouched - so the shared lesskey file still gets its forw-screen and back-screen, and Emacs still gets its scroll-up-command. The same rule is applied to the few defaults WezTerm ships on a bare modifier (CTRL+Tab, ALT+Enter, CTRL+PageUp, CTRL+PageDown, CTRL+Insert): each is re-registered through the guard, so a full-screen program receives it instead of the terminal.

The guard is not free: pane:get_foreground_process_name only answers for local panes (not for a multiplexer or a plain ssh session), and when it does not answer the terminal keeps its own binding. That is the right failure mode - a chord that scrolls where it should have scrolled is better than a chord silently dropped - and it is why the scrollback also stays on S-PageUp and S-PageDown, so a full-screen program keeps its own paging keys too.

The window asks for a title bar and a resize border: window_decorations is TITLE | RESIZE on every platform. That is also WindowDecorations’s default value, and WezTerm’s Wayland backend answers the default with "follow the server" rather than "draw it yourself"; Niri offers no server-side decorations, so nothing is drawn at all. Any non-default value takes the client-side path instead, where this build paints a 33 pixel band of flat dark with no title and no buttons in it - the strip that made the window look broken.

Niri draws no title bars at all, so the visible chrome is WezTerm’s own tab bar, and it is kept on screen even for a single tab for exactly that reason: it carries the pane title (the shell’s, by way of OSC 0) and the per-tab close button, and the tab bar can be dragged to move the window. WezTerm’s integrated title buttons are not an option - this build ignores INTEGRATED_BUTTONS and integrated_title_buttons entirely, so asking for them only adds the blank band above the bar.

Blur is left to the compositor. Niri already blurs the WezTerm window through niri-blur-rules, so only the opacity is set here.

The prefix table maps Emacs’s buffer and window vocabulary onto tabs and panes: C-x t opens a tab, C-x 0 closes it, C-x o moves to the next tab, C-x b picks a tab by name, and C-x <left> / C-x <right> walk the tabs the way they walk buffers in Emacs. The window commands that have no tab equivalent keep their pane meaning: C-x 2 splits below, C-x 3 splits right and C-x 1 zooms.

1.25.1 Parameters

Parameter: wezterm-config-dir

Default value:

"/root/.config/wezterm"
Parameter: wezterm-config-filename

Default value:

"wezterm.lua"
Parameter: wezterm-font-fallbacks

Default value:

("Noto Color Emoji")
Parameter: wezterm-font-size

Default value:

12
Parameter: wezterm-prefix-timeout

Default value:

4000
Parameter: wezterm-scrollback-lines

Default value:

100000
Parameter: wezterm-window-opacity

Default value:

#f
Parameter: wezterm-window-padding

Default value:

16
Parameter: wezterm-yield-apps

Default value:

("alsamixer" "atop" "bat" "btop" "byobu" "cava" "cmus" "emacs" "emacsclient" "fzf" "ghci" "gitui" "guile" "helix" "htop" "hx" "info" "ipython" "irb" "irssi" "joe" "jshell" "kak" "lazygit" "less" "lf" "lua" "man" "mc" "mcedit" "micro" "mongosh" "more" "most" "mosh" "mutt" "nano" "ncspot" "neomutt" "newsboat" "nnn" "nvim" "nvtop" "pg" "pico" "psql" "python" "python3" "ranger" "redis-cli" "rmpc" "sbcl" "screen" "sqlite3" "ssh" "telnet" "tig" "tmux" "top" "tuir" "vi" "view" "vim" "weechat" "yazi")

1.25.2 Procedures

Procedure: install-wezterm-config

Write the WezTerm configuration to wezterm-config-dir.

Local overrides are read from $XDG_CONFIG_HOME/heks-linux/wezterm-settings.scm first, so a machine can change the font, the opacity or the list of applications that get the keyboard without editing this module. WezTerm reloads its configuration when the file changes, so no restart is needed.

Procedure: serialize-wezterm-config KEY: #:config

Join a configuration line list, such as (wezterm-config), into one string with newlines between the lines.

(serialize-wezterm-config #:config (wezterm-config))
Procedure: serialize-wezterm-key KEY: #:key #:mods #:action

Render one guarded key assignment as an inline Lua table.

mods uses the short syntax translated by translate-wezterm-mods, key is a WezTerm key name and action is the Lua expression the chord performs when no application owns the keyboard.

(serialize-wezterm-key #:key "y" #:mods "C" #:action "act.PasteFrom 'Clipboard'")
⇒ { key = 'y', mods = 'CTRL', action = act.PasteFrom 'Clipboard' }
Procedure: serialize-wezterm-name name

Render name as a Lua table field name.

Names that are not a valid Lua identifier - emacs-cx, for instance - are bracketed and quoted, the way Lua requires.

(serialize-wezterm-name "emacs-cx")
⇒ ['emacs-cx']
Procedure: serialize-wezterm-prefix-entry KEY: #:key #:action

Render one entry of a prefix key table as an inline Lua table.

Unlike serialize-wezterm-key, an entry carries no modifier of its own: the runtime expands it into the two spellings a prefix needs - with and without CTRL still held down - so that both C-x 2 and C-x then 2 work.

(serialize-wezterm-prefix-entry #:key "2" #:action "act.SplitVertical")
⇒ { key = '2', action = act.SplitVertical }
Procedure: serialize-wezterm-value value KEY: #:level #:compact?

Render value as a Lua expression.

Strings are single-quoted - Lua’s spelling, and it keeps the generated file free of escapes - numbers and booleans pass through, a list of pairs becomes a Lua object and any other list becomes a Lua array. Tables are laid out one entry per line unless the value was wrapped in wezterm-compact, and literal Lua fragments from wezterm-raw-lua are emitted verbatim.

(serialize-wezterm-value #t)
⇒ true
Procedure: translate-wezterm-mods mods

Translate the project’s short modifier syntax into WezTerm’s spelling.

WezTerm separates modifiers with a vertical bar and spells them out in capitals. Unknown parts are passed through unchanged, so a literal key name survives.

For example, C-S becomes CTRL|SHIFT.

Procedure: wezterm-colors

Return the Lua colors table as an association list.

The foreground and background come from the palette’s text and background values, the cursor borrows the palette’s primary so it matches the accent used by the rest of the desktop, and selections are drawn in the palette’s background-l.

Procedure: wezterm-compact value

Return value marked to be rendered as a single-line Lua table, for values short enough that spreading them over several lines would only add noise.

(serialize-wezterm-value (wezterm-compact '((left . 16) (top . 16))))
⇒ { left = 16, top = 16 }
Procedure: wezterm-compact? value

Return #t when value was marked with wezterm-compact.

Procedure: wezterm-config

Return the complete WezTerm configuration as a list of text lines.

The file is assembled in four parts, in the order WezTerm needs them: the generated application set, the generated settings, the generated key bindings, and finally wezterm-lua-runtime, which consumes all three. Passing the result to string-join with a newline produces the file that is written to disk; serialize-wezterm-config does exactly that.

Procedure: wezterm-default-keys

Return the guarded re-registrations of WezTerm’s own bare-modifier defaults.

WezTerm ships a handful of bindings on a bare Ctrl or Alt, and a binding the terminal owns never reaches the application. Those that a full-screen program may want are re-registered here through the same guard as the Emacs chords, so the default still happens at a shell prompt and the program gets the key while it owns the keyboard:

- C-Tab / C-S-Tab and C-PageUp / C-PageDown switch tabs. - M-Enter toggles full screen. - C-Insert copies the selection to the primary selection.

Everything else WezTerm binds by default is either on a CTRL|SHIFT or SUPER chord - which no Emacs user presses as a bare chord, because shifted keys arrive at the application as their shifted character (CTRL+C in the table is C-S-c, not C-c) - or lives in the copy-mode and pane tables, which only exist while the terminal is already handling the keyboard.

Procedure: wezterm-emacs-key-lines

Return the rendered lines of the emacs_keys array.

Procedure: wezterm-emacs-keys

Return the rendered guarded key assignments, one inline Lua table each.

Every chord here is one an Emacs user reaches for, so every one of them is guarded: the runtime hands it back to the application whenever wezterm-yield-apps owns the keyboard.

The page keys are the point of the guard: C-v pages the scrollback forward and M-v pages it back, exactly as they move through a buffer in Emacs, and inside Emacs or the pager the same two chords are handed over untouched. One chord is missing on purpose: C-c. It is Emacs’s leader key, but in a terminal it is also the interrupt signal, and no terminal may take it.

Renaming tabs is left to WezTerm’s own defaults in the Ctrl+Shift namespace, which neither Emacs nor a pager wants, but the Emacs prefix owns the tab commands: C-x t opens a tab, C-x o moves to the next one, C-x 0 closes it and S-<arrow> cycles tabs from the top level.

Procedure: wezterm-font

Return the Lua expression for the font setting.

The family comes from heks-linux-mono-font so the terminal stays in step with Emacs and the rest of the environment, and wezterm-font-fallbacks supplies the families WezTerm falls back to for glyphs the main font lacks.

Procedure: wezterm-prefix-table-lines

Return the rendered lines of the emacs_prefix_tables table.

Procedure: wezterm-prefix-tables

Return the prefix key tables, as a list of (name . entries) pairs.

emacs-cx is Emacs’s C-x prefix, mapped onto tabs and panes. C-x t opens a tab, C-x 0 closes the current tab, C-x o moves to the next tab the way other-window moves to the next window, C-x b picks a tab the way C-x b picks a buffer, and the arrow keys walk the tabs the way they walk buffers. The window commands that have no tab equivalent keep their pane meaning: C-x 2 splits below, C-x 3 splits right and C-x 1 zooms. g quits, as C-g does in Emacs.

The table is only reachable through a guarded C-x, so Emacs and the pager never lose their prefix.

Procedure: wezterm-raw-lua text

Return text wrapped so that serialize-wezterm-value emits it as literal Lua instead of quoting it.

(serialize-wezterm-value (wezterm-raw-lua "act.TogglePaneZoomState"))
⇒ act.TogglePaneZoomState
Procedure: wezterm-raw-lua? value

Return #t when value is a literal Lua fragment.

(wezterm-raw-lua? (wezterm-raw-lua "act.PasteFrom 'Clipboard'"))
⇒ #t
Procedure: wezterm-settings

Return the settings WezTerm receives, as an association list.

The renderer turns this into the settings table of the generated file, which is then copied onto the configuration one field at a time. Every value here is either a parameter or derived from the palette and the font settings, so changing a setting is enough to change the file.

(assoc-ref (wezterm-settings) "scrollback_lines")
⇒ 100000
Procedure: wezterm-window-decorations

Return WezTerm’s window_decorations value.

The window asks for a title bar and a resize border, on every platform. Who draws them is the platform’s business: an X11 window manager draws the frame and its buttons, macOS draws the native title bar and the traffic lights, and on Wayland the answer comes from the compositor.

Wayland is the case worth knowing about, because TITLE | RESIZE is also WindowDecorations’s default value and this backend answers the default with FollowServer rather than ClientSide. Niri offers no server-side decorations (the xdg-decoration handshake reports client and no decoration object is created), so there is nothing to follow and nothing is drawn: no title strip, no buttons, and resizing stays with Niri’s own keybindings. The title bar the desktop does show is WezTerm’s tab bar, which hide_tab_bar_if_only_one_tab keeps visible for that purpose.

That is preferable to the alternative. Any value that is not the default - RESIZE, TITLE alone, INTEGRATED_BUTTONS mixed in - takes the client-side path, where this build paints a 33 pixel band of flat dark across the top of the window and puts no title and no buttons in it. Asking for the default pair keeps that band off the screen while still telling a compositor that does draw decorations, and macOS, what the window wants.

Procedure: wezterm-window-padding-table

Return the window_padding table, one entry per side.

WezTerm wants the four sides spelled out; wezterm-window-padding holds the single number they all take.

Procedure: wezterm-yield-app-lines

Return the rendered lines of the yield_apps array.

The names stay bare here; the runtime turns each one into a pattern that also accepts a version suffix, so that python3.13 matches python.

1.25.3 Variables

Variable: wezterm-lua-runtime
"-- Strip the directory part of a path, so that '/usr/bin/less' and 'less'\n-- compare equal.\nlocal function basename(path)\n  return (path:gsub('^.*/', ''))\nend\n\n-- One anchored pattern per application.  The suffix allowance is what makes a\n-- bare name enough: the foreground process of a Python REPL is 'python3.13' and\n-- Emacs from Guix may be 'emacs-30'.  Anchoring keeps 'vi' from matching 'vim'.\nlocal yield_patterns = @{@}\nfor _, app in ipairs(yield_apps) do\n  yield_patterns[#yield_patterns + 1] = '^' .. app .. '[%-_.%d]*$'\nend\n\n-- True when the process in front of this pane is one of the applications that\n-- must receive the keys itself.  When the name cannot be determined the\n-- terminal keeps its own bindings rather than swallowing a chord.\nlocal function application_owns_keyboard(pane)\n  if not pane then\n    return false\n  end\n  local ok, name = pcall(function()\n    return pane:get_foreground_process_name()\n  end)\n  if not ok or type(name) ~= 'string' or name == '' then\n    return false\n  end\n  local base = basename(name):lower()\n  for _, pattern in ipairs(yield_patterns) do\n    if base:match(pattern) then\n      return true\n    end\n  end\n  return false\nend\n\n-- Re-send a chord to the pane, encoded the way the application expects it.\nlocal function send_key(spec)\n  local assignment = @{ key = spec.key @}\n  if spec.mods and spec.mods ~= 'NONE' then\n    assignment.mods = spec.mods\n  end\n  return act.SendKey(assignment)\nend\n\n-- Wrap one key assignment so that the application wins when it owns the\n-- keyboard.\nlocal function guarded(spec)\n  return @{\n    key = spec.key,\n    mods = spec.mods,\n    action = wezterm.action_callback(function(window, pane)\n      if application_owns_keyboard(pane) then\n        window:perform_action(send_key(spec), pane)\n      else\n        window:perform_action(spec.action, pane)\n      end\n    end),\n  @}\nend\n\n-- A prefix table has to match the chord whether the modifier is still held\n-- down (C-x 2, as in Emacs) or released in between (C-x, then 2).\nlocal function prefix_table(entries)\n  local assignments = @{@}\n  for _, entry in ipairs(entries) do\n    for _, mods in ipairs @{ 'NONE', 'CTRL' @} do\n      assignments[#assignments + 1] = @{\n        key = entry.key,\n        mods = mods,\n        action = entry.action,\n      @}\n    end\n  end\n  return assignments\nend\n\n-- ===== wiring ==========================================================\n\nfor name, value in pairs(settings) do\n  config[name] = value\nend\n\n-- Build the lists aside and assign them whole: reading an unset field of the\n-- config builder yields a fresh default table each time, so inserting straight\n-- into config.keys would be lost.\nlocal keys = @{@}\nfor _, spec in ipairs(emacs_keys) do\n  keys[#keys + 1] = guarded(spec)\nend\nconfig.keys = keys\n\nlocal key_tables = @{@}\nfor name, entries in pairs(emacs_prefix_tables) do\n  key_tables[name] = prefix_table(entries)\nend\nconfig.key_tables = key_tables"

Next: (heks-linux zen), Previous: (heks-linux waybar), Up: API