// This file is part of Deja-vu. // // Deja-vu is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Deja-vu is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Deja-vu. If not, see . // use ratatui::widgets::ListState; use serde::Deserialize; use std::{ env, error, fs, io, path::PathBuf, process::{Command, Output}, thread::{self, JoinHandle}, }; /// Application result type. #[allow(clippy::module_name_repetitions)] pub type AppResult = std::result::Result>; /// Application exit reasons #[derive(Debug, Default)] pub enum QuitReason { #[default] Exit, Poweroff, Restart, } /// Config #[derive(Debug, Deserialize)] pub struct Config { con_emu: String, tools: Vec, } impl Config { /// # Panics /// /// Will panic for many reasons #[must_use] pub fn load() -> Option { // Main config let exe_path = env::current_exe().expect("Failed to find main executable"); let contents = fs::read_to_string(exe_path.with_file_name("pe-menu.toml")) .expect("Failed to load config file"); let mut new_config: Config = toml::from_str(&contents).expect("Failed to parse config file"); // Tools let tool_config_path = exe_path.parent().unwrap().join("menu_entries"); let mut entries: Vec = std::fs::read_dir(tool_config_path) .expect("Failed to find any tool configs") .map(|res| res.map(|e| e.path())) .filter_map(Result::ok) .collect(); entries.sort(); for entry in entries { let contents = fs::read_to_string(&entry).expect("Failed to read tool config file"); let tool: Tool = toml::from_str(&contents).expect("Failed to parse tool config file"); new_config.tools.push(tool); } // Done Some(new_config) } } /// `PopUp` #[derive(Debug, Clone, PartialEq)] pub struct PopUp { pub title: String, pub body: String, } impl PopUp { #[must_use] pub fn new(title: &str, body: &str) -> PopUp { PopUp { title: String::from(title), body: String::from(body), } } } /// `Tool` #[derive(Debug, Deserialize)] pub struct Tool { name: String, command: String, args: Option>, use_conemu: bool, separator: bool, } /// `MenuEntry` #[derive(Default, Debug, Clone, PartialEq)] pub struct MenuEntry { pub name: String, pub command: String, pub args: Vec, pub use_conemu: bool, pub separator: bool, } impl MenuEntry { #[must_use] pub fn new( name: &str, command: &str, args: Option>, use_conemu: bool, separator: bool, ) -> MenuEntry { let mut my_args = Vec::new(); if let Some(a) = args { my_args.clone_from(&a); } MenuEntry { name: String::from(name), command: String::from(command), args: my_args, use_conemu, separator, } } } /// `StatefulList` #[derive(Default, Debug, Clone, PartialEq)] pub struct StatefulList { pub state: ListState, pub items: Vec, pub last_selected: Option, } impl StatefulList { #[must_use] pub fn new() -> StatefulList { StatefulList { state: ListState::default(), items: Vec::new(), last_selected: None, } } #[must_use] pub fn get_selected(&self) -> Option<&T> { if let Some(i) = self.state.selected() { self.items.get(i) } else { None } } pub fn pop_selected(&mut self) -> Option { if let Some(i) = self.state.selected() { Some(self.items[i].clone()) } else { None } } fn select_first_item(&mut self) { if self.items.is_empty() { self.state.select(None); } else { self.state.select(Some(0)); } self.last_selected = None; } pub fn set_items(&mut self, items: Vec) { // Clear list and rebuild with provided items self.items.clear(); for item in items { self.items.push(item); } // Reset state and select first item (if available) self.state = ListState::default(); self.select_first_item(); } pub fn next(&mut self) { if self.items.is_empty() { return; } let i = match self.state.selected() { Some(i) => { if i >= self.items.len() - 1 { 0 } else { i + 1 } } None => self.last_selected.unwrap_or(0), }; self.state.select(Some(i)); } pub fn previous(&mut self) { if self.items.is_empty() { return; } let i = match self.state.selected() { Some(i) => { if i == 0 { self.items.len() - 1 } else { i - 1 } } None => self.last_selected.unwrap_or(0), }; self.state.select(Some(i)); } } /// Application. #[derive(Debug)] pub struct App { pub config: Config, pub main_menu: StatefulList, pub popup: Option, pub quit_reason: QuitReason, pub running: bool, pub thread_pool: Vec>>, } impl Default for App { fn default() -> Self { let config = Config::load(); Self { config: config.unwrap(), running: true, quit_reason: QuitReason::Exit, main_menu: StatefulList::new(), popup: None, thread_pool: Vec::new(), } } } impl App { /// Constructs a new instance of [`App`]. #[must_use] pub fn new() -> Self { let mut app = Self::default(); // Add MenuEntries for tool in &app.config.tools { app.main_menu.items.push(MenuEntry::new( &tool.name, &tool.command, tool.args.clone(), tool.use_conemu, tool.separator, )); } app.main_menu.select_first_item(); // Done app } /// Handles the tick event of the terminal. pub fn tick(&self) {} /// Actually exit application /// /// # Errors /// # Panics /// /// Will panic if wpeutil fails to reboot or shutdown pub fn exit(&self) -> Result<(), &'static str> { let mut argument: Option = None; match self.quit_reason { QuitReason::Exit => {} QuitReason::Poweroff => argument = Some(String::from("shutdown")), QuitReason::Restart => argument = Some(String::from("reboot")), } if let Some(a) = argument { Command::new("wpeutil") .arg(a) .output() .expect("Failed to run exit command"); } Ok(()) } /// Set running to false to quit the application. pub fn quit(&mut self, reason: QuitReason) { self.running = false; self.quit_reason = reason; } /// # Panics /// /// Will panic if command fails to run pub fn open_terminal(&mut self) { Command::new("cmd.exe") .arg("-new_console:n") .output() .expect("Failed to run command"); } /// # Panics /// /// Will panic if menu entry isn't found pub fn run_tool(&mut self) { // Command let tool: &MenuEntry; if let Some(index) = self.main_menu.state.selected() { tool = &self.main_menu.items[index]; } else { self.popup = Some(PopUp::new( "Failed to find menu entry", "Check for an updated version of Deja-Vu", )); return; } let command = if tool.use_conemu { self.config.con_emu.clone() } else { tool.command.clone() }; // Separators if tool.separator { return; } // Args let mut args = tool.args.clone(); if tool.use_conemu { args.insert(0, tool.command.clone()); args.push(String::from("-new_console:n")); } // Check path let command_path = PathBuf::from(&command); if let Ok(true) = command_path.try_exists() { // File path exists } else { // File path doesn't exist or is a broken symlink/etc // The latter case would be Ok(false) rather than Err(_) self.popup = Some(PopUp::new("Tool Missing", &format!("Tool path: {command}"))); return; } // Run // TODO: This really needs refactored to use channels so we can properly check if the // command fails. let new_thread = thread::spawn(move || Command::new(command_path).args(args).output()); self.thread_pool.push(new_thread); } }