WezTerm ExecDomain 配置详解:用 `wezterm.exec_domain` 包装本地命令派发
ExecDomain 是 WezTerm(当前仓库 GitHub_Trending/we/wezterm)中用于定义"本地执行型多路复用域"(local-execution multiplexer domain)的核心配置对象。它的价值在于:让 WezTerm 不直接执行用户请求的程序,而是先把该命令交给一个 Lua fixup 函数改写(wrap),再由改写后的命令去启动真正的程序。读完本文,你将掌握 wezterm.exec_domain 的完整参数语义、fixup 与 label 回调的编写方式,并能用两个完整范例(systemd scope 隔离、Docker 容器域)把这一机制落地到自己的 wezterm.lua 配置中。
ExecDomain 是什么
根据官方文档 docs/config/lua/ExecDomain.md,一个 ExecDomain 定义的是一个"本地执行的 multiplexer 域"(自版本 20220807-113146-c2fee766 起可用)。通俗地说:与其直接执行请求的程序,ExecDomain 允许你把该命令调用"传递"给其他进程来处理。
举个典型场景:如果你想更方便地在 Docker 容器里使用标签页(tab)与窗格(pane),可以定义一个 ExecDomain,让所有命令都经由 docker exec 来运行。诚然,你也可以手动让 WezTerm 显式 spawn 一条执行 docker exec 的命令,但那样你还得改动默认的"分屏键位"(如 SplitHorizontal、SplitVertical 等 key assignment),让它们知道这个偏好。而使用 ExecDomain,这个偏好会与 pane 绑定在一起,分屏、新开标签页都能"天然"地继承该偏好,交互上更直观。
从源码结构看,ExecDomain 是配置层的一等公民。在 config/src/exec_domain.rs 中,它被定义为一个 Rust 结构体:
#[derive(Debug, Clone, FromDynamic, ToDynamic)]
pub struct ExecDomain {
#[dynamic(validate = "validate_domain_name")]
pub name: String,
pub fixup_command: String,
pub label: Option<ValueOrFunc>,
}
name:域的唯一标识,配置校验时会调用validate_domain_name;fixup_command:Lua 回调(以字符串形式存储的 Lua 函数引用),用于改写命令;label:可选,可以是静态字符串(Value)或 Lua 回调函数(Func),对应ValueOrFunc枚举。
在 config/src/config.rs 中,配置结构体通过 pub exec_domains: Vec<ExecDomain> 承接用户在 Lua 中 config.exec_domains = {...} 的赋值。
定义 ExecDomain
必须使用 wezterm.exec_domain 函数来定义域。它接受如下参数:
wezterm.exec_domain(NAME, FIXUP [, LABEL])
- name:唯一标识该域。必须与其他任何 multiplexer 域(unix/ssh/wsl/tls 等)的名称都不冲突。配置加载时会统一做名称唯一性校验,见 config/src/config.rs 中
check_domain(&d.name, "exec domain")的检查逻辑; - fixup:一个 Lua 函数,被调用来 fixup(改写)请求的命令,并返回修订后的命令;
- label(可选):可以是一个字符串,作为 Launcher Menu(启动器菜单) 中显示的标签;也可以是一个返回标签的 Lua 函数。
fixup 函数
最简单的 fixup 函数长这样:
wezterm.exec_domain('myname', function(cmd)
return cmd
end)
cmd 参数是一个 SpawnCommand 对象,包含将要执行的命令信息。这个 SpawnCommand 要么是用户在 key assignment 中配置的产物,要么是在响应"新开标签页 / 分屏"请求时生成的等价物。
预期行为是:fixup 函数调整传入命令的各个字段,然后把它返回。WezTerm 将执行这个被调整后的命令,以满足用户的 spawn 请求。
从实现上看,WezTerm 在 mux/src/domain.rs 的 LocalDomain::fixup_command 中完成了"执行请求 → 构造 SpawnCommand → 调用 Lua fixup → 重组命令"的完整链路:
- 先通过
resolve_exec_domain()在config::configuration().exec_domains中按name查找匹配的域(mux/src/domain.rs); - 把
CommandBuilder中的 argv、完整环境变量、cwd 提取出来,构造一个SpawnCommand,其中domain字段被设为SpawnTabDomain::DomainName(ed.name.clone()); - 在主线程上以异步回调方式执行 Lua 的
fixup_command,并把返回结果反序列化回SpawnCommand; - 最后用新的
args、set_environment_variables、cwd重新填充CommandBuilder。
也就是说,fixup 函数拥有对 argv、环境变量、工作目录的完全控制权——这正是它能做"命令包装"的根本原因。
label
标签会显示在 Launcher Menu 中。你可以设置为静态字符串,也可以设置为 Lua 回调。默认行为等价于下面这个回调:
-- domain_name 与调用 wezterm.exec_domain() 时的第一个参数相同
wezterm.exec_domains(domain_name, fixup_func, function(domain_name)
return domain_name
end)
(注意:这是文档中表达默认语义的等价示意代码,实际配置中第三参如果省略即取此默认行为。)
使用回调函数可以让你在 Launcher Menu 渲染前的最后一刻生成修订后的标签。这很适合把标签做成某种"状态信息"——例如为 Docker 容器或虚拟机定义 ExecDomain 时,让标签反映它当前是否在运行。
静态字符串与回调生成的字符串都可以包含影响文本样式的转义序列。建议使用 wezterm.format() 来管理这些样式。
从实现上看,domain_label() 位于 mux/src/domain.rs:
- 若
label是ValueOrFunc::Value(Value::String(s)),直接返回该静态字符串; - 若
label是ValueOrFunc::Func(label_func),则在主线程异步调用该 Lua 函数,把domain_name作为参数传入,并将其返回值解释为字符串;若回调出错,会记录log::error!并回退到域名称本身; - 若没有 label,回退到
self.name。
示例一:让每条命令运行在独立的 systemd scope 中
完整示例来自 docs/config/lua/ExecDomain.md:
local wezterm = require 'wezterm'
local config = {}
-- Equivalent to POSIX basename(3)
-- Given "/foo/bar" returns "bar"
-- Given "c:\\foo\\bar" returns "bar"
local function basename(s)
return string.gsub(s, '(.*[/\\])(.*)', '%2')
end
config.exec_domains = {
-- Defines a domain called "scoped" that will run the requested
-- command inside its own individual systemd scope.
-- This defines a strong boundary for resource control and can
-- help to avoid OOMs in one pane causing other panes to be
-- killed.
wezterm.exec_domain('scoped', function(cmd)
-- The "cmd" parameter is a SpawnCommand object.
-- You can log it to see what's inside:
wezterm.log_info(cmd)
-- Synthesize a human understandable scope name that is
-- (reasonably) unique. WEZTERM_PANE is the pane id that
-- will be used for the newly spawned pane.
-- WEZTERM_UNIX_SOCKET is associated with the wezterm
-- process id.
local env = cmd.set_environment_variables
local ident = 'wezterm-pane-'
.. env.WEZTERM_PANE
.. '-on-'
.. basename(env.WEZTERM_UNIX_SOCKET)
-- Generate a new argument array that will launch a
-- program via systemd-run
local wrapped = {
'/usr/bin/systemd-run',
'--user',
'--scope',
'--description=Shell started by wezterm',
'--same-dir',
'--collect',
'--unit=' .. ident,
}
-- Append the requested command
-- Note that cmd.args may be nil; that indicates that the
-- default program should be used. Here we're using the
-- shell defined by the SHELL environment variable.
for _, arg in ipairs(cmd.args or { os.getenv 'SHELL' }) do
table.insert(wrapped, arg)
end
-- replace the requested argument array with our new one
cmd.args = wrapped
-- and return the SpawnCommand that we want to execute
return cmd
end),
}
-- Making the domain the default means that every pane/tab/window
-- spawned by wezterm will have its own scope
config.default_domain = 'scoped'
return config
这个范例的几个关键点值得展开:
- 身份合成:
WEZTERM_PANE是新 pane 的 id,WEZTERM_UNIX_SOCKET与 wezterm 进程 id 相关,两者拼出的ident使 scope 名"合理唯一"。basename从 socket 路径中取出最后一段,避免路径中的/破坏 systemd unit 名的合法性。 - 默认程序兜底:
cmd.args可能为nil,表示应使用默认程序。这里用cmd.args or { os.getenv 'SHELL' }显式把$SHELL作为兜底。 - 资源隔离语义:systemd scope 为命令建立了强资源控制边界,可避免某个 pane 触发 OOM 时殃及其他 pane。这也是把
default_domain设为'scoped'的动机——让 WezTerm 派生的每个 pane/tab/window 都拥有独立的 scope。 - 调试辅助:fixup 里调用
wezterm.log_info(cmd)可以把 SpawnCommand 的字段打印到日志中,方便你确认对象结构。
示例二:把每个运行中的 Docker 容器变成一个域
第二个完整示例(同样来自 docs/config/lua/ExecDomain.md)展示了如何动态地把每个运行中的 Docker 容器注册为域,从而直接向容器内 spawn shell、或对容器进行分屏:
local wezterm = require 'wezterm'
local config = wezterm.config_builder()
function docker_list()
local docker_list = {}
local success, stdout, stderr = wezterm.run_child_process {
'docker',
'container',
'ls',
'--format',
'{{.ID}}:{{.Names}}',
}
for _, line in ipairs(wezterm.split_by_newlines(stdout)) do
local id, name = line:match '(.-):(.+)'
if id and name then
docker_list[id] = name
end
end
return docker_list
end
function make_docker_label_func(id)
return function(name)
local success, stdout, stderr = wezterm.run_child_process {
'docker',
'inspect',
'--format',
'{{.State.Running}}',
id,
}
local running = stdout == 'true\n'
local color = running and 'Green' or 'Red'
return wezterm.format {
{ Foreground = { AnsiColor = color } },
{ Text = 'docker container named ' .. name },
}
end
end
function make_docker_fixup_func(id)
return function(cmd)
cmd.args = cmd.args or { '/bin/sh' }
local wrapped = {
'docker',
'exec',
'-it',
id,
}
for _, arg in ipairs(cmd.args) do
table.insert(wrapped, arg)
end
cmd.args = wrapped
return cmd
end
end
function compute_exec_domains()
local exec_domains = {}
for id, name in pairs(docker_list()) do
table.insert(
exec_domains,
wezterm.exec_domain(
'docker:' .. name,
make_docker_fixup_func(id),
make_docker_label_func(id)
)
)
end
return exec_domains
end
config.exec_domains = compute_exec_domains()
return config
使用以上配置后,每次配置被重新加载(reload),可用域列表都会随之更新。打开 Launcher Menu 就能看到各个容器及其运行状态,并可直接在这些容器内启动程序。
该示例值得关注的工程细节:
- 动态域 + 即时标签:
make_docker_label_func(id)返回的闭包每次渲染菜单时都会执行一次docker inspect,把{{.State.Running}}映射为Green/Red前景色,并通过 wezterm.format() 生成带样式的文本——这正是"用回调生成 label 以反映状态"的最佳实践。颜色常量对应AnsiColor,其语义与color模块一致。 - 闭包捕获 id:
make_docker_fixup_func(id)与make_docker_label_func(id)均以闭包捕获容器的真实 id,而域 name 使用可读性更好的docker:<容器名>,二者解耦。 wezterm.config_builder():返回一个可写配置对象,最终return config生效;config.exec_domains支持表(数组)形式的多个域。
ExecDomain 的底层实现原理
从 mux/src/domain.rs 的源码可以确认以下实现事实:
- ExecDomain 基于 LocalDomain:
LocalDomain::new_exec_domain(exec_domain)(mux/src/domain.rs)只是用域名称创建一个本地域,执行仍然走本机 pty 通道;resolve_exec_domain()按名称反查配置中的exec_domains,未命中时返回None。 - 命令改写发生在 spawn 之前:
fixup_command(mux/src/domain.rs)依次处理 WSL 域(wsl.exe包装)、ExecDomain(Lua fixup 改写)与 Flatpak 沙箱(flatpak-spawn --host),三者按优先级串联。对 ExecDomain 而言,整个 Lua 回调通过config::lua::emit_async_callback在配置主线程上以异步方式执行,返回值必须能被解释为SpawnCommand,否则报错并带上下文信息calling ExecDomain {name} function。 - label 渲染同样经过 Lua 回调:
domain_label()(mux/src/domain.rs)把域名称作为参数传给 label 函数;回调抛错时不会中断渲染,而是记录错误并回退到域名。 - 名称全局唯一性校验:配置校验阶段会对
unix_domains、ssh_domains、exec_domains、wsl_domains、tls_clients逐一执行check_domain(config/src/config.rs),因此 exec domain 的名称不能与任何其他类型的域重名。
结合 SpawnCommand 理解 fixup 能改什么
fixup 收到的 cmd 是 SpawnCommand 对象,其字段在文档与实现中保持一致,均可省略:
wezterm.action.SpawnCommandInNewWindow {
-- 可选标签;仅当该 SpawnCommand 出现在 launch_menu 中时才使用
label = 'List all the files!',
-- 命令及其参数的数组;省略时使用目标域的默认程序
args = { 'ls', '-al' },
-- 命令的工作目录;省略时 wezterm 根据触发时刻的活动 pane 推断,
-- 若活动 pane 的域与本次 SpawnCommand 指定的域一致,则沿用其 cwd,
-- 无法推断时通常回退到当前用户主目录
cwd = '/some/path',
-- 为本命令追加设置的环境变量
set_environment_variables = {
SOMETHING = 'a value',
},
-- 使用当前活动 pane 的 multiplexer 域(默认行为)
domain = 'CurrentPaneDomain',
-- 或使用默认域(通常为 "local",除非用 wezterm connect/serial 启动)
-- domain = 'DefaultDomain',
-- 或显式指定命名域
-- domain = { DomainName = 'my.server' },
}
对照 mux/src/domain.rs 的实现,fixup 中真正会被采纳的字段是:args(重写 argv)、set_environment_variables(整体替换环境变量)、cwd(替换工作目录);其余字段主要用于传递上下文。这也解释了为何两个示例都集中在"重写 cmd.args"上——argv 是命令包装的核心载体。
总结与适用范围
ExecDomain 最适合以下场景:
- 命令包装:如通过
docker exec、systemd-run、ssh、sudo、flatpak-spawn等包装器执行目标程序; - 默认域注入:把
config.default_domain设为某个 ExecDomain 名称,使所有 pane/tab/window 都套用同一种包装策略; - 动态域名:如示例二那样,在配置加载时扫描外部资源(容器、VM、连接)并批量生成域,配合
label回调呈现实时状态; - 键位联动:由于偏好与 pane 关联,默认的分屏/新标签键位会自动把命令交给正确的域,无需逐个改 key assignment。
需要留意的是:ExecDomain 是"本地执行"型域,与远程多路复用域(SSH/TLS/Unix domain)定位不同;它改写的是命令本身,而不是建立跨主机的会话连接。相关对比可参考 docs/config/lua/SshDomain.md 与 docs/config/lua/WslDomain.md(WSL 域在 mux/src/domain.rs 中同样走 fixup_command 链路,用 wsl.exe --distribution ... --exec 包装命令,是理解"本地包装"思路的另一个实例)。
配置好 config.exec_domains 后,重启或 reload WezTerm,即可在 Launcher Menu(Ctrl+Shift+Space 等默认键位打开)中看到每个域的标签,并在其中启动程序。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java50
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280