1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +03:00

new: exec_domains

An ExecDomain is a variation on WslDomain with the key difference
being that you can control how to map the command that would be
executed.

The idea is that the user can define eg: a domain for a docker
container, or a domain that chooses to run every command in its
own cgroup.

The example below shows a really crappy implementation as a
demonstration:

```
local wezterm = require 'wezterm'

return {
  exec_domains = {
    -- Commands executed in the woot domain have "WOOT" echoed
    -- first and are then run via bash.
    -- `cmd` is a SpawnCommand
    wezterm.exec_domain("woot", function(cmd)
      if cmd.args then
        cmd.args = {
          "bash",
          "-c",
          "echo WOOT && " .. wezterm.shell_join_args(cmd.args)
        }
      end
      -- you must return the SpawnCommand that will be run
      return cmd
    end),
  },
  default_domain = "woot",
}
```

This commit unfortunately does more than should go into a single
commit, but I'm a bit too lazy to wrangle splitting it up.

* Reverts the nil/null stuff from #2177 and makes the
  `ExtendSelectionToMouseCursor` parameter mandatory to dodge
  a whole load of urgh around nil in table values. That is
  necessary because SpawnCommand uses optional fields and the
  userdata proxy was making that a PITA.
* Adds some shell quoting helper functions
* Adds ExecDomain itself, which is really just a way to
  to run a callback to fixup the command that will be run.
  That command is converted to a SpawnCommand for the callback
  to process in lua and return an adjusted version of it,
  then converted back to a command builder for execution.

refs: https://github.com/wez/wezterm/issues/1776
This commit is contained in:
Wez Furlong 2022-07-07 16:38:14 -07:00
parent 10b558fae2
commit d78cc6edb8
15 changed files with 198 additions and 59 deletions

View File

@ -4,6 +4,7 @@ use crate::color::{
ColorSchemeFile, HsbTransform, Palette, SrgbaTuple, TabBarStyle, WindowFrameConfig,
};
use crate::daemon::DaemonOptions;
use crate::exec_domain::ExecDomain;
use crate::font::{
AllowSquareGlyphOverflow, FontLocatorSelection, FontRasterizerSelection, FontShaperSelection,
FreeTypeLoadFlags, FreeTypeLoadTarget, StyleRule, TextStyle,
@ -239,6 +240,9 @@ pub struct Config {
#[dynamic(default = "WslDomain::default_domains")]
pub wsl_domains: Vec<WslDomain>,
#[dynamic(default)]
pub exec_domains: Vec<ExecDomain>,
/// The set of unix domains
#[dynamic(default = "UnixDomain::default_unix_domains")]
pub unix_domains: Vec<UnixDomain>,

View File

@ -0,0 +1,9 @@
use luahelper::impl_lua_conversion_dynamic;
use wezterm_dynamic::{FromDynamic, ToDynamic};
#[derive(Default, Debug, Clone, FromDynamic, ToDynamic)]
pub struct ExecDomain {
pub name: String,
pub event_name: String,
}
impl_lua_conversion_dynamic!(ExecDomain);

View File

@ -190,6 +190,7 @@ pub struct SpawnCommand {
#[dynamic(default)]
pub domain: SpawnTabDomain,
}
impl_lua_conversion_dynamic!(SpawnCommand);
impl std::fmt::Debug for SpawnCommand {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
@ -352,7 +353,7 @@ pub enum KeyAssignment {
ActivateCopyMode,
SelectTextAtMouseCursor(SelectionMode),
ExtendSelectionToMouseCursor(Option<SelectionMode>),
ExtendSelectionToMouseCursor(SelectionMode),
OpenLinkAtMouseCursor,
ClearSelection,
CompleteSelection(ClipboardCopyDestination),

View File

@ -24,6 +24,7 @@ mod bell;
mod color;
mod config;
mod daemon;
mod exec_domain;
mod font;
mod frontend;
pub mod keyassignment;
@ -42,6 +43,7 @@ pub use background::*;
pub use bell::*;
pub use color::*;
pub use daemon::*;
pub use exec_domain::*;
pub use font::*;
pub use frontend::*;
pub use keys::*;

View File

@ -1,3 +1,4 @@
use crate::exec_domain::ExecDomain;
use crate::keyassignment::KeyAssignment;
use crate::{
FontAttributes, FontStretch, FontStyle, FontWeight, FreeTypeLoadTarget, Gradient, RgbaColor,
@ -195,6 +196,7 @@ end
lua.set_named_registry_value(LUA_REGISTRY_USER_CALLBACK_COUNT, 0)?;
wezterm_mod.set("action_callback", lua.create_function(action_callback)?)?;
wezterm_mod.set("exec_domain", lua.create_function(exec_domain)?)?;
wezterm_mod.set("utf16_to_utf8", lua.create_function(utf16_to_utf8)?)?;
wezterm_mod.set("split_by_newlines", lua.create_function(split_by_newlines)?)?;
@ -204,6 +206,9 @@ end
wezterm_mod.set("strftime", lua.create_function(strftime)?)?;
wezterm_mod.set("strftime_utc", lua.create_function(strftime_utc)?)?;
wezterm_mod.set("gradient_colors", lua.create_function(gradient_colors)?)?;
wezterm_mod.set("shell_join_args", lua.create_function(shell_join_args)?)?;
wezterm_mod.set("shell_quote_arg", lua.create_function(shell_quote_arg)?)?;
wezterm_mod.set("shell_split", lua.create_function(shell_split)?)?;
package.set("path", path_array.join(";"))?;
}
@ -215,6 +220,20 @@ end
Ok(lua)
}
fn shell_split<'lua>(_: &'lua Lua, line: String) -> mlua::Result<Vec<String>> {
shlex::split(&line).ok_or_else(|| {
mlua::Error::external(format!("cannot tokenize `{line}` using posix shell rules"))
})
}
fn shell_join_args<'lua>(_: &'lua Lua, args: Vec<String>) -> mlua::Result<String> {
Ok(shlex::join(args.iter().map(|arg| arg.as_ref())))
}
fn shell_quote_arg<'lua>(_: &'lua Lua, arg: String) -> mlua::Result<String> {
Ok(shlex::quote(&arg).into_owned().to_string())
}
fn strftime_utc<'lua>(_: &'lua Lua, format: String) -> mlua::Result<String> {
use chrono::prelude::*;
let local: DateTime<Utc> = Utc::now();
@ -457,7 +476,16 @@ fn action_callback<'lua>(lua: &'lua Lua, callback: mlua::Function) -> mlua::Resu
let user_event_id = format!("user-defined-{}", callback_count);
lua.set_named_registry_value(LUA_REGISTRY_USER_CALLBACK_COUNT, callback_count + 1)?;
register_event(lua, (user_event_id.clone(), callback))?;
return Ok(KeyAssignment::EmitEvent(user_event_id));
Ok(KeyAssignment::EmitEvent(user_event_id))
}
fn exec_domain<'lua>(
lua: &'lua Lua,
(name, callback): (String, mlua::Function),
) -> mlua::Result<ExecDomain> {
let event_name = format!("exec-domain-{name}");
register_event(lua, (event_name.clone(), callback))?;
Ok(ExecDomain { name, event_name })
}
fn split_by_newlines<'lua>(_: &'lua Lua, text: String) -> mlua::Result<Vec<String>> {

View File

@ -22,22 +22,3 @@ return {
}
```
It is also possible to leave the mode unspecified like this:
```lua
local wezterm = require "wezterm"
return {
mouse_bindings = {
{
event={Up={streak=1, button="Left"}},
mods="SHIFT",
action=wezterm.action.ExtendSelectionToMouseCursor(nil),
},
}
}
```
when unspecified, wezterm will use a default mode which at the time
of writing is `Cell`, but in a future release may be context sensitive
based on recent actions.

View File

@ -0,0 +1,15 @@
# wezterm.shell_join_args({"foo", "bar"})
*Since: nightly builds only*
`wezterm.shell_join_args` joins together its array arguments by applying posix
style shell quoting on each argument and then adding a space.
```
> wezterm.shell_join_args{"foo", "bar"}
"foo bar"
> wezterm.shell_join_args{"hello there", "you"}
"\"hello there\" you"
```
This is useful to safely construct command lines that you wish to pass to the shell.

View File

@ -0,0 +1,10 @@
# wezterm.shell_quote_arg(string)
*Since: nightly builds only*
Quotes its single argument using posix shell quoting rules.
```
> wezterm.shell_quote_arg("hello there")
"\"hello there\""
```

View File

@ -0,0 +1,21 @@
# wezterm.shell_split(line)
*Since: nightly builds only*
Splits a command line into an argument array according to posix shell rules.
```
> wezterm.shell_split("ls -a")
[
"ls",
"-a",
]
```
```
> wezterm.shell_split("echo 'hello there'")
[
"echo",
"hello there",
]
```

View File

@ -58,29 +58,8 @@ macro_rules! impl_lua_conversion_dynamic {
pub fn dynamic_to_lua_value<'lua>(
lua: &'lua mlua::Lua,
value: DynValue,
) -> mlua::Result<mlua::Value> {
dynamic_to_lua_value_impl(lua, value, false)
}
pub fn dynamic_to_lua_table_value<'lua>(
lua: &'lua mlua::Lua,
value: DynValue,
) -> mlua::Result<mlua::Value> {
dynamic_to_lua_value_impl(lua, value, true)
}
fn dynamic_to_lua_value_impl<'lua>(
lua: &'lua mlua::Lua,
value: DynValue,
is_table_value: bool,
) -> mlua::Result<mlua::Value> {
Ok(match value {
// Use a special userdata as a proxy for Null, because if we are a value
// and we use Nil then the key is implicitly deleted and that changes
// the representation of the data in unexpected ways
DynValue::Null if is_table_value => {
LuaValue::LightUserData(mlua::LightUserData(std::ptr::null_mut()))
}
DynValue::Null => LuaValue::Nil,
DynValue::Bool(b) => LuaValue::Boolean(b),
DynValue::String(s) => s.to_lua(lua)?,
@ -99,7 +78,7 @@ fn dynamic_to_lua_value_impl<'lua>(
for (key, value) in object.into_iter() {
table.set(
dynamic_to_lua_value(lua, key)?,
dynamic_to_lua_table_value(lua, value)?,
dynamic_to_lua_value(lua, value)?,
)?;
}
LuaValue::Table(table)

View File

@ -10,12 +10,15 @@ use crate::pane::{alloc_pane_id, Pane, PaneId};
use crate::tab::{SplitRequest, Tab, TabId};
use crate::window::WindowId;
use crate::Mux;
use anyhow::{bail, Error};
use anyhow::{bail, Context, Error};
use async_trait::async_trait;
use config::{configuration, WslDomain};
use config::keyassignment::{SpawnCommand, SpawnTabDomain};
use config::{configuration, ExecDomain, WslDomain};
use downcast_rs::{impl_downcast, Downcast};
use portable_pty::{native_pty_system, CommandBuilder, PtySystem};
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
use std::rc::Rc;
use wezterm_term::TerminalSize;
@ -172,6 +175,7 @@ pub struct LocalDomain {
id: DomainId,
name: String,
wsl: Option<WslDomain>,
exec_domain: Option<ExecDomain>,
}
impl LocalDomain {
@ -186,6 +190,7 @@ impl LocalDomain {
id,
name: name.to_string(),
wsl: None,
exec_domain: None,
}
}
@ -195,6 +200,12 @@ impl LocalDomain {
Ok(dom)
}
pub fn new_exec_domain(exec_domain: ExecDomain) -> anyhow::Result<Self> {
let mut dom = Self::new(&exec_domain.name)?;
dom.exec_domain.replace(exec_domain);
Ok(dom)
}
#[cfg(unix)]
fn is_conpty(&self) -> bool {
false
@ -207,7 +218,7 @@ impl LocalDomain {
.is_some()
}
fn fixup_command(&self, cmd: &mut CommandBuilder) {
fn fixup_command(&self, cmd: &mut CommandBuilder) -> anyhow::Result<()> {
if let Some(wsl) = &self.wsl {
let mut args: Vec<OsString> = cmd.get_argv().clone();
@ -250,6 +261,64 @@ impl LocalDomain {
cmd.clear_cwd();
*cmd.get_argv_mut() = argv;
} else if let Some(ed) = &self.exec_domain {
let mut args = vec![];
let mut set_environment_variables = HashMap::new();
for arg in cmd.get_argv() {
args.push(
arg.to_str()
.ok_or_else(|| anyhow::anyhow!("command argument is not utf8"))?
.to_string(),
);
}
for (k, v) in cmd.iter_full_env_as_str() {
set_environment_variables.insert(k.to_string(), v.to_string());
}
let cwd = match cmd.get_cwd() {
Some(cwd) => Some(PathBuf::from(cwd)),
None => None,
};
let spawn_command = SpawnCommand {
label: None,
domain: SpawnTabDomain::DomainName(ed.name.clone()),
args: if args.is_empty() { None } else { Some(args) },
set_environment_variables,
cwd,
};
let spawn_command = config::run_immediate_with_lua_config(|lua| {
let lua = lua.ok_or_else(|| anyhow::anyhow!("missing lua context"))?;
let value = config::lua::emit_sync_callback(
&*lua,
(ed.event_name.clone(), (spawn_command.clone())),
)?;
let cmd: SpawnCommand =
luahelper::from_lua_value_dynamic(value).with_context(|| {
format!(
"interpreting SpawnCommand result from ExecDomain {}",
ed.name
)
})?;
Ok(cmd)
})
.with_context(|| format!("calling ExecDomain {} function", ed.name))?;
// Reinterpret the SpawnCommand into the builder
cmd.get_argv_mut().clear();
if let Some(args) = &spawn_command.args {
for arg in args {
cmd.get_argv_mut().push(arg.into());
}
}
cmd.env_clear();
for (k, v) in &spawn_command.set_environment_variables {
cmd.env(k, v);
}
cmd.clear_cwd();
if let Some(cwd) = &spawn_command.cwd {
cmd.cwd(cwd);
}
} else if let Some(dir) = cmd.get_cwd() {
// I'm not normally a fan of existence checking, but not checking here
// can be painful; in the case where a tab is local but has connected
@ -267,6 +336,7 @@ impl LocalDomain {
cmd.clear_cwd();
}
}
Ok(())
}
fn build_command(
@ -295,7 +365,8 @@ impl LocalDomain {
if let Some(dir) = command_dir {
cmd.cwd(dir);
}
self.fixup_command(&mut cmd);
self.fixup_command(&mut cmd)?;
log::info!("built: {cmd:?}");
Ok(cmd)
}
}

View File

@ -307,6 +307,20 @@ impl CommandBuilder {
)
}
pub fn iter_full_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> {
self.envs.values().filter_map(
|EnvEntry {
preferred_key,
value,
..
}| {
let key = preferred_key.to_str()?;
let value = value.to_str()?;
Some((key, value))
},
)
}
/// Return the configured command and arguments as a single string,
/// quoted per the unix shell conventions.
pub fn as_unix_command_line(&self) -> anyhow::Result<String> {

View File

@ -89,7 +89,7 @@ impl InputMap {
streak: 1,
button: MouseButton::Left
},
ExtendSelectionToMouseCursor(None)
ExtendSelectionToMouseCursor(SelectionMode::Cell)
],
[
Modifiers::SHIFT,
@ -125,7 +125,7 @@ impl InputMap {
streak: 1,
button: MouseButton::Left
},
ExtendSelectionToMouseCursor(Some(SelectionMode::Block))
ExtendSelectionToMouseCursor(SelectionMode::Block)
],
[
Modifiers::ALT | Modifiers::SHIFT,
@ -159,7 +159,7 @@ impl InputMap {
streak: 1,
button: MouseButton::Left
},
ExtendSelectionToMouseCursor(Some(SelectionMode::Cell))
ExtendSelectionToMouseCursor(SelectionMode::Cell)
],
[
Modifiers::ALT,
@ -167,7 +167,7 @@ impl InputMap {
streak: 1,
button: MouseButton::Left
},
ExtendSelectionToMouseCursor(Some(SelectionMode::Block))
ExtendSelectionToMouseCursor(SelectionMode::Block)
],
[
Modifiers::NONE,
@ -175,7 +175,7 @@ impl InputMap {
streak: 2,
button: MouseButton::Left
},
ExtendSelectionToMouseCursor(Some(SelectionMode::Word))
ExtendSelectionToMouseCursor(SelectionMode::Word)
],
[
Modifiers::NONE,
@ -183,7 +183,7 @@ impl InputMap {
streak: 3,
button: MouseButton::Left
},
ExtendSelectionToMouseCursor(Some(SelectionMode::Line))
ExtendSelectionToMouseCursor(SelectionMode::Line)
],
[
Modifiers::NONE,

View File

@ -366,6 +366,15 @@ fn update_mux_domains(config: &ConfigHandle) -> anyhow::Result<()> {
mux.add_domain(&domain);
}
for exec_dom in &config.exec_domains {
if mux.get_domain_by_name(&exec_dom.name).is_some() {
continue;
}
let domain: Arc<dyn Domain> = Arc::new(LocalDomain::new_exec_domain(exec_dom.clone())?);
mux.add_domain(&domain);
}
if let Some(name) = &config.default_domain {
if let Some(dom) = mux.get_domain_by_name(name) {
mux.set_default_domain(&dom);

View File

@ -118,13 +118,8 @@ impl super::TermWindow {
self.window.as_ref().unwrap().invalidate();
}
pub fn extend_selection_at_mouse_cursor(
&mut self,
mode: Option<SelectionMode>,
pane: &Rc<dyn Pane>,
) {
pub fn extend_selection_at_mouse_cursor(&mut self, mode: SelectionMode, pane: &Rc<dyn Pane>) {
self.selection(pane.pane_id()).seqno = pane.get_current_seqno();
let mode = mode.unwrap_or(SelectionMode::Cell);
let (position, y) = match self.pane_state(pane.pane_id()).mouse_terminal_coords {
Some(coords) => coords,
None => return,