diff --git a/wezterm-gui/src/overlay/launcher.rs b/wezterm-gui/src/overlay/launcher.rs index 4e57fa517..05739d255 100644 --- a/wezterm-gui/src/overlay/launcher.rs +++ b/wezterm-gui/src/overlay/launcher.rs @@ -30,6 +30,17 @@ use termwiz::surface::{Change, Position}; use termwiz::terminal::Terminal; use window::WindowOps; +bitflags::bitflags! { + pub struct LauncherFlags :u32 { + const ZERO = 0; + const WSL_DISTROS = 1; + const TABS = 2; + const LAUNCH_MENU_ITEMS = 4; + const DOMAINS = 8; + const KEY_ASSIGNMENTS = 16; + } +} + #[derive(Clone)] enum EntryKind { Spawn { @@ -238,13 +249,119 @@ fn enumerate_wsl_entries(entries: &mut Vec) -> anyhow::Result<()> { Ok(()) } -pub fn launcher( - _tab_id: TabId, +pub struct LauncherTabEntry { + pub title: String, + pub tab_id: TabId, + pub tab_idx: usize, + pub pane_count: usize, +} + +pub struct LauncherDomainEntry { + pub domain_id: DomainId, + pub name: String, + pub state: DomainState, + pub label: String, +} + +pub struct LauncherArgs { + flags: LauncherFlags, + domains: Vec, + tabs: Vec, pane_id: PaneId, domain_id_of_current_tab: DomainId, - mut term: TermWizTerminal, mux_window_id: WindowId, - domains: Vec<(DomainId, String, DomainState, String)>, + title: String, +} + +impl LauncherArgs { + /// Must be called on the Mux thread! + pub fn new( + title: &str, + flags: LauncherFlags, + mux_window_id: WindowId, + pane_id: PaneId, + domain_id_of_current_tab: DomainId, + ) -> Self { + let mux = Mux::get().unwrap(); + + let tabs = if flags.contains(LauncherFlags::TABS) { + // Ideally we'd resolve the tabs on the fly once we've started the + // overlay, but since the overlay runs in a different thread, accessing + // the mux list is a bit awkward. To get the ball rolling we capture + // the list of tabs up front and live with a static list. + let window = mux + .get_window(mux_window_id) + .expect("to resolve my own window_id"); + window + .iter() + .enumerate() + .map(|(tab_idx, tab)| LauncherTabEntry { + title: tab + .get_active_pane() + .expect("tab to have a pane") + .get_title(), + tab_id: tab.tab_id(), + tab_idx, + pane_count: tab.count_panes(), + }) + .collect() + } else { + vec![] + }; + + let domains = if flags.contains(LauncherFlags::DOMAINS) { + let mut domains = mux.iter_domains(); + domains.sort_by(|a, b| { + let a_state = a.state(); + let b_state = b.state(); + if a_state != b_state { + use std::cmp::Ordering; + return if a_state == DomainState::Attached { + Ordering::Less + } else { + Ordering::Greater + }; + } + a.domain_id().cmp(&b.domain_id()) + }); + domains.retain(|dom| dom.spawnable()); + domains + .iter() + .map(|dom| { + let name = dom.domain_name(); + let label = dom.domain_label(); + let label = if name == label || label == "" { + format!("domain `{}`", name) + } else { + format!("domain `{}` - {}", name, label) + }; + LauncherDomainEntry { + domain_id: dom.domain_id(), + name: name.to_string(), + state: dom.state(), + label, + } + }) + .collect() + } else { + vec![] + }; + + Self { + flags, + mux_window_id, + domains, + tabs, + pane_id, + domain_id_of_current_tab, + title: title.to_string(), + } + } +} + +pub fn launcher( + args: LauncherArgs, + mut term: TermWizTerminal, clipboard: ClipboardHelper, size: PtySize, term_config: Arc, @@ -310,36 +427,36 @@ pub fn launcher( // Pull in the user defined entries from the launch_menu // section of the configuration. - for item in &config.launch_menu { - state.entries.push(Entry { - label: match item.label.as_ref() { - Some(label) => label.to_string(), - None => match item.args.as_ref() { - Some(args) => args.join(" "), - None => "(default shell)".to_string(), + if args.flags.contains(LauncherFlags::LAUNCH_MENU_ITEMS) { + for item in &config.launch_menu { + state.entries.push(Entry { + label: match item.label.as_ref() { + Some(label) => label.to_string(), + None => match item.args.as_ref() { + Some(args) => args.join(" "), + None => "(default shell)".to_string(), + }, }, - }, - kind: EntryKind::Spawn { - command: item.clone(), - spawn_where: SpawnWhere::NewTab, - }, - }); - } - - #[cfg(windows)] - { - if config.add_wsl_distributions_to_launch_menu { - let _ = enumerate_wsl_entries(&mut state.entries); + kind: EntryKind::Spawn { + command: item.clone(), + spawn_where: SpawnWhere::NewTab, + }, + }); } } - for (domain_id, domain_name, domain_state, domain_label) in &domains { - let entry = if *domain_state == DomainState::Attached { + #[cfg(windows)] + if args.flags.contains(LauncherFlags::WSL_DISTROS) { + let _ = enumerate_wsl_entries(&mut state.entries); + } + + for domain in &args.domains { + let entry = if domain.state == DomainState::Attached { Entry { - label: format!("New Tab ({})", domain_label), + label: format!("New Tab ({})", domain.label), kind: EntryKind::Spawn { command: SpawnCommand { - domain: SpawnTabDomain::DomainName(domain_name.to_string()), + domain: SpawnTabDomain::DomainName(domain.name.to_string()), ..SpawnCommand::default() }, spawn_where: SpawnWhere::NewTab, @@ -347,54 +464,66 @@ pub fn launcher( } } else { Entry { - label: format!("Attach {}", domain_label), - kind: EntryKind::Attach { domain: *domain_id }, + label: format!("Attach {}", domain.label), + kind: EntryKind::Attach { + domain: domain.domain_id, + }, } }; // Preselect the entry that corresponds to the active tab // at the time that the launcher was set up, so that pressing // Enter immediately afterwards spawns a tab in the same domain. - if *domain_id == domain_id_of_current_tab { + if domain.domain_id == args.domain_id_of_current_tab { state.active_idx = state.entries.len(); } state.entries.push(entry); } - // Grab interestig key assignments and show those as a kind of command palette - let input_map = InputMap::new(&config); - let mut key_entries: Vec = vec![]; - for ((keycode, mods), assignment) in input_map.keys { - if matches!( - &assignment, - KeyAssignment::ActivateTabRelative(_) | KeyAssignment::ActivateTab(_) - ) { - // Filter out some noisy, repetitive entries - continue; - } - if key_entries - .iter() - .find(|ent| match &ent.kind { - EntryKind::KeyAssignment(a) => a == &assignment, - _ => false, - }) - .is_some() - { - // Avoid duplicate entries - continue; - } - key_entries.push(Entry { - label: format!( - "{:?} ({} {})", - assignment, - mods.to_string(), - keycode.to_string() - ), - kind: EntryKind::KeyAssignment(assignment), + for tab in &args.tabs { + state.entries.push(Entry { + label: format!("{}. {} panes", tab.title, tab.pane_count), + kind: EntryKind::KeyAssignment(KeyAssignment::ActivateTab(tab.tab_idx as isize)), }); } - key_entries.sort_by(|a, b| a.label.cmp(&b.label)); - state.entries.append(&mut key_entries); + + // Grab interestig key assignments and show those as a kind of command palette + if args.flags.contains(LauncherFlags::KEY_ASSIGNMENTS) { + let input_map = InputMap::new(&config); + let mut key_entries: Vec = vec![]; + for ((keycode, mods), assignment) in input_map.keys { + if matches!( + &assignment, + KeyAssignment::ActivateTabRelative(_) | KeyAssignment::ActivateTab(_) + ) { + // Filter out some noisy, repetitive entries + continue; + } + if key_entries + .iter() + .find(|ent| match &ent.kind { + EntryKind::KeyAssignment(a) => a == &assignment, + _ => false, + }) + .is_some() + { + // Avoid duplicate entries + continue; + } + key_entries.push(Entry { + label: format!( + "{:?} ({} {})", + assignment, + mods.to_string(), + keycode.to_string() + ), + kind: EntryKind::KeyAssignment(assignment), + }); + } + key_entries.sort_by(|a, b| a.label.cmp(&b.label)); + state.entries.append(&mut key_entries); + } + state.update_filter(); fn render(state: &mut LauncherState, term: &mut TermWizTerminal) -> termwiz::Result<()> { @@ -474,7 +603,7 @@ pub fn launcher( term.render(&changes) } - term.render(&[Change::Title("Launcher".to_string())])?; + term.render(&[Change::Title(args.title.to_string())])?; render(&mut state, &mut term)?; fn launch( @@ -532,11 +661,11 @@ pub fn launcher( state.top_row + (c as u32 - '1' as u32) as usize, &state.filtered_entries, size, - mux_window_id, + args.mux_window_id, clipboard, term_config, &window, - pane_id, + args.pane_id, ); break; } @@ -583,11 +712,11 @@ pub fn launcher( state.active_idx, &state.filtered_entries, size, - mux_window_id, + args.mux_window_id, clipboard, term_config, &window, - pane_id, + args.pane_id, ); break; } @@ -605,11 +734,11 @@ pub fn launcher( state.active_idx, &state.filtered_entries, size, - mux_window_id, + args.mux_window_id, clipboard, term_config, &window, - pane_id, + args.pane_id, ); break; } diff --git a/wezterm-gui/src/overlay/mod.rs b/wezterm-gui/src/overlay/mod.rs index e19cb3a95..325661059 100644 --- a/wezterm-gui/src/overlay/mod.rs +++ b/wezterm-gui/src/overlay/mod.rs @@ -12,7 +12,6 @@ mod debug; mod launcher; mod quickselect; mod search; -mod tabnavigator; pub use confirm_close_pane::confirm_close_pane; pub use confirm_close_pane::confirm_close_tab; @@ -20,10 +19,9 @@ pub use confirm_close_pane::confirm_close_window; pub use confirm_close_pane::confirm_quit_program; pub use copy::CopyOverlay; pub use debug::show_debug_overlay; -pub use launcher::launcher; +pub use launcher::{launcher, LauncherArgs, LauncherFlags}; pub use quickselect::QuickSelectOverlay; pub use search::SearchOverlay; -pub use tabnavigator::tab_navigator; pub fn start_overlay( term_window: &TermWindow, diff --git a/wezterm-gui/src/overlay/tabnavigator.rs b/wezterm-gui/src/overlay/tabnavigator.rs deleted file mode 100644 index dcfbc9c45..000000000 --- a/wezterm-gui/src/overlay/tabnavigator.rs +++ /dev/null @@ -1,159 +0,0 @@ -use anyhow::anyhow; -use mux::tab::TabId; -use mux::termwiztermtab::TermWizTerminal; -use mux::window::WindowId; -use mux::Mux; -use termwiz::cell::{AttributeChange, CellAttributes}; -use termwiz::color::ColorAttribute; -use termwiz::input::{InputEvent, KeyCode, KeyEvent, MouseButtons, MouseEvent}; -use termwiz::surface::{Change, Position}; -use termwiz::terminal::Terminal; - -pub fn tab_navigator( - tab_id: TabId, - mut term: TermWizTerminal, - tab_list: Vec<(String, TabId, usize)>, - mux_window_id: WindowId, -) -> anyhow::Result<()> { - let mut active_tab_idx = tab_list - .iter() - .position(|(_title, id, _)| *id == tab_id) - .unwrap_or(0); - - term.set_raw_mode()?; - - fn render( - active_tab_idx: usize, - tab_list: &[(String, TabId, usize)], - term: &mut TermWizTerminal, - ) -> termwiz::Result<()> { - // let dims = term.get_screen_size()?; - let mut changes = vec![ - Change::ClearScreen(ColorAttribute::Default), - Change::CursorPosition { - x: Position::Absolute(0), - y: Position::Absolute(0), - }, - Change::Text( - "Select a tab and press Enter to activate it. Press Escape to cancel\r\n" - .to_string(), - ), - Change::AllAttributes(CellAttributes::default()), - ]; - - for (idx, (title, _tab_id, num_panes)) in tab_list.iter().enumerate() { - if idx == active_tab_idx { - changes.push(AttributeChange::Reverse(true).into()); - } - - changes.push(Change::Text(format!( - " {}. {}. {} panes\r\n", - idx + 1, - title, - num_panes - ))); - - if idx == active_tab_idx { - changes.push(AttributeChange::Reverse(false).into()); - } - } - - term.render(&changes)?; - term.flush() - } - - term.render(&[Change::Title("Tab Navigator".to_string())])?; - - render(active_tab_idx, &tab_list, &mut term)?; - - fn select_tab_by_idx( - idx: usize, - mux_window_id: WindowId, - tab_list: &Vec<(String, TabId, usize)>, - ) -> bool { - if idx >= tab_list.len() { - false - } else { - promise::spawn::spawn_into_main_thread(async move { - let mux = Mux::get().unwrap(); - let mut window = mux - .get_window_mut(mux_window_id) - .ok_or_else(|| anyhow!("no such window"))?; - - window.save_and_then_set_active(idx); - anyhow::Result::<()>::Ok(()) - }) - .detach(); - true - } - } - - while let Ok(Some(event)) = term.poll_input(None) { - match event { - InputEvent::Key(KeyEvent { - key: KeyCode::Char('k'), - .. - }) - | InputEvent::Key(KeyEvent { - key: KeyCode::UpArrow, - .. - }) => { - active_tab_idx = active_tab_idx.saturating_sub(1); - } - InputEvent::Key(KeyEvent { - key: KeyCode::Char('j'), - .. - }) - | InputEvent::Key(KeyEvent { - key: KeyCode::DownArrow, - .. - }) => { - active_tab_idx = (active_tab_idx + 1).min(tab_list.len() - 1); - } - InputEvent::Key(KeyEvent { - key: KeyCode::Escape, - .. - }) => { - break; - } - InputEvent::Key(KeyEvent { - key: KeyCode::Char(c), - .. - }) => { - if c >= '1' && c <= '9' { - let idx = c as u8 - '1' as u8; - if select_tab_by_idx(idx as usize, mux_window_id, &tab_list) { - break; - } - } - } - InputEvent::Mouse(MouseEvent { - y, mouse_buttons, .. - }) => { - if y > 0 && y as usize <= tab_list.len() { - active_tab_idx = y as usize - 1; - - if mouse_buttons == MouseButtons::LEFT { - select_tab_by_idx(active_tab_idx, mux_window_id, &tab_list); - break; - } - } - if mouse_buttons != MouseButtons::NONE { - // Treat any other mouse button as cancel - break; - } - } - InputEvent::Key(KeyEvent { - key: KeyCode::Enter, - .. - }) => { - select_tab_by_idx(active_tab_idx, mux_window_id, &tab_list); - break; - } - _ => {} - } - render(active_tab_idx, &tab_list, &mut term)?; - } - - Ok(()) -} diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs index 5b08a7df3..e239888e1 100644 --- a/wezterm-gui/src/termwindow/mod.rs +++ b/wezterm-gui/src/termwindow/mod.rs @@ -5,8 +5,8 @@ use crate::cache::LruCache; use crate::glium::texture::SrgbTexture2d; use crate::overlay::{ confirm_close_pane, confirm_close_tab, confirm_close_window, confirm_quit_program, launcher, - start_overlay, start_overlay_pane, tab_navigator, CopyOverlay, QuickSelectOverlay, - SearchOverlay, + start_overlay, start_overlay_pane, CopyOverlay, LauncherArgs, LauncherFlags, + QuickSelectOverlay, SearchOverlay, }; use crate::scripting::guiwin::GuiWin; use crate::scripting::pane::PaneObject; @@ -27,7 +27,6 @@ use config::{ WindowCloseConfirmation, }; use mlua::{FromLua, UserData, UserDataFields}; -use mux::domain::{DomainId, DomainState}; use mux::pane::{CloseReason, Pane, PaneId}; use mux::renderable::RenderableDimensions; use mux::tab::{PositionedPane, PositionedSplit, SplitDirection, Tab, TabId}; @@ -1657,42 +1656,23 @@ impl TermWindow { } fn show_tab_navigator(&mut self) { - let mux = Mux::get().unwrap(); - let tab = match mux.get_active_tab_for_window(self.mux_window_id) { - Some(tab) => tab, - None => return, - }; - - let window = mux - .get_window(self.mux_window_id) - .expect("to resolve my own window_id"); - - // Ideally we'd resolve the tabs on the fly once we've started the - // overlay, but since the overlay runs in a different thread, accessing - // the mux list is a bit awkward. To get the ball rolling we capture - // the list of tabs up front and live with a static list. - let tabs: Vec<(String, TabId, usize)> = window - .iter() - .map(|tab| { - ( - tab.get_active_pane() - .expect("tab to have a pane") - .get_title(), - tab.tab_id(), - tab.count_panes(), - ) - }) - .collect(); - - let mux_window_id = self.mux_window_id; - let (overlay, future) = start_overlay(self, &tab, move |tab_id, term| { - tab_navigator(tab_id, term, tabs, mux_window_id) - }); - self.assign_overlay(tab.tab_id(), overlay); - promise::spawn::spawn(future).detach(); + self.show_launcher_impl("Tab Navigator", LauncherFlags::TABS); } fn show_launcher(&mut self) { + self.show_launcher_impl( + "Launcher", + if self.config.add_wsl_distributions_to_launch_menu { + LauncherFlags::WSL_DISTROS + } else { + LauncherFlags::ZERO + } | LauncherFlags::LAUNCH_MENU_ITEMS + | LauncherFlags::DOMAINS + | LauncherFlags::KEY_ASSIGNMENTS, + ); + } + + fn show_launcher_impl(&mut self, title: &str, flags: LauncherFlags) { let mux = Mux::get().unwrap(); let tab = match mux.get_active_tab_for_window(self.mux_window_id) { Some(tab) => tab, @@ -1711,35 +1691,6 @@ impl TermWindow { window: window.clone(), }; - let mut domains = mux.iter_domains(); - domains.sort_by(|a, b| { - let a_state = a.state(); - let b_state = b.state(); - if a_state != b_state { - use std::cmp::Ordering; - return if a_state == DomainState::Attached { - Ordering::Less - } else { - Ordering::Greater - }; - } - a.domain_id().cmp(&b.domain_id()) - }); - domains.retain(|dom| dom.spawnable()); - let domains: Vec<(DomainId, String, DomainState, String)> = domains - .iter() - .map(|dom| { - let name = dom.domain_name(); - let label = dom.domain_label(); - let label = if name == label || label == "" { - format!("domain `{}`", name) - } else { - format!("domain `{}` - {}", name, label) - }; - (dom.domain_id(), name.to_string(), dom.state(), label) - }) - .collect(); - let domain_id_of_current_pane = tab .get_active_pane() .expect("tab has no panes!") @@ -1748,19 +1699,17 @@ impl TermWindow { let term_config = Arc::new(TermConfig::with_config(self.config.clone())); let pane_id = pane.pane_id(); - let (overlay, future) = start_overlay(self, &tab, move |tab_id, term| { - launcher( - tab_id, - pane_id, - domain_id_of_current_pane, - term, - mux_window_id, - domains, - clipboard, - size, - term_config, - window, - ) + + let args = LauncherArgs::new( + title, + flags, + mux_window_id, + pane_id, + domain_id_of_current_pane, + ); + + let (overlay, future) = start_overlay(self, &tab, move |_tab_id, term| { + launcher(args, term, clipboard, size, term_config, window) }); self.assign_overlay(tab.tab_id(), overlay); promise::spawn::spawn(future).detach();