1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 21:32:13 +03:00

launcher now has fuzzy matching

Just start typing text to build up the fuzzy matching term.
The list of items updates to match the results.

refs: #664
refs: #1485
This commit is contained in:
Wez Furlong 2022-01-04 08:45:15 -07:00
parent 68fb416b34
commit 142f3c7d81
3 changed files with 141 additions and 54 deletions

10
Cargo.lock generated
View File

@ -1485,6 +1485,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.12.4" version = "0.12.4"
@ -4711,6 +4720,7 @@ dependencies = [
"euclid", "euclid",
"fastrand", "fastrand",
"filedescriptor", "filedescriptor",
"fuzzy-matcher",
"hdrhistogram", "hdrhistogram",
"http_req", "http_req",
"image", "image",

View File

@ -33,6 +33,7 @@ env-bootstrap = { path = "../env-bootstrap" }
euclid = "0.22" euclid = "0.22"
fastrand = "1.6" fastrand = "1.6"
filedescriptor = { version="0.8", path = "../filedescriptor" } filedescriptor = { version="0.8", path = "../filedescriptor" }
fuzzy-matcher = "0.3"
hdrhistogram = "7.1" hdrhistogram = "7.1"
http_req = "0.8" http_req = "0.8"
image = "0.23" image = "0.23"

View File

@ -12,6 +12,8 @@ use anyhow::anyhow;
use config::keyassignment::{InputMap, KeyAssignment, SpawnCommand, SpawnTabDomain}; use config::keyassignment::{InputMap, KeyAssignment, SpawnCommand, SpawnTabDomain};
use config::lua::truncate_right; use config::lua::truncate_right;
use config::{configuration, TermConfig}; use config::{configuration, TermConfig};
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use mux::domain::{DomainId, DomainState}; use mux::domain::{DomainId, DomainState};
use mux::pane::PaneId; use mux::pane::PaneId;
use mux::tab::TabId; use mux::tab::TabId;
@ -248,9 +250,59 @@ pub fn launcher(
term_config: Arc<TermConfig>, term_config: Arc<TermConfig>,
window: ::window::Window, window: ::window::Window,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut active_idx = 0; struct LauncherState {
let mut top_row; active_idx: usize,
let mut entries = vec![]; top_row: usize,
entries: Vec<Entry>,
filter_term: String,
filtered_entries: Vec<Entry>,
}
impl LauncherState {
fn update_filter(&mut self) {
if self.filter_term.is_empty() {
self.filtered_entries = self.entries.clone();
return;
}
self.filtered_entries.clear();
let matcher = SkimMatcherV2::default();
struct MatchResult {
row_idx: usize,
score: i64,
}
let mut scores: Vec<MatchResult> = self
.entries
.iter()
.enumerate()
.filter_map(|(row_idx, entry)| {
let score = matcher.fuzzy_match(&entry.label, &self.filter_term)?;
Some(MatchResult { row_idx, score })
})
.collect();
scores.sort_by(|a, b| a.score.cmp(&b.score).reverse());
for result in scores {
self.filtered_entries
.push(self.entries[result.row_idx].clone());
}
self.active_idx = 0;
self.top_row = 0;
}
}
let mut state = LauncherState {
active_idx: 0,
top_row: 0,
entries: vec![],
filter_term: String::new(),
filtered_entries: vec![],
};
term.set_raw_mode()?; term.set_raw_mode()?;
@ -259,7 +311,7 @@ pub fn launcher(
// Pull in the user defined entries from the launch_menu // Pull in the user defined entries from the launch_menu
// section of the configuration. // section of the configuration.
for item in &config.launch_menu { for item in &config.launch_menu {
entries.push(Entry { state.entries.push(Entry {
label: match item.label.as_ref() { label: match item.label.as_ref() {
Some(label) => label.to_string(), Some(label) => label.to_string(),
None => match item.args.as_ref() { None => match item.args.as_ref() {
@ -277,7 +329,7 @@ pub fn launcher(
#[cfg(windows)] #[cfg(windows)]
{ {
if config.add_wsl_distributions_to_launch_menu { if config.add_wsl_distributions_to_launch_menu {
let _ = enumerate_wsl_entries(&mut entries); let _ = enumerate_wsl_entries(&mut state.entries);
} }
} }
@ -304,9 +356,9 @@ pub fn launcher(
// at the time that the launcher was set up, so that pressing // at the time that the launcher was set up, so that pressing
// Enter immediately afterwards spawns a tab in the same domain. // Enter immediately afterwards spawns a tab in the same domain.
if *domain_id == domain_id_of_current_tab { if *domain_id == domain_id_of_current_tab {
active_idx = entries.len(); state.active_idx = state.entries.len();
} }
entries.push(entry); state.entries.push(entry);
} }
// Grab interestig key assignments and show those as a kind of command palette // Grab interestig key assignments and show those as a kind of command palette
@ -342,13 +394,10 @@ pub fn launcher(
}); });
} }
key_entries.sort_by(|a, b| a.label.cmp(&b.label)); key_entries.sort_by(|a, b| a.label.cmp(&b.label));
entries.append(&mut key_entries); state.entries.append(&mut key_entries);
state.update_filter();
fn render( fn render(state: &mut LauncherState, term: &mut TermWizTerminal) -> termwiz::Result<()> {
active_idx: usize,
entries: &[Entry],
term: &mut TermWizTerminal,
) -> termwiz::Result<usize> {
let size = term.get_screen_size()?; let size = term.get_screen_size()?;
let max_width = size.cols.saturating_sub(6); let max_width = size.cols.saturating_sub(6);
@ -370,22 +419,28 @@ pub fn launcher(
]; ];
let max_items = size.rows - 3; let max_items = size.rows - 3;
let num_items = entries.len(); let num_items = state.filtered_entries.len();
let skip = if num_items < max_items { let skip = if num_items < max_items {
0 0
} else if num_items - active_idx < max_items { } else if num_items - state.active_idx < max_items {
// Align to bottom // Align to bottom
(num_items - max_items).saturating_sub(1) (num_items - max_items).saturating_sub(1)
} else { } else {
active_idx.saturating_sub(2) state.active_idx.saturating_sub(2)
}; };
for (row_num, (entry_idx, entry)) in entries.iter().enumerate().skip(skip).enumerate() { for (row_num, (entry_idx, entry)) in state
.filtered_entries
.iter()
.enumerate()
.skip(skip)
.enumerate()
{
if row_num > max_items { if row_num > max_items {
break; break;
} }
if entry_idx == active_idx { if entry_idx == state.active_idx {
changes.push(AttributeChange::Reverse(true).into()); changes.push(AttributeChange::Reverse(true).into());
} }
@ -396,16 +451,31 @@ pub fn launcher(
changes.push(Change::Text(format!(" {} \r\n", label))); changes.push(Change::Text(format!(" {} \r\n", label)));
} }
if entry_idx == active_idx { if entry_idx == state.active_idx {
changes.push(AttributeChange::Reverse(false).into()); changes.push(AttributeChange::Reverse(false).into());
} }
} }
term.render(&changes)?; state.top_row = skip;
Ok(skip)
if !state.filter_term.is_empty() {
changes.append(&mut vec![
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(0),
},
Change::ClearToEndOfLine(ColorAttribute::Default),
Change::Text(truncate_right(
&format!("Fuzzy matching: {}", state.filter_term),
max_width,
)),
]);
}
term.render(&changes)
} }
term.render(&[Change::Title("Launcher".to_string())])?; term.render(&[Change::Title("Launcher".to_string())])?;
top_row = render(active_idx, &entries, &mut term)?; render(&mut state, &mut term)?;
fn launch( fn launch(
active_idx: usize, active_idx: usize,
@ -455,24 +525,46 @@ pub fn launcher(
while let Ok(Some(event)) = term.poll_input(None) { while let Ok(Some(event)) = term.poll_input(None) {
match event { match event {
InputEvent::Key(KeyEvent { InputEvent::Key(KeyEvent {
key: KeyCode::Char('k'), key: KeyCode::Char(c),
.. ..
}) }) if c >= '1' && c <= '9' => {
| InputEvent::Key(KeyEvent { launch(
state.top_row + (c as u32 - '1' as u32) as usize,
&state.filtered_entries,
size,
mux_window_id,
clipboard,
term_config,
&window,
pane_id,
);
break;
}
InputEvent::Key(KeyEvent {
key: KeyCode::Backspace,
..
}) => {
state.filter_term.pop();
state.update_filter();
}
InputEvent::Key(KeyEvent {
key: KeyCode::Char(c),
..
}) => {
state.filter_term.push(c);
state.update_filter();
}
InputEvent::Key(KeyEvent {
key: KeyCode::UpArrow, key: KeyCode::UpArrow,
.. ..
}) => { }) => {
active_idx = active_idx.saturating_sub(1); state.active_idx = state.active_idx.saturating_sub(1);
} }
InputEvent::Key(KeyEvent { InputEvent::Key(KeyEvent {
key: KeyCode::Char('j'),
..
})
| InputEvent::Key(KeyEvent {
key: KeyCode::DownArrow, key: KeyCode::DownArrow,
.. ..
}) => { }) => {
active_idx = (active_idx + 1).min(entries.len() - 1); state.active_idx = (state.active_idx + 1).min(state.entries.len() - 1);
} }
InputEvent::Key(KeyEvent { InputEvent::Key(KeyEvent {
key: KeyCode::Escape, key: KeyCode::Escape,
@ -483,13 +575,13 @@ pub fn launcher(
InputEvent::Mouse(MouseEvent { InputEvent::Mouse(MouseEvent {
y, mouse_buttons, .. y, mouse_buttons, ..
}) => { }) => {
if y > 0 && y as usize <= entries.len() { if y > 0 && y as usize <= state.entries.len() {
active_idx = top_row + y as usize - 1; state.active_idx = state.top_row + y as usize - 1;
if mouse_buttons == MouseButtons::LEFT { if mouse_buttons == MouseButtons::LEFT {
launch( launch(
active_idx, state.active_idx,
&entries, &state.filtered_entries,
size, size,
mux_window_id, mux_window_id,
clipboard, clipboard,
@ -505,29 +597,13 @@ pub fn launcher(
break; break;
} }
} }
InputEvent::Key(KeyEvent {
key: KeyCode::Char(c),
..
}) if c >= '1' && c <= '9' => {
launch(
top_row + (c as u32 - '1' as u32) as usize,
&entries,
size,
mux_window_id,
clipboard,
term_config,
&window,
pane_id,
);
break;
}
InputEvent::Key(KeyEvent { InputEvent::Key(KeyEvent {
key: KeyCode::Enter, key: KeyCode::Enter,
.. ..
}) => { }) => {
launch( launch(
active_idx, state.active_idx,
&entries, &state.filtered_entries,
size, size,
mux_window_id, mux_window_id,
clipboard, clipboard,
@ -539,7 +615,7 @@ pub fn launcher(
} }
_ => {} _ => {}
} }
top_row = render(active_idx, &entries, &mut term)?; render(&mut state, &mut term)?;
} }
Ok(()) Ok(())