diff --git a/docs/changelog.md b/docs/changelog.md index 8c55e0c4d..02901a587 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -60,6 +60,8 @@ As features stabilize some brief notes about them will accumulate here. [tab:rotate_counter_clockwise()](config/lua/MuxTab/rotate_counter_clockwise.md). [tab:rotate_counter_clockwise()](config/lua/MuxTab/rotate_counter_clockwise.md). * [wezterm.config_builder()](config/lua/wezterm/config_builder.md) +* [gui-attached](config/lua/gui-events/gui-attached.md) event provides some + more flexibility at startup. #### Fixed * X11: hanging or killing the IME could hang wezterm @@ -116,6 +118,9 @@ As features stabilize some brief notes about them will accumulate here. shown, which meant that a number of minor config issues could be overlooked. * Referencing `wezterm.GLOBAL` now returns references rather than copies, making it less cumbersome to code read/modify/write with global state +* `wezterm start` now accepts `--domain` and `--attach` options. `wezterm + connect DOMAIN` is now implemented internally as `wezterm start --domain + DOMAIN --attach`. #### Updated * Bundled harfbuzz updated to version 6.0.0 diff --git a/docs/config/lua/gui-events/gui-attached.md b/docs/config/lua/gui-events/gui-attached.md new file mode 100644 index 000000000..ecd9774b8 --- /dev/null +++ b/docs/config/lua/gui-events/gui-attached.md @@ -0,0 +1,40 @@ +# `gui-attached` + +*Since: nightly builds only* + +This event is triggered when the GUI is starting up after attaching the +selected domain. For example, when you use `wezterm connect DOMAIN` or +`wezterm start --domain DOMAIN` to start the GUI, the `gui-attached` event will +be triggered and passed the [MuxDomain](../MuxDomain/index.md) object +associated with `DOMAIN`. In cases where you don't specify the domain, the +default domain will be passed instead. + +This event fires after the [gui-startup](gui-startup.md) event. + +Note that the `gui-startup` event does not fire when invoking `wezterm connect +DOMAIN` or `wezterm start --domain DOMAIN --attach`. + +You can use this opportunity to take whatever action suits your purpose; some +users like to maximize all of their windows on startup, and this event would +allow you do that: + +```lua +local wezterm = require 'wezterm' +local mux = wezterm.mux + +wezterm.on('gui-attached', function(domain) + -- maximize all displayed windows on startup + local workspace = mux.get_active_workspace() + for _, window in ipairs(mux.all_windows()) do + if window:get_workspace() == workspace then + window:gui_window():maximize() + end + end +end) + +local config = wezterm.config_builder() + +return config +``` + +See also: [gui-startup](gui-startup.md). diff --git a/docs/config/lua/gui-events/gui-startup.md b/docs/config/lua/gui-events/gui-startup.md index 492485d95..255caca9f 100644 --- a/docs/config/lua/gui-events/gui-startup.md +++ b/docs/config/lua/gui-events/gui-startup.md @@ -15,6 +15,10 @@ program will be spawned. This event is useful for starting a set of programs in a standard configuration to save you the effort of doing it manually each time. +This event fires before [gui-attached](gui-attached.md). + +This event does not fire for `wezterm connect` invocations. + *Since: 20220807-113146-c2fee766* The event receives an optional [SpawnCommand](../SpawnCommand.md) argument that @@ -105,3 +109,4 @@ return {} See also: * [wezterm.mux](../wezterm.mux/index.md) +* [gui-attached](gui-attached.md). diff --git a/wezterm-gui-subcommands/src/lib.rs b/wezterm-gui-subcommands/src/lib.rs index 09bc5e4d4..c93283afa 100644 --- a/wezterm-gui-subcommands/src/lib.rs +++ b/wezterm-gui-subcommands/src/lib.rs @@ -204,6 +204,19 @@ pub struct StartCommand { #[arg(long, verbatim_doc_comment)] pub position: Option, + /// Name of the multiplexer domain section from the configuration + /// to which you'd like to connect. If omitted, the default domain + /// will be used. + #[arg(long)] + pub domain: Option, + + /// When used with --domain, if the domain already has running panes, + /// wezterm will simply attach and will NOT spawn the specified PROG. + /// If you omit --attach when using --domain, wezterm will attach + /// AND then spawn PROG. + #[arg(long, requires = "domain")] + pub attach: bool, + /// Instead of executing your shell, run PROG. /// For example: `wezterm start -- bash -l` will spawn bash /// as if it were a login shell. diff --git a/wezterm-gui/src/main.rs b/wezterm-gui/src/main.rs index a4f6a821c..8369dd627 100644 --- a/wezterm-gui/src/main.rs +++ b/wezterm-gui/src/main.rs @@ -7,12 +7,13 @@ use ::window::*; use anyhow::{anyhow, Context}; use clap::builder::ValueParser; use clap::{Parser, ValueHint}; -use config::keyassignment::SpawnCommand; +use config::keyassignment::{SpawnCommand, SpawnTabDomain}; use config::{ConfigHandle, SshDomain, SshMultiplexing}; use mux::activity::Activity; use mux::domain::{Domain, LocalDomain}; use mux::ssh::RemoteSshDomain; use mux::Mux; +use mux_lua::MuxDomain; use portable_pty::cmdbuilder::CommandBuilder; use promise::spawn::block_on; use std::borrow::Cow; @@ -240,84 +241,14 @@ fn client_domains(config: &config::ConfigHandle) -> Vec { domains } -async fn async_run_with_domain_as_default( - domain: Arc, - cmd: Option, -) -> anyhow::Result<()> { - let mux = Mux::get(); - crate::update::load_last_release_info_and_set_banner(); - - // Allow spawning local commands into new tabs/panes - let local_domain: Arc = Arc::new(LocalDomain::new("local")?); - mux.add_domain(&local_domain); - - // And configure their desired domain as the default - mux.add_domain(&domain); - mux.set_default_domain(&domain); - - let is_connecting = true; - spawn_tab_in_default_domain_if_mux_is_empty(cmd, is_connecting).await -} - -async fn async_run_mux_client(opts: ConnectCommand) -> anyhow::Result<()> { - if let Some(cls) = opts.class.as_ref() { - crate::set_window_class(cls); - } - if let Some(pos) = opts.position.as_ref() { - set_window_position(pos.clone()); - } - - let unix_socket_path = - config::RUNTIME_DIR.join(format!("gui-sock-{}", unsafe { libc::getpid() })); - std::env::set_var("WEZTERM_UNIX_SOCKET", unix_socket_path.clone()); - - let should_publish = false; - if let Err(err) = spawn_mux_server(unix_socket_path, should_publish) { - log::warn!("{:#}", err); - } - - let domain = Mux::get() - .get_domain_by_name(&opts.domain_name) - .ok_or_else(|| { - anyhow!( - "no multiplexer domain with name `{}` was found in the configuration", - opts.domain_name - ) - })?; - - let opts = opts.clone(); - let cmd = if !opts.prog.is_empty() { - let builder = CommandBuilder::from_argv(opts.prog); - Some(builder) - } else { - None - }; - - async_run_with_domain_as_default(domain, cmd).await -} - -fn run_mux_client(opts: ConnectCommand) -> anyhow::Result<()> { - let activity = Activity::new(); - build_initial_mux(&config::configuration(), None, opts.workspace.as_deref())?; - let gui = crate::frontend::try_new()?; - promise::spawn::spawn(async { - if let Err(err) = async_run_mux_client(opts).await { - terminate_with_error(err); - } - drop(activity); - }) - .detach(); - - gui.run_forever() -} - -async fn spawn_tab_in_default_domain_if_mux_is_empty( +async fn spawn_tab_in_domain_if_mux_is_empty( cmd: Option, is_connecting: bool, + domain: Option>, ) -> anyhow::Result<()> { let mux = Mux::get(); - let domain = mux.default_domain(); + let domain = domain.unwrap_or_else(|| mux.default_domain()); if !is_connecting { let have_panes_in_domain = mux @@ -350,6 +281,7 @@ async fn spawn_tab_in_default_domain_if_mux_is_empty( .any(|p| p.domain_id() == domain.domain_id()); if have_panes_in_domain { + trigger_and_log_gui_attached(MuxDomain(domain.domain_id())).await; return Ok(()); } @@ -369,6 +301,7 @@ async fn spawn_tab_in_default_domain_if_mux_is_empty( let _tab = domain .spawn(config.initial_size(dpi), cmd, None, window_id) .await?; + trigger_and_log_gui_attached(MuxDomain(domain.domain_id())).await; Ok(()) } @@ -437,6 +370,46 @@ async fn connect_to_auto_connect_domains() -> anyhow::Result<()> { Ok(()) } +async fn trigger_gui_startup( + lua: Option>, + spawn: Option, +) -> anyhow::Result<()> { + if let Some(lua) = lua { + let args = lua.pack_multi(spawn)?; + config::lua::emit_event(&lua, ("gui-startup".to_string(), args)).await?; + } + Ok(()) +} + +async fn trigger_and_log_gui_startup(spawn_command: Option) { + if let Err(err) = + config::with_lua_config_on_main_thread(move |lua| trigger_gui_startup(lua, spawn_command)) + .await + { + let message = format!("while processing gui-startup event: {:#}", err); + log::error!("{}", message); + persistent_toast_notification("Error", &message); + } +} + +async fn trigger_gui_attached(lua: Option>, domain: MuxDomain) -> anyhow::Result<()> { + if let Some(lua) = lua { + let args = lua.pack_multi(domain)?; + config::lua::emit_event(&lua, ("gui-attached".to_string(), args)).await?; + } + Ok(()) +} + +async fn trigger_and_log_gui_attached(domain: MuxDomain) { + if let Err(err) = + config::with_lua_config_on_main_thread(move |lua| trigger_gui_attached(lua, domain)).await + { + let message = format!("while processing gui-attached event: {:#}", err); + log::error!("{}", message); + persistent_toast_notification("Error", &message); + } +} + async fn async_run_terminal_gui( cmd: Option, opts: StartCommand, @@ -454,33 +427,65 @@ async fn async_run_terminal_gui( connect_to_auto_connect_domains().await?; } - async fn trigger_gui_startup( - lua: Option>, - spawn: Option, - ) -> anyhow::Result<()> { - if let Some(lua) = lua { - let args = lua.pack_multi(spawn)?; - config::lua::emit_event(&lua, ("gui-startup".to_string(), args)).await?; - } - Ok(()) - } - let spawn_command = match &cmd { Some(cmd) => Some(SpawnCommand::from_command_builder(cmd)?), None => None, }; - if let Err(err) = - config::with_lua_config_on_main_thread(move |lua| trigger_gui_startup(lua, spawn_command)) - .await - { - let message = format!("while processing gui-startup event: {:#}", err); - log::error!("{}", message); - persistent_toast_notification("Error", &message); + // Apply the domain to the command + let spawn_command = match (spawn_command, &opts.domain) { + (Some(spawn), Some(name)) => Some(SpawnCommand { + domain: SpawnTabDomain::DomainName(name.to_string()), + ..spawn + }), + (None, Some(name)) => Some(SpawnCommand { + domain: SpawnTabDomain::DomainName(name.to_string()), + ..SpawnCommand::default() + }), + (spawn, None) => spawn, + }; + let mux = Mux::get(); + + let domain = if let Some(name) = &opts.domain { + let domain = mux + .get_domain_by_name(name) + .ok_or_else(|| anyhow!("invalid domain {name}"))?; + Some(domain) + } else { + None + }; + + if !opts.attach { + trigger_and_log_gui_startup(spawn_command).await; } - let is_connecting = false; - spawn_tab_in_default_domain_if_mux_is_empty(cmd, is_connecting).await + let is_connecting = opts.attach; + + if let Some(domain) = &domain { + if !opts.attach { + let window_id = { + // Force the builder to notify the frontend early, + // so that the attach await below doesn't block it. + let builder = mux.new_empty_window(None); + *builder + }; + + domain.attach(Some(window_id)).await?; + let config = config::configuration(); + let dpi = config.dpi.unwrap_or_else(|| ::window::default_dpi()) as u32; + let tab = domain + .spawn(config.initial_size(dpi), cmd.clone(), None, window_id) + .await?; + let mut window = mux + .get_window_mut(window_id) + .ok_or_else(|| anyhow!("failed to get mux window id {window_id}"))?; + if let Some(tab_idx) = window.idx_by_id(tab.tab_id()) { + window.set_active_without_saving(tab_idx); + } + trigger_and_log_gui_attached(MuxDomain(domain.domain_id())).await; + } + } + spawn_tab_in_domain_if_mux_is_empty(cmd, is_connecting, domain).await } #[derive(Debug)] @@ -528,6 +533,7 @@ impl Publish { cmd: Option, config: &ConfigHandle, workspace: Option<&str>, + domain: SpawnTabDomain, ) -> anyhow::Result { if let Publish::TryPathOrPublish(gui_sock) = &self { let dom = config::UnixDomain { @@ -559,7 +565,7 @@ impl Publish { } client .spawn_v2(codec::SpawnV2 { - domain: config::keyassignment::SpawnTabDomain::DefaultDomain, + domain, window_id: None, command, command_dir: None, @@ -675,7 +681,7 @@ fn build_initial_mux( setup_mux(domain, config, default_domain_name, default_workspace_name) } -fn run_terminal_gui(opts: StartCommand) -> anyhow::Result<()> { +fn run_terminal_gui(opts: StartCommand, default_domain_name: Option) -> anyhow::Result<()> { if let Some(cls) = opts.class.as_ref() { crate::set_window_class(cls); } @@ -705,7 +711,11 @@ fn run_terminal_gui(opts: StartCommand) -> anyhow::Result<()> { None }; - let mux = build_initial_mux(&config, None, opts.workspace.as_deref())?; + let mux = build_initial_mux( + &config, + default_domain_name.as_deref(), + opts.workspace.as_deref(), + )?; // First, let's see if we can ask an already running wezterm to do this. // We must do this before we start the gui frontend as the scheduler @@ -716,7 +726,15 @@ fn run_terminal_gui(opts: StartCommand) -> anyhow::Result<()> { opts.always_new_process || opts.position.is_some(), ); log::trace!("{:?}", publish); - if publish.try_spawn(cmd.clone(), &config, opts.workspace.as_deref())? { + if publish.try_spawn( + cmd.clone(), + &config, + opts.workspace.as_deref(), + match &opts.domain { + Some(name) => SpawnTabDomain::DomainName(name.to_string()), + None => SpawnTabDomain::DefaultDomain, + }, + )? { return Ok(()); } @@ -1169,11 +1187,25 @@ fn run() -> anyhow::Result<()> { match sub { SubCommand::Start(start) => { log::trace!("Using configuration: {:#?}\nopts: {:#?}", config, opts); - run_terminal_gui(start) + run_terminal_gui(start, None) } SubCommand::Ssh(ssh) => run_ssh(ssh), SubCommand::Serial(serial) => run_serial(config, &serial), - SubCommand::Connect(connect) => run_mux_client(connect), + SubCommand::Connect(connect) => run_terminal_gui( + StartCommand { + domain: Some(connect.domain_name.clone()), + class: connect.class, + workspace: connect.workspace, + position: connect.position, + prog: connect.prog, + always_new_process: true, + attach: true, + _cmd: false, + no_auto_connect: false, + cwd: None, + }, + Some(connect.domain_name), + ), SubCommand::LsFonts(cmd) => run_ls_fonts(config, &cmd), SubCommand::ShowKeys(cmd) => run_show_keys(config, &cmd), }