Next: (heks-linux zen), Previous: (heks-linux waybar), Up: API
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.
Default value:
"/root/.config/wezterm"
Default value:
"wezterm.lua"
Default value:
("Noto Color Emoji")
Default value:
12
Default value:
4000
Default value:
100000
Default value:
#f
Default value:
16
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")
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.
Join a configuration line list, such as (wezterm-config), into
one string with newlines between the lines.
(serialize-wezterm-config #:config (wezterm-config))
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' }
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']
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 }
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
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.
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.
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 }
Return #t when value was marked with
wezterm-compact.
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.
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.
Return the rendered lines of the emacs_keys array.
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.
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.
Return the rendered lines of the emacs_prefix_tables table.
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.
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
Return #t when value is a literal Lua fragment.
(wezterm-raw-lua? (wezterm-raw-lua "act.PasteFrom 'Clipboard'")) ⇒ #t
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
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.
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.
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.
"-- 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