366 lines
9.5 KiB
Rust
366 lines
9.5 KiB
Rust
// 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 <https://www.gnu.org/licenses/>.
|
|
//
|
|
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<T> = std::result::Result<T, Box<dyn error::Error>>;
|
|
|
|
/// 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<Tool>,
|
|
}
|
|
|
|
impl Config {
|
|
/// # Panics
|
|
///
|
|
/// Will panic for many reasons
|
|
#[must_use]
|
|
pub fn load() -> Option<Config> {
|
|
// 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<PathBuf> = 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<Vec<String>>,
|
|
use_conemu: bool,
|
|
separator: bool,
|
|
}
|
|
|
|
/// `MenuEntry`
|
|
#[derive(Default, Debug, Clone, PartialEq)]
|
|
pub struct MenuEntry {
|
|
pub name: String,
|
|
pub command: String,
|
|
pub args: Vec<String>,
|
|
pub use_conemu: bool,
|
|
pub separator: bool,
|
|
}
|
|
|
|
impl MenuEntry {
|
|
#[must_use]
|
|
pub fn new(
|
|
name: &str,
|
|
command: &str,
|
|
args: Option<Vec<String>>,
|
|
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<T> {
|
|
pub state: ListState,
|
|
pub items: Vec<T>,
|
|
pub last_selected: Option<usize>,
|
|
}
|
|
|
|
impl<T: Clone> StatefulList<T> {
|
|
#[must_use]
|
|
pub fn new() -> StatefulList<T> {
|
|
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<T> {
|
|
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<T>) {
|
|
// 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<MenuEntry>,
|
|
pub popup: Option<PopUp>,
|
|
pub quit_reason: QuitReason,
|
|
pub running: bool,
|
|
pub thread_pool: Vec<JoinHandle<Result<Output, io::Error>>>,
|
|
}
|
|
|
|
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<String> = 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);
|
|
}
|
|
}
|