Repository navigation
WezTerm: ship Lua module + activation OSC for app-scoped shortcuts #49
Description
Sub-issue of #40. Sibling of #39 (iTerm2).
Updated (post-#59): terminal-integration ownership now lives inside TuiCode itself (
ITerminalIntegrationinterface, seeIterm2Integrationfor the reference impl). WezTerm support should follow the same pattern — aWezTermIntegrationclass insrc/TuiCode.Workbench/TerminalIntegration/, surfaced through the existing--install-terminal-integrationCLI flag and Settings → Terminal Integration UI. No Homebrew formula changes, no per-channel install logic.
Goal
When TuiCode runs under WezTerm, the same OS-native shortcuts (Cmd+C/V/X/Z, Cmd+arrow navigation, Option+arrow word movement, etc.) work as they do under iTerm2 — and only while TuiCode is running, so a user's normal WezTerm Cmd+C (copy from terminal selection) still works in their shell.
Mechanism
WezTerm has no equivalent of iTerm2's auto-watched DynamicProfiles/ directory. Instead we use two WezTerm features in combination:
- Key tables (
config.key_tables) — define a named set of bindings that can be activated/deactivated on demand. We define atuicodetable whose bindings send the same byte sequences as the iTerm2 profile (control bytes for Ctrl+letter, CSI sequences for navigation, CSI u for chord combinations). - User-var events (
wezterm.on('user-var-changed', ...)) — TuiCode emits an OSC 1337SetUserVarsequence on startup and exit; WezTerm reacts by pushing / popping thetuicodekey table on the active pane.
This is cleaner than iTerm2's process-name matching because it's app-driven: works regardless of binary rename, wrapper scripts, or being launched through SSH (user vars travel through the pty).
Distribution — via WezTermIntegration : ITerminalIntegration
Following the #59 pattern. The integration class is the single source of truth:
IsAvailable()→$TERM_PROGRAM == "WezTerm".Install()→- Writes
~/.config/wezterm/tuicode.lua(full module embedded as a resource, idempotent overwrite — analogous to howIterm2Integrationships the JSON). - Appends a fenced
-- TuiCode begin / -- TuiCode endblock to~/.config/wezterm/wezterm.luacontainingrequire 'tuicode'.apply(config). If the block already exists, replaces it in place. Ifwezterm.luadoesn't exist, creates it.
- Writes
Uninstall()→ removes the Lua file and strips the fenced block fromwezterm.lua.GetStatus()→ reads both files, checks for the fenced block + aTuiCodeIntegrationVersionmarker comment inside it. DistinguishesNotInstalled/Installed/Stale.
This is more aggressive than the original "never edit wezterm.lua without consent" position, but the in-app Install action and --install-terminal-integration CLI flag are both explicit consent — the user clicked/typed it. The fenced block makes the edit reversible and visible, which is the actual concern config-purists have with installers.
Register the class in Program.cs alongside Iterm2Integration; the Settings UI auto-picks it up via the IEnumerable<ITerminalIntegration> injection.
Module sketch
-- TuiCodeIntegrationVersion: 1
local M = {}
function M.apply(config)
local wezterm = require 'wezterm'
config.key_tables = config.key_tables or {}
config.key_tables.tuicode = {
{ key = 'c', mods = 'CMD', action = wezterm.action.SendString '\x03' },
{ key = 'v', mods = 'CMD', action = wezterm.action.SendString '\x16' },
{ key = 'x', mods = 'CMD', action = wezterm.action.SendString '\x18' },
{ key = 'z', mods = 'CMD', action = wezterm.action.SendString '\x1a' },
{ key = 'z', mods = 'CMD|SHIFT', action = wezterm.action.SendString '\x19' },
{ key = 'a', mods = 'CMD', action = wezterm.action.SendString '\x01' },
{ key = 'LeftArrow', mods = 'CMD', action = wezterm.action.SendString '\x1b[H' },
{ key = 'RightArrow', mods = 'CMD', action = wezterm.action.SendString '\x1b[F' },
{ key = 'LeftArrow', mods = 'OPT', action = wezterm.action.SendString '\x1b[1;5D' },
{ key = 'RightArrow', mods = 'OPT', action = wezterm.action.SendString '\x1b[1;5C' },
-- ... full set mirroring the iTerm2 profile from the #41 comment
}
wezterm.on('user-var-changed', function(window, pane, name, value)
if name == 'TUICODE_ACTIVE' then
if value == '1' then
window:perform_action(
wezterm.action.ActivateKeyTable { name = 'tuicode', one_shot = false },
pane
)
else
window:perform_action(wezterm.action.PopKeyTable, pane)
end
end
end)
end
return MApp-side work (TuiCode)
Emit OSC 1337 user-var sequences from WorkbenchHost:
- On startup (after
IApplication.Init): write\x1b]1337;SetUserVar=TUICODE_ACTIVE=MQ==\x07to stdout. (MQ=== base64 "1".) - On shutdown (dispose path, matching the
TerminalFlowControllifecycle): write\x1b]1337;SetUserVar=TUICODE_ACTIVE=MA==\x07. (MA=== base64 "0".)
Should be unconditional — terminals that don't understand OSC 1337 ignore it silently. No detection needed.
Gotchas
- tmux pass-through. If a user launches TuiCode inside tmux, OSC 1337 needs
allow-passthrough onin their tmux config to reach WezTerm. Document this somewhere the user will see — likely the Settings → Terminal Integration panel when$TERM_PROGRAM == "tmux"is detected wrapped around WezTerm. - Per-pane scope. The user-var-changed event gives us the originating pane, and
ActivateKeyTableapplies per-window. Need to verify behavior when the user has multiple TuiCode instances in different tabs/panes (key table is window-scoped, so cross-pane bleed is possible). Worth a manual test. - Same byte sequences as iTerm2. Both terminals end up sending the same bytes to TuiCode, so the in-app key handling is unchanged. Any fix to the byte-sequence mapping (e.g. the Redo issue Cmd+Shift+Z (Redo) doesn't work in TextView #46) benefits both terminals.
- Editing user's
wezterm.lua. The fenced-block edit is reversible and visible, but it is an edit. The CLI flag and Settings UI are the consent points; document the block format so a user who later reads their config understands what's there.
Work breakdown
- App: emit OSC 1337 user-var on startup/exit (
WorkbenchHost). - Embed
tuicode.luaas a resource string inWezTermIntegration(analogous toIterm2Integration.ProfileJson). - Implement
WezTermIntegration : ITerminalIntegration— Install/Uninstall/GetStatus including the fenced-block edit towezterm.lua. - Register in
Program.cs. - Unit tests with
MockFileSystem+FakeEnvironment: install creates both files / appends block, idempotent reinstall, stale detection, uninstall strips the block and leaves the rest ofwezterm.luaintact. - Manual test: multi-pane behavior, tmux nesting, SSH session.
- Update the Settings → Terminal Integration panel's "Supported so far" line to include WezTerm once the integration ships.
Dependencies
- Blocked by: nothing — the in-app integration is self-contained.
- Related: Enable OS default navigation/editing shortcuts in iTerm2 #39 (iTerm2 sibling), Make native OS shortcuts (Cmd+C/V/X etc.) work across different terminal emulators #40 (umbrella), Cmd+Shift+Z (Redo) doesn't work in TextView #46 (Redo binding — fix benefits both terminals once resolved), Move terminal-integration setup from Homebrew formula into TuiCode itself #59 (the abstraction this issue plugs into).
Activity
WezTerm: ship Lua module + activation OSC for app-scoped shortcuts (#69)