1
1
mirror of https://github.com/wez/wezterm.git synced 2024-09-21 19:58:15 +03:00

refactor tab navigator into launcher menu

The same core code is used to render both the tab navigator
and the launcher.  The context is specified using some flags
so that you don't get an unholy mess in both places.

The net result of this is that the tab navigator now supports
fuzzy matching.

refs: #664
refs: 1485
This commit is contained in:
Wez Furlong 2022-01-04 09:35:07 -07:00
parent 142f3c7d81
commit 541701d822
4 changed files with 226 additions and 309 deletions

View File

@ -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<Entry>) -> 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<LauncherDomainEntry>,
tabs: Vec<LauncherTabEntry>,
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<TermConfig>,
@ -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<Entry> = 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<Entry> = 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;
}

View File

@ -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<T, F>(
term_window: &TermWindow,

View File

@ -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(())
}

View File

@ -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();