From d7145561c231e7144bcfb8cbbff9629b37990461 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 26 Jan 2023 15:50:49 -0700 Subject: [PATCH] Add wezterm start --domain DOMAIN --attach + gui-attached event The motivation here was to remove some similar but not quite the same logic that existed for starting up when using `wezterm connect`. Now `wezterm connect DOMAIN` is implemented as `wezterm start --domain DOMAIN --attach --always-new-process` and a little extra hand-wave to ensure that the default domain is set correctly. The startup events have been refactored a bit; a new gui-attached event is emitted after attaching and starting any default programs in the selected domain at startup. That event can be used to maximize windows and so on, if desired. The gui-attached event is independent of the startup command and fires for `wezterm connect`, which `gui-startup` did not (and could not) do. --- docs/changelog.md | 5 + docs/config/lua/gui-events/gui-attached.md | 40 ++++ docs/config/lua/gui-events/gui-startup.md | 5 + wezterm-gui-subcommands/src/lib.rs | 13 ++ wezterm-gui/src/main.rs | 232 ++++++++++++--------- 5 files changed, 195 insertions(+), 100 deletions(-) create mode 100644 docs/config/lua/gui-events/gui-attached.md 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), }