diff --git a/Cargo.lock b/Cargo.lock index 1b860f2..8d96576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "heck" version = "0.5.0" @@ -133,6 +142,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "proc-macro2" version = "1.0.83" @@ -156,6 +171,7 @@ name = "runner" version = "0.1.0" dependencies = [ "clap", + "fuzzy-matcher", "sdl2", ] @@ -199,6 +215,16 @@ dependencies = [ "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]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 270d636..f1bb880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] clap = { version = "4.5.4", features = ["derive"] } +fuzzy-matcher = "0.3.7" sdl2 = { version = "0.36.0", features = ["ttf"] } diff --git a/src/main.rs b/src/main.rs index 72dc8be..e9f44e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,11 +15,11 @@ mod config; mod utils; fn main() -> Result<(), Box> { - // let args = arguments::Arguments::parse(); + let args = arguments::Arguments::parse(); let executables = get_executables()?; - let mut runner = Runner::new(executables); + let mut runner = Runner::new(args.prompt, executables); if let Some(program) = runner.run() { run_program(program); diff --git a/src/runner/mod.rs b/src/runner/mod.rs index bd1f2fa..c5c243b 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use sdl2::{ event::Event, keyboard::Keycode, @@ -12,22 +13,24 @@ use sdl2::{ use crate::{ config::{ - BACKGROUND_COLOR, FONT_COLOR, FONT_POINT_SIZE, LINE_SPACING, MAX_ITEM_DISPLAY_COUNT, - PADDING, + BACKGROUND_COLOR, BACKGROUND_COLOR_SELECTED, FONT_COLOR, FONT_COLOR_SELECTED, + FONT_POINT_SIZE, LINE_SPACING, MAX_ITEM_DISPLAY_COUNT, PADDING, }, utils::color_from_hex, }; pub struct Runner { + prompt: String, executables: Vec, context: Sdl, canvas: Canvas, ttf: ttf::Sdl2TtfContext, input: String, + window_size: (u32, u32), } impl Runner { - pub fn new(executables: Vec) -> Self { + pub fn new(prompt: String, executables: Vec) -> Self { let context = sdl2::init().expect("Error creating SDL context"); let ttf = ttf::init().expect("Error creating SDL TTF context"); @@ -55,6 +58,7 @@ impl Runner { .expect("Error creating window"); window.set_opacity(0.0).unwrap(); + let window_size = window.size(); 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())); Self { + prompt, executables: cloned_executables, context, canvas, input: String::from(""), ttf, + window_size, } } pub fn run(&mut self) -> Option { + let matcher = SkimMatcherV2::default(); + + let mut selection_index: u16 = 0; let mut filtered_executables = self.executables.clone(); - self.canvas - .set_draw_color(color_from_hex(BACKGROUND_COLOR).unwrap()); + let background_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"); @@ -83,11 +92,16 @@ impl Runner { .load_font(font_path.clone(), FONT_POINT_SIZE) .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 mut event_pump = self.context.event_pump().unwrap(); - 'running: loop { + 'run: loop { + self.canvas.set_draw_color(background_color); self.canvas.clear(); for event in event_pump.poll_iter() { @@ -98,7 +112,7 @@ impl Runner { .. } => { self.input = String::from(""); - break 'running; + break 'run; } Event::KeyDown { keycode, .. } => { if let Some(key) = keycode { @@ -111,15 +125,44 @@ impl Runner { &self.input, &self.executables, &mut filtered_executables, + &matcher, ); + selection_index = 0; } } Keycode::Return => { // TODO: Improve this - if filtered_executables.len() > 0 { - self.input = filtered_executables[0].clone(); + let executables_len = filtered_executables.len(); + 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.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() { + if !self.input.is_empty() || !self.prompt.is_empty() { let surface = font - .render(&self.input) + .render(&format!("{}{}", &self.prompt, &self.input)) .blended(font_color) .expect("Error rendering text"); @@ -166,7 +209,11 @@ impl Runner { let surface = font .render(&filtered_executables[i as usize]) - .blended(font_color) + .blended(if i != selection_index { + font_color + } else { + font_color_selected + }) .expect("Error rendering text"); let rect = Rect::new( @@ -176,6 +223,17 @@ impl Runner { 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 .create_texture_from_surface(surface) .expect("Error creating texture"); @@ -200,10 +258,13 @@ fn filter_executables( input: &String, executables: &Vec, filtered_executables: &mut Vec, + matcher: &SkimMatcherV2, ) { *filtered_executables = executables .iter() - .filter(|e| (*e).starts_with(input)) + .filter(|e| matcher.fuzzy_indices(*e, input).is_some()) .map(|e| e.to_string()) .collect(); + + filtered_executables.sort_by(|a, b| b.starts_with(input).cmp(&a.starts_with(input))); }