feat: selection (tab completion), visual improvements
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user