From 926ea5ec3ac6580845617b8c36ece22b81f918d1 Mon Sep 17 00:00:00 2001 From: 409 Date: Tue, 21 May 2024 20:51:34 +0200 Subject: [PATCH] feat: a working run menu --- Cargo.lock | 54 ++++++++++++ Cargo.toml | 1 + src/config.rs | 12 +++ src/main.rs | 26 +++++- src/runner/mod.rs | 209 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 16 ++++ 6 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 src/config.rs create mode 100644 src/runner/mod.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index e84ceae..1b860f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.4" @@ -109,6 +121,18 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + [[package]] name = "proc-macro2" version = "1.0.83" @@ -132,6 +156,30 @@ name = "runner" version = "0.1.0" dependencies = [ "clap", + "sdl2", +] + +[[package]] +name = "sdl2" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8356b2697d1ead5a34f40bcc3c5d3620205fe0c7be0a14656223bfeec0258891" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29" +dependencies = [ + "cfg-if", + "libc", + "version-compare", ] [[package]] @@ -163,6 +211,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index cd75b90..270d636 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] clap = { version = "4.5.4", features = ["derive"] } +sdl2 = { version = "0.36.0", features = ["ttf"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6e5684f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,12 @@ +pub const MAX_ITEM_DISPLAY_COUNT: u16 = 10; +pub const PADDING: u16 = 8; +pub const LINE_SPACING: u16 = 2; +pub const FONT_POINT_SIZE: u16 = 16; + +pub const FONT_NAME: &str = "GeistMono Nerd Font"; + +pub const FONT_COLOR: &str = "#cdd6f4"; +pub const FONT_COLOR_SELECTED: &str = "#1e1e2e"; + +pub const BACKGROUND_COLOR: &str = "#1e1e2e"; +pub const BACKGROUND_COLOR_SELECTED: &str = "#89b4fa"; diff --git a/src/main.rs b/src/main.rs index 739ed65..72dc8be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,37 @@ -use std::{error::Error, process::{self, Stdio}}; +use std::{ + error::Error, + process::{self, Stdio}, +}; +#[allow(unused_imports)] use clap::Parser; use executables::get_executables; +use runner::Runner; mod arguments; mod executables; +mod runner; +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); + + if let Some(program) = runner.run() { + run_program(program); + } + Ok(()) } -fn run(program: impl ToString) { - let _ = process::Command::new(program.to_string()).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).spawn(); +fn run_program(program: impl ToString) { + let _ = process::Command::new(program.to_string()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn(); } diff --git a/src/runner/mod.rs b/src/runner/mod.rs new file mode 100644 index 0000000..bd1f2fa --- /dev/null +++ b/src/runner/mod.rs @@ -0,0 +1,209 @@ +use std::time::Duration; + +use sdl2::{ + event::Event, + keyboard::Keycode, + rect::Rect, + render::Canvas, + ttf::{self}, + video::Window, + Sdl, +}; + +use crate::{ + config::{ + BACKGROUND_COLOR, FONT_COLOR, FONT_POINT_SIZE, LINE_SPACING, MAX_ITEM_DISPLAY_COUNT, + PADDING, + }, + utils::color_from_hex, +}; + +pub struct Runner { + executables: Vec, + context: Sdl, + canvas: Canvas, + ttf: ttf::Sdl2TtfContext, + input: String, +} + +impl Runner { + pub fn new(executables: Vec) -> Self { + let context = sdl2::init().expect("Error creating SDL context"); + + let ttf = ttf::init().expect("Error creating SDL TTF context"); + + let window_height: u32; + { + let font_path = String::from("/usr/share/fonts/OTF/GeistMonoNerdFontMono-Regular.otf"); + + let font = ttf + .load_font(font_path.clone(), FONT_POINT_SIZE) + .expect(&format!("Error loading font {}", font_path)); + + window_height = (PADDING + + ((font.height() as u16 + LINE_SPACING) * (1 + MAX_ITEM_DISPLAY_COUNT))) + .into(); + } + + let video = context.video().expect("Error initializing SDL video"); + + let mut window = video + .window("Practical runner", 480, window_height) + .position_centered() + .borderless() + .build() + .expect("Error creating window"); + + window.set_opacity(0.0).unwrap(); + + let canvas = window.into_canvas().build().expect("Error creating canvas"); + + let mut cloned_executables = executables.clone(); + cloned_executables.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + + Self { + executables: cloned_executables, + context, + canvas, + input: String::from(""), + ttf, + } + } + + pub fn run(&mut self) -> Option { + let mut filtered_executables = self.executables.clone(); + + self.canvas + .set_draw_color(color_from_hex(BACKGROUND_COLOR).unwrap()); + + let font_path = String::from("/usr/share/fonts/OTF/GeistMonoNerdFontMono-Regular.otf"); + + let font = self + .ttf + .load_font(font_path.clone(), FONT_POINT_SIZE) + .expect(&format!("Error loading font {}", font_path)); + + let creator = self.canvas.texture_creator(); + + let mut event_pump = self.context.event_pump().unwrap(); + + 'running: loop { + self.canvas.clear(); + + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => { + self.input = String::from(""); + break 'running; + } + Event::KeyDown { keycode, .. } => { + if let Some(key) = keycode { + match key { + Keycode::Backspace => { + if !self.input.is_empty() { + self.input.pop(); + + filter_executables( + &self.input, + &self.executables, + &mut filtered_executables, + ); + } + } + Keycode::Return => { + // TODO: Improve this + if filtered_executables.len() > 0 { + self.input = filtered_executables[0].clone(); + } + break 'running; + } + _ => (), + } + } + } + Event::TextInput { text, .. } => { + self.input += &text; + + filter_executables( + &self.input, + &self.executables, + &mut filtered_executables, + ); + } + _ => {} + } + } + + let font_color = color_from_hex(FONT_COLOR).expect("Error loading FONT_COLOR"); + + if !self.input.is_empty() { + let surface = font + .render(&self.input) + .blended(font_color) + .expect("Error rendering text"); + + let rect = Rect::new( + PADDING.into(), + PADDING.into(), + surface.width(), + surface.height(), + ); + + let texture = creator + .create_texture_from_surface(surface) + .expect("Error creating texture"); + + let _ = self.canvas.copy(&texture, None, Some(rect)); + } + + let item_count = MAX_ITEM_DISPLAY_COUNT.min(filtered_executables.len() as u16); + for i in 0..item_count { + let offset = PADDING + (font.height() as u16 + LINE_SPACING) * (i + 1); + + let surface = font + .render(&filtered_executables[i as usize]) + .blended(font_color) + .expect("Error rendering text"); + + let rect = Rect::new( + PADDING.into(), + offset.into(), + surface.width(), + surface.height(), + ); + + let texture = creator + .create_texture_from_surface(surface) + .expect("Error creating texture"); + + let _ = self.canvas.copy(&texture, None, Some(rect)); + } + + self.canvas.present(); + + std::thread::sleep(Duration::from_millis(8)) + } + + if self.input.is_empty() { + None + } else { + Some(self.input.clone()) + } + } +} + +fn filter_executables( + input: &String, + executables: &Vec, + filtered_executables: &mut Vec, +) { + *filtered_executables = executables + .iter() + .filter(|e| (*e).starts_with(input)) + .map(|e| e.to_string()) + .collect(); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..2f07ffe --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,16 @@ +use sdl2::pixels::Color; + +#[derive(Debug, Clone)] +pub struct ColorParseError; + +pub fn color_from_hex(hex: &str) -> Result { + if hex.len() != 7 || hex.chars().nth(0).unwrap() != '#' { + return Err(ColorParseError); + } + + let r = u8::from_str_radix(&hex[1..3], 16).or(Err(ColorParseError))?; + let g = u8::from_str_radix(&hex[3..5], 16).or(Err(ColorParseError))?; + let b = u8::from_str_radix(&hex[5..7], 16).or(Err(ColorParseError))?; + + Ok(Color::RGB(r, g, b)) +}