feat: selection (tab completion), visual improvements

This commit is contained in:
2024-05-21 21:46:41 +02:00
parent 926ea5ec3a
commit df1d4c6398
4 changed files with 106 additions and 18 deletions

26
Cargo.lock generated
View File

@@ -109,6 +109,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[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 = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -133,6 +142,12 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.83" version = "1.0.83"
@@ -156,6 +171,7 @@ name = "runner"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"fuzzy-matcher",
"sdl2", "sdl2",
] ]
@@ -199,6 +215,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"

View File

@@ -8,4 +8,5 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
fuzzy-matcher = "0.3.7"
sdl2 = { version = "0.36.0", features = ["ttf"] } sdl2 = { version = "0.36.0", features = ["ttf"] }

View File

@@ -15,11 +15,11 @@ mod config;
mod utils; mod utils;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
// let args = arguments::Arguments::parse(); let args = arguments::Arguments::parse();
let executables = get_executables()?; let executables = get_executables()?;
let mut runner = Runner::new(executables); let mut runner = Runner::new(args.prompt, executables);
if let Some(program) = runner.run() { if let Some(program) = runner.run() {
run_program(program); run_program(program);

View File

@@ -1,5 +1,6 @@
use std::time::Duration; use std::time::Duration;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use sdl2::{ use sdl2::{
event::Event, event::Event,
keyboard::Keycode, keyboard::Keycode,
@@ -12,22 +13,24 @@ use sdl2::{
use crate::{ use crate::{
config::{ config::{
BACKGROUND_COLOR, FONT_COLOR, FONT_POINT_SIZE, LINE_SPACING, MAX_ITEM_DISPLAY_COUNT, BACKGROUND_COLOR, BACKGROUND_COLOR_SELECTED, FONT_COLOR, FONT_COLOR_SELECTED,
PADDING, FONT_POINT_SIZE, LINE_SPACING, MAX_ITEM_DISPLAY_COUNT, PADDING,
}, },
utils::color_from_hex, utils::color_from_hex,
}; };
pub struct Runner { pub struct Runner {
prompt: String,
executables: Vec<String>, executables: Vec<String>,
context: Sdl, context: Sdl,
canvas: Canvas<Window>, canvas: Canvas<Window>,
ttf: ttf::Sdl2TtfContext, ttf: ttf::Sdl2TtfContext,
input: String, input: String,
window_size: (u32, u32),
} }
impl Runner { impl Runner {
pub fn new(executables: Vec<String>) -> Self { pub fn new(prompt: String, executables: Vec<String>) -> Self {
let context = sdl2::init().expect("Error creating SDL context"); let context = sdl2::init().expect("Error creating SDL context");
let ttf = ttf::init().expect("Error creating SDL TTF context"); let ttf = ttf::init().expect("Error creating SDL TTF context");
@@ -55,6 +58,7 @@ impl Runner {
.expect("Error creating window"); .expect("Error creating window");
window.set_opacity(0.0).unwrap(); window.set_opacity(0.0).unwrap();
let window_size = window.size();
let canvas = window.into_canvas().build().expect("Error creating canvas"); let canvas = window.into_canvas().build().expect("Error creating canvas");
@@ -62,19 +66,24 @@ impl Runner {
cloned_executables.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); cloned_executables.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
Self { Self {
prompt,
executables: cloned_executables, executables: cloned_executables,
context, context,
canvas, canvas,
input: String::from(""), input: String::from(""),
ttf, ttf,
window_size,
} }
} }
pub fn run(&mut self) -> Option<String> { pub fn run(&mut self) -> Option<String> {
let matcher = SkimMatcherV2::default();
let mut selection_index: u16 = 0;
let mut filtered_executables = self.executables.clone(); let mut filtered_executables = self.executables.clone();
self.canvas let background_color = color_from_hex(BACKGROUND_COLOR).unwrap();
.set_draw_color(color_from_hex(BACKGROUND_COLOR).unwrap()); let background_color_selected = color_from_hex(BACKGROUND_COLOR_SELECTED).unwrap();
let font_path = String::from("/usr/share/fonts/OTF/GeistMonoNerdFontMono-Regular.otf"); let font_path = String::from("/usr/share/fonts/OTF/GeistMonoNerdFontMono-Regular.otf");
@@ -83,11 +92,16 @@ impl Runner {
.load_font(font_path.clone(), FONT_POINT_SIZE) .load_font(font_path.clone(), FONT_POINT_SIZE)
.expect(&format!("Error loading font {}", font_path)); .expect(&format!("Error loading font {}", font_path));
let font_color = color_from_hex(FONT_COLOR).expect("Error loading FONT_COLOR");
let font_color_selected =
color_from_hex(FONT_COLOR_SELECTED).expect("Error loading FONT_COLOR_SELECTED");
let creator = self.canvas.texture_creator(); let creator = self.canvas.texture_creator();
let mut event_pump = self.context.event_pump().unwrap(); let mut event_pump = self.context.event_pump().unwrap();
'running: loop { 'run: loop {
self.canvas.set_draw_color(background_color);
self.canvas.clear(); self.canvas.clear();
for event in event_pump.poll_iter() { for event in event_pump.poll_iter() {
@@ -98,7 +112,7 @@ impl Runner {
.. ..
} => { } => {
self.input = String::from(""); self.input = String::from("");
break 'running; break 'run;
} }
Event::KeyDown { keycode, .. } => { Event::KeyDown { keycode, .. } => {
if let Some(key) = keycode { if let Some(key) = keycode {
@@ -111,15 +125,44 @@ impl Runner {
&self.input, &self.input,
&self.executables, &self.executables,
&mut filtered_executables, &mut filtered_executables,
&matcher,
); );
selection_index = 0;
} }
} }
Keycode::Return => { Keycode::Return => {
// TODO: Improve this // TODO: Improve this
if filtered_executables.len() > 0 { let executables_len = filtered_executables.len();
self.input = filtered_executables[0].clone(); if executables_len > 0 {
self.input = filtered_executables
[executables_len.min(selection_index.into()) as usize]
.clone();
}
break 'run;
}
Keycode::Down => {
if selection_index < (filtered_executables.len() - 1) as u16 {
selection_index += 1;
}
}
Keycode::Up => {
if selection_index > 0 {
selection_index -= 1;
}
}
Keycode::Tab => {
if filtered_executables.len() > 0 {
self.input =
filtered_executables[selection_index as usize].clone();
filter_executables(
&self.input,
&self.executables,
&mut filtered_executables,
&matcher,
);
selection_index = 0;
} }
break 'running;
} }
_ => (), _ => (),
} }
@@ -132,17 +175,17 @@ impl Runner {
&self.input, &self.input,
&self.executables, &self.executables,
&mut filtered_executables, &mut filtered_executables,
&matcher,
); );
selection_index = 0;
} }
_ => {} _ => {}
} }
} }
let font_color = color_from_hex(FONT_COLOR).expect("Error loading FONT_COLOR"); if !self.input.is_empty() || !self.prompt.is_empty() {
if !self.input.is_empty() {
let surface = font let surface = font
.render(&self.input) .render(&format!("{}{}", &self.prompt, &self.input))
.blended(font_color) .blended(font_color)
.expect("Error rendering text"); .expect("Error rendering text");
@@ -166,7 +209,11 @@ impl Runner {
let surface = font let surface = font
.render(&filtered_executables[i as usize]) .render(&filtered_executables[i as usize])
.blended(font_color) .blended(if i != selection_index {
font_color
} else {
font_color_selected
})
.expect("Error rendering text"); .expect("Error rendering text");
let rect = Rect::new( let rect = Rect::new(
@@ -176,6 +223,17 @@ impl Runner {
surface.height(), surface.height(),
); );
let background_rect =
Rect::new(0, offset.into(), self.window_size.0, surface.height());
self.canvas.set_draw_color(if i != selection_index {
background_color
} else {
background_color_selected
});
let _ = self.canvas.fill_rect(background_rect);
let texture = creator let texture = creator
.create_texture_from_surface(surface) .create_texture_from_surface(surface)
.expect("Error creating texture"); .expect("Error creating texture");
@@ -200,10 +258,13 @@ fn filter_executables(
input: &String, input: &String,
executables: &Vec<String>, executables: &Vec<String>,
filtered_executables: &mut Vec<String>, filtered_executables: &mut Vec<String>,
matcher: &SkimMatcherV2,
) { ) {
*filtered_executables = executables *filtered_executables = executables
.iter() .iter()
.filter(|e| (*e).starts_with(input)) .filter(|e| matcher.fuzzy_indices(*e, input).is_some())
.map(|e| e.to_string()) .map(|e| e.to_string())
.collect(); .collect();
filtered_executables.sort_by(|a, b| b.starts_with(input).cmp(&a.starts_with(input)));
} }