diff --git a/config/src/config.rs b/config/src/config.rs index 42b8f10d4..1f01e0156 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -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, + #[dynamic(default)] + pub exec_domains: Vec, + /// The set of unix domains #[dynamic(default = "UnixDomain::default_unix_domains")] pub unix_domains: Vec, diff --git a/config/src/exec_domain.rs b/config/src/exec_domain.rs new file mode 100644 index 000000000..2092a2c6a --- /dev/null +++ b/config/src/exec_domain.rs @@ -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); diff --git a/config/src/keyassignment.rs b/config/src/keyassignment.rs index 770fc62d1..20dd08180 100644 --- a/config/src/keyassignment.rs +++ b/config/src/keyassignment.rs @@ -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), + ExtendSelectionToMouseCursor(SelectionMode), OpenLinkAtMouseCursor, ClearSelection, CompleteSelection(ClipboardCopyDestination), diff --git a/config/src/lib.rs b/config/src/lib.rs index 50c1a4347..10aca52c5 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -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::*; diff --git a/config/src/lua.rs b/config/src/lua.rs index 8a520f15e..0626733c2 100644 --- a/config/src/lua.rs +++ b/config/src/lua.rs @@ -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> { + 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) -> mlua::Result { + Ok(shlex::join(args.iter().map(|arg| arg.as_ref()))) +} + +fn shell_quote_arg<'lua>(_: &'lua Lua, arg: String) -> mlua::Result { + Ok(shlex::quote(&arg).into_owned().to_string()) +} + fn strftime_utc<'lua>(_: &'lua Lua, format: String) -> mlua::Result { use chrono::prelude::*; let local: DateTime = 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 { + 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> { diff --git a/docs/config/lua/keyassignment/ExtendSelectionToMouseCursor.md b/docs/config/lua/keyassignment/ExtendSelectionToMouseCursor.md index 6919e9298..fc98ee484 100644 --- a/docs/config/lua/keyassignment/ExtendSelectionToMouseCursor.md +++ b/docs/config/lua/keyassignment/ExtendSelectionToMouseCursor.md @@ -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. diff --git a/docs/config/lua/wezterm/shell_join_args.md b/docs/config/lua/wezterm/shell_join_args.md new file mode 100644 index 000000000..6e93b484d --- /dev/null +++ b/docs/config/lua/wezterm/shell_join_args.md @@ -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. diff --git a/docs/config/lua/wezterm/shell_quote_arg.md b/docs/config/lua/wezterm/shell_quote_arg.md new file mode 100644 index 000000000..44c2faf51 --- /dev/null +++ b/docs/config/lua/wezterm/shell_quote_arg.md @@ -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\"" +``` diff --git a/docs/config/lua/wezterm/shell_split.md b/docs/config/lua/wezterm/shell_split.md new file mode 100644 index 000000000..87a5bee57 --- /dev/null +++ b/docs/config/lua/wezterm/shell_split.md @@ -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", +] +``` diff --git a/luahelper/src/lib.rs b/luahelper/src/lib.rs index 11e036f06..2bc094698 100644 --- a/luahelper/src/lib.rs +++ b/luahelper/src/lib.rs @@ -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 { - dynamic_to_lua_value_impl(lua, value, false) -} - -pub fn dynamic_to_lua_table_value<'lua>( - lua: &'lua mlua::Lua, - value: DynValue, -) -> mlua::Result { - 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 { 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) diff --git a/mux/src/domain.rs b/mux/src/domain.rs index d9012ef5b..b36474659 100644 --- a/mux/src/domain.rs +++ b/mux/src/domain.rs @@ -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, + exec_domain: Option, } 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 { + 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 = 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) } } diff --git a/pty/src/cmdbuilder.rs b/pty/src/cmdbuilder.rs index c2a5961b6..004274c8e 100644 --- a/pty/src/cmdbuilder.rs +++ b/pty/src/cmdbuilder.rs @@ -307,6 +307,20 @@ impl CommandBuilder { ) } + pub fn iter_full_env_as_str(&self) -> impl Iterator { + 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 { diff --git a/wezterm-gui/src/inputmap.rs b/wezterm-gui/src/inputmap.rs index 5d2583c17..6a7b30617 100644 --- a/wezterm-gui/src/inputmap.rs +++ b/wezterm-gui/src/inputmap.rs @@ -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, diff --git a/wezterm-gui/src/main.rs b/wezterm-gui/src/main.rs index f3407c63b..fc7db1904 100644 --- a/wezterm-gui/src/main.rs +++ b/wezterm-gui/src/main.rs @@ -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 = 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); diff --git a/wezterm-gui/src/termwindow/selection.rs b/wezterm-gui/src/termwindow/selection.rs index cb882c84b..cfedbe192 100644 --- a/wezterm-gui/src/termwindow/selection.rs +++ b/wezterm-gui/src/termwindow/selection.rs @@ -118,13 +118,8 @@ impl super::TermWindow { self.window.as_ref().unwrap().invalidate(); } - pub fn extend_selection_at_mouse_cursor( - &mut self, - mode: Option, - pane: &Rc, - ) { + pub fn extend_selection_at_mouse_cursor(&mut self, mode: SelectionMode, pane: &Rc) { 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,