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<Program>,
filtered_programs: Vec<Program>,
}
#[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<Message>) {
(State::default(), Command::none())
}
fn title(&self) -> String {
String::from("Application Launcher")
}
fn view(&mut self) -> Element<Message> {
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<Message> = {
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<Message> {
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<Message> {
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<Message> {
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);
}