// 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);
}
}