use iced::{ checkbox, keyboard, text_input, Application, Checkbox, Column, Command, Container, Element, Length, Row, Subscription, Text, TextInput, }; use iced_native::{subscription, window, Event}; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum ProgramType { Native, Flatpak, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct Program { path: String, name: String, ptype: ProgramType, } pub struct State { input: text_input::State, input_value: String, flatpak_only: bool, cursor: usize, page: usize, epp: usize, // Entries per page programs: Vec, filtered_programs: Vec, } #[derive(Clone, Debug)] pub enum Message { InputChanged(String), FlatpakOnlyChanged(bool), ToggleFlatpakOnly, MoveCursor(i32), Execute, Exit, } impl Default for State { fn default() -> Self { // Paths to include programs from let mut nativepaths = vec![]; let mut flatpakpaths = vec![]; // Programs let mut programs = vec![]; // Add program locations from PATH environment variable match std::env::var("PATH") { Ok(path) => { for path in path.split(':') { let mut path = PathBuf::from(path); // Resolve symlinks if path.is_symlink() { path = path.canonicalize().unwrap(); } nativepaths.push(PathBuf::from(path)); } } Err(_) => { println!("Could not get PATH environment variable. Using common paths."); nativepaths.push(PathBuf::from("/usr/local/bin")); nativepaths.push(PathBuf::from("/usr/bin")); nativepaths.push(PathBuf::from("/bin")); } } // Add programs to vec for path in nativepaths.clone() { for binary in std::fs::read_dir(&path).unwrap() { let binary = binary.unwrap(); programs.push(Program { path: binary.path().into_os_string().into_string().unwrap(), name: binary.file_name().into_string().unwrap(), ptype: ProgramType::Native, }); } } // Location of flatpak programs flatpakpaths.push(PathBuf::from("/var/lib/flatpak/exports/bin")); // Add programs to vec for path in flatpakpaths.clone() { for binary in std::fs::read_dir(&path).unwrap() { let binary = binary.unwrap(); programs.push(Program { path: binary.path().into_os_string().into_string().unwrap(), name: binary.file_name().into_string().unwrap(), ptype: ProgramType::Flatpak, }); } } // Sort alphabetically and remove duplicates from programs programs.sort(); programs.dedup(); State { input: text_input::State::focused(), input_value: String::new(), flatpak_only: false, cursor: 0, page: 0, epp: 10, programs: programs.clone(), filtered_programs: programs, } } } impl Application for State { type Executor = iced::executor::Default; type Message = Message; type Flags = (); fn new(_flags: ()) -> (Self, Command) { (State::default(), Command::none()) } fn title(&self) -> String { String::from("Application Launcher") } fn view(&mut self) -> Element { let input = TextInput::new( &mut self.input, "search", &mut self.input_value, Message::InputChanged, ) .padding(10) .size(20); let flatpak_only = Checkbox::new(self.flatpak_only, "", Message::FlatpakOnlyChanged).size(40); let program_list: Element = { self.filtered_programs .iter() .skip(self.page * self.epp) .take(self.epp) .enumerate() .fold(Column::new(), |column, (i, program)| { // Assign binary to everything after the last slash let binary = program .path .split('/') .last() .unwrap_or(&program.path) .to_string(); if (i + (self.page * self.epp)) == self.cursor { column.push(Element::new(Text::new(binary).color([1.0, 0.5, 0.0]))) } else { column.push(Element::new(Text::new(binary))) } }) .into() }; let search_row = Row::new().push(input).push(flatpak_only); let content = Column::new() .padding(20) .spacing(5) .push(search_row) .push(program_list); Container::new(content) .width(Length::Fill) .height(Length::Fill) .into() } fn update(&mut self, message: Message) -> Command { match message { Message::InputChanged(value) => { self.input_value = value; // Trim whitespace. This is a bit of a hack, but it works. // Space is used for toggling flatpak_only, but it also places // a space in the input, which we do not want. This trims it off. self.input_value = self.input_value.trim().to_string(); self.filter_programs(); } Message::FlatpakOnlyChanged(value) => { self.flatpak_only = value; self.filter_programs(); } Message::ToggleFlatpakOnly => { self.flatpak_only = !self.flatpak_only; self.filter_programs(); } Message::MoveCursor(md) => match md { 1 => { if self.cursor > 0 { if self.cursor % self.epp == 0 { self.page -= 1; } self.cursor -= 1; } } 0 => { if self.cursor < self.filtered_programs.len() - 1 { if (self.cursor + 1) % self.epp == 0 { self.page += 1; } self.cursor += 1; } } _ => {} }, Message::Execute => { let program = self.filtered_programs.get(self.cursor); if let Some(program) = program { exec(&program.path); } } Message::Exit => std::process::exit(0), } Command::none() } fn subscription(&self) -> Subscription { subscription::events_with(|event, _status| match event { Event::Keyboard(keyboard::Event::KeyPressed { modifiers: _, key_code, }) => process_key(key_code), Event::Window(window::Event::Unfocused) => Some(Message::Exit), // Exit on focus lost. _ => None, }) } } impl State { fn filter_programs(&mut self) { if self.flatpak_only { self.filtered_programs .retain(|program| program.ptype == ProgramType::Flatpak); } else { self.filtered_programs = self .programs .iter() .filter(|program| program.name.contains(self.input_value.as_str())) .cloned() .collect(); } self.cursor = 0; self.page = 0; } } fn process_key(key_code: keyboard::KeyCode) -> Option { match key_code { keyboard::KeyCode::Up => Some(Message::MoveCursor(1)), keyboard::KeyCode::Down | keyboard::KeyCode::Tab => Some(Message::MoveCursor(0)), keyboard::KeyCode::Space => Some(Message::ToggleFlatpakOnly), keyboard::KeyCode::Enter => Some(Message::Execute), keyboard::KeyCode::Escape => Some(Message::Exit), _ => None, } } fn exec(path: &str) { // We don't care if the application runs or not, out job is done at this point. let _ = std::process::Command::new(path) .spawn() .expect("Failed to execute"); // Bye! std::process::exit(0); }