diff --git a/Cargo.lock b/Cargo.lock index 71b9463..b63888c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2024,12 +2024,20 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" name = "pe-menu" version = "0.2.0" dependencies = [ + "anyhow", + "clap", + "color-eyre", + "core", "crossterm", "futures", "ratatui", "serde", "tokio", "toml", + "tracing", + "tracing-error", + "tracing-subscriber", + "vergen-gix", ] [[package]] diff --git a/config/config.json5 b/config/config.json5 index f4bd9e2..0d54fcf 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -1,21 +1,28 @@ { "app_title": "Deja-vu", "clone_app_path": "C:/Program Files/Some Clone Tool/app.exe", + "conemu_path": "C:/Program Files/ConEmu/ConEmu64.exe", "keybindings": { + "Home": { + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" + }, "ScanDisks": { - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "InstallDrivers": { "": "Process", "": "KeyUp", "": "KeyDown", - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "SelectDisks": { "": "InstallDriver", @@ -23,70 +30,79 @@ "": "Process", "": "KeyUp", "": "KeyDown", - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "SelectTableType": { "": "PrevScreen", "": "Process", "": "KeyUp", "": "KeyDown", - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "Confirm": { "": "PrevScreen", "": "Process", - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "PreClone": { - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "Clone": { - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "SelectParts": { "": "Process", "": "KeyUp", "": "KeyDown", - "": "ScanDisks", // Start over - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "ScanDisks", + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "PostClone": { - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "Done": { "": "Quit", - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, "Failed": { "": "Quit", - "": "Quit", // Quit the application - "": "Quit", // Another way to quit - "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" }, - } + "PEMenu": { + "": "Process", + "": "KeyUp", + "": "KeyDown", + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" + }, + }, } diff --git a/core/src/action.rs b/core/src/action.rs index 48257c4..06c937a 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -29,7 +29,11 @@ pub enum Action { SelectRight(Option, Option), // indicies for right info pane UpdateDiskList(Vec), UpdateFooter(String), - UpdateLeft(String, Vec, Vec, bool), // (title, labels, items, select_one) + UpdateLeft(String, Vec, Vec, usize), // (title, labels, items, select_num) + // NOTE: select_num should be set to 0, 1, or 2. + // 0: For repeating selections + // 1: For a single choice + // 2: For two selections (obviously) UpdateRight(Vec>, usize, Vec>), // (labels, start_index, items) - items before start are always shown // Screens DismissPopup, diff --git a/core/src/components/left.rs b/core/src/components/left.rs index beff17a..0f1c833 100644 --- a/core/src/components/left.rs +++ b/core/src/components/left.rs @@ -30,7 +30,7 @@ pub struct Left { config: Config, labels: Vec, list: StatefulList, - select_one: bool, + select_num: usize, selections: Vec>, selections_saved: Vec>, title_text: String, @@ -39,7 +39,7 @@ pub struct Left { impl Left { pub fn new() -> Self { Self { - select_one: false, + select_num: 0, labels: vec![String::from("one"), String::from("two")], selections: vec![None, None], selections_saved: vec![None, None], @@ -75,12 +75,14 @@ impl Component for Left { Action::KeyUp => self.list.previous(), Action::KeyDown => self.list.next(), Action::Process => { - if let Some(command_tx) = self.command_tx.clone() { + if self.select_num == 0 { + // Selections aren't being used so this is a no-op + } else if let Some(command_tx) = self.command_tx.clone() { match (self.selections[0], self.selections[1]) { (None, None) => { // Making first selection command_tx.send(Action::Select(self.list.selected(), None))?; - if self.select_one { + if self.select_num == 1 { // Confirm selection command_tx.send(Action::NextScreen)?; } @@ -115,14 +117,14 @@ impl Component for Left { self.selections[0] = None; self.selections[1] = None; } - Action::UpdateLeft(title, labels, items, select_one) => { + Action::UpdateLeft(title, labels, items, select_num) => { self.title_text = title; self.labels = labels .iter() .map(|label| format!(" ~{}~", label.to_lowercase())) .collect(); self.list.set_items(items); - self.select_one = select_one; + self.select_num = select_num; } _ => {} } @@ -136,7 +138,7 @@ impl Component for Left { .areas(area); // Title - let title_text = if self.selections[1].is_some() || self.select_one { + let title_text = if self.selections[1].is_some() || self.select_num == 1 { "Confirm Selections" } else { self.title_text.as_str() diff --git a/core/src/components/right.rs b/core/src/components/right.rs index 80622c2..569d0a1 100644 --- a/core/src/components/right.rs +++ b/core/src/components/right.rs @@ -154,8 +154,8 @@ impl Component for Right { label .iter() .for_each(|dv| body_text.push(dv.as_line().bold())); + body_text.push(Line::from("")); } - body_text.push(Line::from("")); first_desc .iter() .for_each(|dv| body_text.push(dv.as_line())); diff --git a/core/src/config.rs b/core/src/config.rs index 9599e0d..49eb824 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -44,6 +44,8 @@ pub struct Config { pub app_title: String, #[serde(default)] pub clone_app_path: PathBuf, + #[serde(default)] + pub conemu_path: PathBuf, #[serde(default, flatten)] pub config: AppConfig, #[serde(default)] @@ -76,6 +78,10 @@ impl Config { "clone_app_path", String::from("C:\\Program Files\\Some Clone Tool\\app.exe"), )? + .set_default( + "conemu_path", + String::from("C:\\Program Files\\ConEmu\\ConEmu64.exe"), + )? .set_default("config_dir", config_dir.to_str().unwrap())? .set_default("data_dir", data_dir.to_str().unwrap())?; @@ -529,7 +535,7 @@ mod tests { let c = Config::new()?; assert_eq!( c.keybindings - .get(&Mode::ScanDisks) // i.e. Home + .get(&Mode::Home) // i.e. Home .unwrap() .get(&parse_key_sequence("").unwrap_or_default()) .unwrap(), diff --git a/core/src/logging.rs b/core/src/logging.rs index 2e8b40d..741e981 100644 --- a/core/src/logging.rs +++ b/core/src/logging.rs @@ -21,7 +21,7 @@ use crate::config; lazy_static::lazy_static! { pub static ref LOG_ENV: String = format!("{}_LOG_LEVEL", config::PROJECT_NAME); - pub static ref LOG_FILE: String = format!("{}.log", config::PROJECT_NAME); + pub static ref LOG_FILE: String = format!("{}.log", config::PROJECT_NAME.to_lowercase()); //pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); } diff --git a/core/src/state.rs b/core/src/state.rs index b3219b0..fab9ede 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -25,7 +25,12 @@ use crate::system::{ #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Mode { + // Core #[default] + Home, + Done, + Failed, + // Clone ScanDisks, InstallDrivers, SelectDisks, @@ -35,8 +40,8 @@ pub enum Mode { Clone, SelectParts, PostClone, - Done, - Failed, + // WinPE + PEMenu, } #[derive(Debug, Default)] diff --git a/deja_vu/src/app.rs b/deja_vu/src/app.rs index c235f3a..f2ab74d 100644 --- a/deja_vu/src/app.rs +++ b/deja_vu/src/app.rs @@ -69,8 +69,7 @@ impl App { pub fn new(tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); let disk_list_arc = Arc::new(Mutex::new(Vec::new())); - let mut tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); - tasks.add(Task::ScanDisks); + let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); Ok(Self { // TUI action_rx, @@ -91,9 +90,9 @@ impl App { tick_rate, // App clone: CloneSettings::new(disk_list_arc), - cur_mode: Mode::ScanDisks, + cur_mode: Mode::default(), list: StatefulList::default(), - prev_mode: Mode::ScanDisks, + prev_mode: Mode::default(), selections: vec![None, None], tasks, }) @@ -101,24 +100,28 @@ impl App { pub fn prev_mode(&mut self) -> Option { let new_mode = match self.cur_mode { + Mode::Home => Some(Mode::Home), Mode::Failed => Some(Mode::Failed), Mode::Done => Some(Mode::Done), Mode::SelectParts => Some(Mode::SelectParts), Mode::Confirm => Some(Mode::SelectTableType), Mode::SelectTableType => Some(Mode::SelectDisks), Mode::SelectDisks => Some(Mode::ScanDisks), - // + // Disallowed moves Mode::InstallDrivers | Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => None, + // Invalid states + Mode::PEMenu => panic!("This shouldn't happen?"), }; new_mode } pub fn next_mode(&mut self) -> Option { let new_mode = match self.cur_mode { + Mode::Home => Mode::ScanDisks, Mode::InstallDrivers => Mode::ScanDisks, Mode::ScanDisks => Mode::SelectDisks, Mode::SelectDisks => Mode::SelectTableType, @@ -129,6 +132,8 @@ impl App { Mode::SelectParts => Mode::PostClone, Mode::PostClone | Mode::Done => Mode::Done, Mode::Failed => Mode::Failed, + // Invalid states + Mode::PEMenu => panic!("This shouldn't happen?"), }; if new_mode == self.cur_mode { @@ -269,10 +274,7 @@ impl App { } let action_tx = self.action_tx.clone(); - action_tx.send(Action::DisplayPopup( - popup::Type::Info, - String::from("Scanning Disks..."), - ))?; + action_tx.send(Action::SetMode(Mode::ScanDisks))?; loop { self.handle_events(&mut tui).await?; self.handle_actions(&mut tui)?; @@ -431,16 +433,10 @@ impl App { self.set_mode(new_mode)?; self.action_tx .send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?; - let (title, labels, items, select_one) = build_left_items(self, self.cur_mode); + self.action_tx.send(build_left_items(self, self.cur_mode))?; self.action_tx - .send(Action::UpdateLeft(title, labels, items, select_one))?; - let (labels, start, items) = build_right_items(self, self.cur_mode); - self.action_tx - .send(Action::UpdateRight(labels, start, items))?; + .send(build_right_items(self, self.cur_mode))?; match new_mode { - // Mode::InstallDrivers | Mode::SelectDisks => { - // self.action_tx.send(Action::Select(None, None))? - // } Mode::SelectTableType | Mode::Confirm => { // Select source/dest disks self.action_tx.send(Action::SelectRight( @@ -560,7 +556,7 @@ fn get_chunks(r: Rect) -> Vec { fn build_footer_string(cur_mode: Mode) -> String { match cur_mode { - Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { + Mode::Home | Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { String::from("(q) to quit") } Mode::SelectParts => String::from("(Enter) to select / (s) to start over / (q) to quit"), @@ -570,23 +566,31 @@ fn build_footer_string(cur_mode: Mode) -> String { Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"), Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"), Mode::Done | Mode::Failed | Mode::InstallDrivers => String::from("(Enter) or (q) to quit"), + // Invalid states + Mode::PEMenu => panic!("This shouldn't happen?"), } } -fn build_left_items(app: &App, cur_mode: Mode) -> (String, Vec, Vec, bool) { + +fn build_left_items(app: &App, cur_mode: Mode) -> Action { + let select_num: usize; let title: String; let mut items = Vec::new(); let mut labels: Vec = Vec::new(); - let mut select_one = false; match cur_mode { + Mode::Home => { + select_num = 0; + title = String::from("Home"); + } Mode::InstallDrivers => { + select_num = 1; title = String::from("Install Drivers"); - select_one = true; app.clone .driver_list .iter() .for_each(|driver| items.push(driver.to_string())); } Mode::SelectDisks => { + select_num = 2; title = String::from("Select Source and Destination Disks"); labels.push(String::from("source")); labels.push(String::from("dest")); @@ -596,18 +600,21 @@ fn build_left_items(app: &App, cur_mode: Mode) -> (String, Vec, Vec { + select_num = 1; title = String::from("Select Partition Table Type"); - select_one = true; items.push(format!("{}", PartitionTableType::Guid)); items.push(format!("{}", PartitionTableType::Legacy)); } Mode::Confirm => { + select_num = 0; title = String::from("Confirm Selections"); } - Mode::PreClone | Mode::Clone | Mode::PostClone | Mode::ScanDisks => { + Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { + select_num = 0; title = String::from("Processing"); } Mode::SelectParts => { + select_num = 2; title = String::from("Select Boot and OS Partitions"); labels.push(String::from("boot")); labels.push(String::from("os")); @@ -620,12 +627,17 @@ fn build_left_items(app: &App, cur_mode: Mode) -> (String, Vec, Vec title = String::from("Done"), + Mode::Done | Mode::Failed => { + select_num = 0; + title = String::from("Done"); + } + // Invalid states + Mode::PEMenu => panic!("This shouldn't happen?"), }; - (title, labels, items, select_one) + Action::UpdateLeft(title, labels, items, select_num) } -fn build_right_items(app: &App, cur_mode: Mode) -> (Vec>, usize, Vec>) { +fn build_right_items(app: &App, cur_mode: Mode) -> Action { let mut items = Vec::new(); let mut labels: Vec> = Vec::new(); let mut start_index = 0; @@ -698,5 +710,5 @@ fn build_right_items(app: &App, cur_mode: Mode) -> (Vec>, usize, Vec } _ => {} } - (labels, start_index, items) + Action::UpdateRight(labels, start_index, items) } diff --git a/include/menu_entries/01_deja-vu.toml b/include/menu_entries/01_deja-vu.toml index 6899622..2f33c07 100644 --- a/include/menu_entries/01_deja-vu.toml +++ b/include/menu_entries/01_deja-vu.toml @@ -1,4 +1,5 @@ name = 'Deja-Vu' command = 'X:\tools\deja-vu.exe' +description = "Windows clone assistant tool" use_conemu = true separator = false diff --git a/include/menu_entries/02_separator.toml b/include/menu_entries/02_separator.toml index ed50f1c..cfd668c 100644 --- a/include/menu_entries/02_separator.toml +++ b/include/menu_entries/02_separator.toml @@ -1,4 +1,5 @@ name = '' command = '' +description = '' use_conemu = false separator = true diff --git a/include/menu_entries/03_ntpwedit.toml b/include/menu_entries/03_ntpwedit.toml index 9b07df9..d8124dd 100644 --- a/include/menu_entries/03_ntpwedit.toml +++ b/include/menu_entries/03_ntpwedit.toml @@ -1,4 +1,5 @@ name = 'NTPWEdit' command = 'X:\Program Files\NTPWEdit\ntpwedit.exe' +description = 'Mostly used to unlock the built-in admin account' use_conemu = false separator = false diff --git a/include/menu_entries/04_clone-tool.toml b/include/menu_entries/04_clone-tool.toml index 1fe2dd3..530ddd3 100644 --- a/include/menu_entries/04_clone-tool.toml +++ b/include/menu_entries/04_clone-tool.toml @@ -1,4 +1,5 @@ name = 'Some Clone Tool' command = 'X:\Program Files\Some\Tool.exe' +description = 'Run Some Clone tool' use_conemu = false separator = false diff --git a/include/menu_entries/05_taskmgr.toml b/include/menu_entries/05_taskmgr.toml index c33d623..f4484fc 100644 --- a/include/menu_entries/05_taskmgr.toml +++ b/include/menu_entries/05_taskmgr.toml @@ -1,4 +1,5 @@ name = 'Task Manager' command = 'X:\Windows\System32\taskmgr.exe' +description = 'Manage those tasks' use_conemu = false separator = false diff --git a/include/pe-menu.toml b/include/pe-menu.toml deleted file mode 100644 index 45f8b6b..0000000 --- a/include/pe-menu.toml +++ /dev/null @@ -1,17 +0,0 @@ -# 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 . - -con_emu = 'X:\Program Files\ConEmu\ConEmu64.exe' -tools = [] diff --git a/pe_menu/Cargo.toml b/pe_menu/Cargo.toml index b1fdbd0..cf7bcfb 100644 --- a/pe_menu/Cargo.toml +++ b/pe_menu/Cargo.toml @@ -21,9 +21,26 @@ license = "GPL" version = "0.2.0" [dependencies] +core = { path = "../core" } +clap = { version = "4.4.5", features = [ + "derive", + "cargo", + "wrap_help", + "unicode", + "string", + "unstable-styles", + ] } +color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["event-stream"] } futures = "0.3.30" ratatui = "0.29.0" serde = { version = "1.0.217", features = ["derive"] } tokio = { version = "1.43.0", features = ["full"] } toml = "0.8.13" +tracing = "0.1.41" +tracing-error = "0.2.0" +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } + +[build-dependencies] +anyhow = "1.0.86" +vergen-gix = { version = "1.0.0", features = ["build", "cargo"] } diff --git a/pe_menu/src/lib.rs b/pe_menu/build.rs similarity index 63% rename from pe_menu/src/lib.rs rename to pe_menu/build.rs index 02aac4f..c3ced6b 100644 --- a/pe_menu/src/lib.rs +++ b/pe_menu/build.rs @@ -13,17 +13,16 @@ // You should have received a copy of the GNU General Public License // along with Deja-vu. If not, see . // -/// Application. -pub mod app; +use anyhow::Result; +use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder}; -/// Terminal events handler. -pub mod event; - -/// Widget renderer. -pub mod ui; - -/// Terminal user interface. -pub mod tui; - -/// Event handler. -pub mod handler; +fn main() -> Result<()> { + let build = BuildBuilder::all_build()?; + let gix = GixBuilder::all_git()?; + let cargo = CargoBuilder::all_cargo()?; + Emitter::default() + .add_instructions(&build)? + .add_instructions(&gix)? + .add_instructions(&cargo)? + .emit() +} diff --git a/pe_menu/src/app.rs b/pe_menu/src/app.rs index fbe5c23..94fbc28 100644 --- a/pe_menu/src/app.rs +++ b/pe_menu/src/app.rs @@ -13,354 +13,416 @@ // 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 core::{ + action::Action, + components::{ + footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, state::StatefulList, + title::Title, Component, + }, + config::Config, + line::DVLine, + state::Mode, + tasks::{Task, Tasks}, + tui::{Event, Tui}, +}; use std::{ - env, error, fs, io, + env, fs, + iter::zip, path::PathBuf, - process::{Command, Output}, - thread::{self, JoinHandle}, + sync::{Arc, Mutex}, }; -/// Application result type. -#[allow(clippy::module_name_repetitions)] -pub type AppResult = std::result::Result>; +use color_eyre::Result; +use ratatui::{ + crossterm::event::KeyEvent, + layout::{Constraint, Direction, Layout}, + prelude::Rect, + style::Color, +}; +use serde::Deserialize; +use tokio::sync::mpsc; +use tracing::{debug, info}; -/// 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)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct Tool { name: String, command: String, + description: 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(), - } - } + // TUI + action_rx: mpsc::UnboundedReceiver, + action_tx: mpsc::UnboundedSender, + components: Vec>, + config: Config, + frame_rate: f64, + last_tick_key_events: Vec, + should_quit: bool, + should_suspend: bool, + tick_rate: f64, + // App + list: StatefulList, + mode: Mode, + tasks: Tasks, } 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 + pub fn new(tick_rate: f64, frame_rate: f64) -> Result { + let (action_tx, action_rx) = mpsc::unbounded_channel(); + let disk_list_arc = Arc::new(Mutex::new(Vec::new())); + let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); + let mut list = StatefulList::default(); + list.set_items(load_tools()); + Ok(Self { + // TUI + action_rx, + action_tx, + components: vec![ + Box::new(Title::new("PE Menu")), + Box::new(FpsCounter::new()), + Box::new(Left::new()), + Box::new(Right::new()), + Box::new(Footer::new()), + Box::new(popup::Popup::new()), + ], + config: Config::new()?, + frame_rate, + last_tick_key_events: Vec::new(), + should_quit: false, + should_suspend: false, + tick_rate, + // App + list, + mode: Mode::default(), + tasks, + }) } - /// Handles the tick event of the terminal. - pub fn tick(&self) {} + pub async fn run(&mut self) -> Result<()> { + let mut tui = Tui::new()? + // .mouse(true) // uncomment this line to enable mouse support + .tick_rate(self.tick_rate) + .frame_rate(self.frame_rate); + tui.enter()?; - /// 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")), + for component in &mut self.components { + component.register_action_handler(self.action_tx.clone())?; } - if let Some(a) = argument { - Command::new("wpeutil") - .arg(a) - .output() - .expect("Failed to run exit command"); + for component in &mut self.components { + component.register_config_handler(self.config.clone())?; + } + for component in &mut self.components { + component.init(tui.size()?)?; + } + + let action_tx = self.action_tx.clone(); + action_tx.send(Action::SetMode(Mode::PEMenu))?; + loop { + self.handle_events(&mut tui).await?; + self.handle_actions(&mut tui)?; + if self.should_suspend { + tui.suspend()?; + action_tx.send(Action::Resume)?; + action_tx.send(Action::ClearScreen)?; + // tui.mouse(true); + tui.enter()?; + } else if self.should_quit { + tui.stop()?; + break; + } + } + tui.exit()?; + Ok(()) + } + + async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { + let Some(event) = tui.next_event().await else { + return Ok(()); + }; + let action_tx = self.action_tx.clone(); + match event { + Event::Quit => action_tx.send(Action::Quit)?, + Event::Tick => action_tx.send(Action::Tick)?, + Event::Render => action_tx.send(Action::Render)?, + Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, + Event::Key(key) => self.handle_key_event(key)?, + _ => {} + } + for component in &mut self.components { + if let Some(action) = component.handle_events(Some(event.clone()))? { + action_tx.send(action)?; + } } 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() + fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { + let action_tx = self.action_tx.clone(); + let Some(keymap) = self.config.keybindings.get(&self.mode) else { + return Ok(()); }; - - // 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 + if let Some(action) = keymap.get(&vec![key]) { + info!("Got action: {action:?}"); + action_tx.send(action.clone())?; } 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; - } + // If the key was not handled as a single key action, + // then consider it for multi-key combinations. + self.last_tick_key_events.push(key); - // 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); + // Check for multi-key combinations + if let Some(action) = keymap.get(&self.last_tick_key_events) { + info!("Got action: {action:?}"); + action_tx.send(action.clone())?; + } + } + Ok(()) + } + + fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> { + while let Ok(action) = self.action_rx.try_recv() { + if action != Action::Tick && action != Action::Render { + debug!("{action:?}"); + } + match action { + Action::Tick => { + self.last_tick_key_events.drain(..); + } + Action::Quit => self.should_quit = true, + Action::Suspend => self.should_suspend = true, + Action::Resume => self.should_suspend = false, + Action::ClearScreen => tui.terminal.clear()?, + Action::KeyUp => { + self.list.previous(); + if let Some(tool) = self.list.get_selected() { + if tool.separator { + // Skip over separator + self.list.previous(); + if let Some(index) = self.list.selected() { + self.action_tx.send(Action::Highlight(index))?; + } + } + } + } + Action::KeyDown => { + self.list.next(); + if let Some(tool) = self.list.get_selected() { + if tool.separator { + // Skip over separator + self.list.next(); + if let Some(index) = self.list.selected() { + self.action_tx.send(Action::Highlight(index))?; + } + } + } + } + Action::Error(ref msg) => { + self.action_tx + .send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?; + self.action_tx.send(Action::SetMode(Mode::Failed))?; + } + Action::Process => { + // Run selected tool + if let Some(tool) = self.list.get_selected() { + info!("Run tool: {:?}", &tool); + self.tasks.add(build_command(&self, &tool)); + } + } + Action::Resize(w, h) => self.handle_resize(tui, w, h)?, + Action::Render => self.render(tui)?, + Action::SetMode(mode) => { + self.mode = mode; + self.action_tx + .send(Action::UpdateFooter(String::from("(Enter) to select")))?; + self.action_tx.send(build_left_items(self))?; + self.action_tx.send(build_right_items(self))?; + self.action_tx.send(Action::Select(None, None))?; + } + Action::UpdateFooter(_) + | Action::UpdateLeft(_, _, _, _) + | Action::UpdateRight(_, _, _) + | Action::Highlight(_) => { + info!("Processing Action: {:?}", action); + } + _ => {} + } + for component in &mut self.components { + if let Some(action) = component.update(action.clone())? { + self.action_tx.send(action)?; + }; + } + } + Ok(()) + } + + fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> { + tui.resize(Rect::new(0, 0, w, h))?; + self.render(tui)?; + Ok(()) + } + + fn render(&mut self, tui: &mut Tui) -> Result<()> { + tui.draw(|frame| { + if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] { + let component_areas = vec![ + header, // Title Bar + header, // FPS Counter + left, right, footer, popup, + ]; + for (component, area) in zip(self.components.iter_mut(), component_areas) { + if let Err(err) = component.draw(frame, area) { + let _ = self + .action_tx + .send(Action::Error(format!("Failed to draw: {err:?}"))); + } + } + }; + })?; + Ok(()) } } + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + // Cut the given rectangle into three vertical pieces + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + // Then cut the middle vertical piece into three width-wise pieces + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] // Return the middle chunk +} + +fn get_chunks(r: Rect) -> Vec { + let mut chunks: Vec = Vec::with_capacity(6); + + // Main sections + chunks.extend( + Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) + .split(r) + .to_vec(), + ); + + // Left/Right + chunks.extend( + Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(centered_rect(90, 90, chunks[1])) + .to_vec(), + ); + + // Popup + chunks.push(centered_rect(60, 25, r)); + + // Done + chunks +} + +pub fn build_command(app: &App, tool: &Tool) -> Task { + let cmd_path: PathBuf; + let mut cmd_args: Vec = Vec::new(); + let start_index: usize; + if tool.use_conemu { + cmd_path = app.config.conemu_path.clone(); + cmd_args.push(String::from("-new_console:n")); + cmd_args.push(tool.command.clone()); + start_index = 1; + } else { + cmd_path = PathBuf::from(tool.command.clone()); + start_index = 0; + } + if let Some(args) = &tool.args { + if args.len() > start_index { + args[start_index..].iter().for_each(|a| { + cmd_args.push(a.clone()); + }); + } + } + Task::Command(cmd_path, cmd_args) +} + +fn build_left_items(app: &App) -> Action { + let title = String::from("Tools"); + let labels = vec![String::new(), String::new()]; + let items = app + .list + .items + .iter() + .map(|tool| { + if tool.separator { + String::from("──────────────") + } else { + tool.name.clone() + } + }) + // ─ + .collect(); + Action::UpdateLeft(title, labels, items, 0) +} + +fn build_right_items(app: &App) -> Action { + let labels: Vec> = Vec::new(); + let items = app + .list + .items + .iter() + .map(|entry| { + vec![ + DVLine { + line_parts: vec![entry.name.clone()], + line_colors: vec![Color::Cyan], + }, + DVLine { + line_parts: vec![String::new()], + line_colors: vec![Color::Reset], + }, + DVLine { + line_parts: vec![entry.description.clone()], + line_colors: vec![Color::Reset], + }, + ] + }) + .collect(); + let start_index = 0; + Action::UpdateRight(labels, start_index, items) +} + +pub fn load_tools() -> Vec { + let exe_path = env::current_exe().expect("Failed to find main executable"); + 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(); + entries + .iter() + .map(|entry| { + 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"); + tool + }) + .collect() +} diff --git a/pe_menu/src/event.rs b/pe_menu/src/event.rs deleted file mode 100644 index 340e426..0000000 --- a/pe_menu/src/event.rs +++ /dev/null @@ -1,116 +0,0 @@ -// 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 std::time::Duration; - -use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent}; -use futures::{FutureExt, StreamExt}; -use tokio::sync::mpsc; - -use crate::app::AppResult; - -/// Terminal events. -#[derive(Clone, Copy, Debug)] -pub enum Event { - /// Terminal tick. - Tick, - /// Key press. - Key(KeyEvent), - /// Mouse click/scroll. - Mouse(MouseEvent), - /// Terminal resize. - Resize(u16, u16), -} - -/// Terminal event handler. -#[allow(dead_code)] -#[derive(Debug)] -pub struct Handler { - /// Event sender channel. - sender: mpsc::UnboundedSender, - /// Event receiver channel. - receiver: mpsc::UnboundedReceiver, - /// Event handler thread. - handler: tokio::task::JoinHandle<()>, -} - -impl Handler { - /// Constructs a new instance of [`Handler`]. - /// - /// # Panics - /// - /// Will panic if `sender_clone ` doesn't unwrap - #[must_use] - pub fn new(tick_rate: u64) -> Self { - let tick_rate = Duration::from_millis(tick_rate); - let (sender, receiver) = mpsc::unbounded_channel(); - let sender_clone = sender.clone(); - let handler = tokio::spawn(async move { - let mut reader = crossterm::event::EventStream::new(); - let mut tick = tokio::time::interval(tick_rate); - loop { - let tick_delay = tick.tick(); - let crossterm_event = reader.next().fuse(); - tokio::select! { - () = sender_clone.closed() => { - break; - } - _ = tick_delay => { - sender_clone.send(Event::Tick).unwrap(); - } - Some(Ok(evt)) = crossterm_event => { - match evt { - CrosstermEvent::Key(key) => { - if key.kind == crossterm::event::KeyEventKind::Press { - sender_clone.send(Event::Key(key)).unwrap(); - } - }, - CrosstermEvent::Mouse(mouse) => { - sender_clone.send(Event::Mouse(mouse)).unwrap(); - }, - CrosstermEvent::Resize(x, y) => { - sender_clone.send(Event::Resize(x, y)).unwrap(); - }, - CrosstermEvent::FocusGained | CrosstermEvent::FocusLost | CrosstermEvent::Paste(_) => {}, - } - } - }; - } - }); - Self { - sender, - receiver, - handler, - } - } - - /// Receive the next event from the handler thread. - /// - /// This function will always block the current thread if - /// there is no data available and it's possible for more data to be sent. - /// - /// # Errors - /// - /// Will return error if a event is not found - pub async fn next(&mut self) -> AppResult { - self.receiver - .recv() - .await - .ok_or(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - "This is an IO error", - ))) - } -} diff --git a/pe_menu/src/handler.rs b/pe_menu/src/handler.rs deleted file mode 100644 index fed2d63..0000000 --- a/pe_menu/src/handler.rs +++ /dev/null @@ -1,61 +0,0 @@ -// 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 crate::app::{App, QuitReason}; -use crossterm::event::{KeyCode, KeyEvent}; - -/// Handles the key events and updates the state of [`App`]. -pub fn handle_key_events(key_event: KeyEvent, app: &mut App) { - match key_event.code { - KeyCode::F(5) => app.quit(QuitReason::Exit), - KeyCode::Char('p' | 'P') => app.quit(QuitReason::Poweroff), - KeyCode::Char('r' | 'R') => app.quit(QuitReason::Restart), - KeyCode::Char('t' | 'T') => app.open_terminal(), - KeyCode::Up => { - if app.popup.is_none() { - app.main_menu.previous(); - if let Some(e) = app.main_menu.get_selected() { - if e.separator { - // Skip over separators - app.main_menu.previous(); - } - } - } - } - KeyCode::Down => { - if app.popup.is_none() { - app.main_menu.next(); - if let Some(e) = app.main_menu.get_selected() { - if e.separator { - // Skip over separators - app.main_menu.next(); - } - } - } - } - KeyCode::Enter => { - if app.popup.is_some() { - // Clear popup and return to main menu - app.popup = None; - } else { - app.run_tool(); - } - } - KeyCode::Esc | KeyCode::Char('q' | 'Q') => { - app.popup = None; - } - _ => {} - } -} diff --git a/pe_menu/src/main.rs b/pe_menu/src/main.rs index b7fab7a..8bcfda9 100644 --- a/pe_menu/src/main.rs +++ b/pe_menu/src/main.rs @@ -13,40 +13,21 @@ // You should have received a copy of the GNU General Public License // along with Deja-vu. If not, see . // -use pe_menu::app::{App, AppResult}; -use pe_menu::event::{Event, Handler}; -use pe_menu::handler::handle_key_events; -use pe_menu::tui::Tui; -use ratatui::backend::CrosstermBackend; -use ratatui::Terminal; -use std::io; +use clap::Parser; +use color_eyre::Result; +use core; + +use crate::app::App; + +mod app; #[tokio::main] -async fn main() -> AppResult<()> { - // Create an application. - let mut app = App::new(); +async fn main() -> Result<()> { + core::errors::init()?; + core::logging::init()?; - // Initialize the terminal user interface. - let backend = CrosstermBackend::new(io::stderr()); - let terminal = Terminal::new(backend)?; - let events = Handler::new(250); - let mut tui = Tui::new(terminal, events); - tui.init()?; - - // Start the main loop. - while app.running { - // Render the user interface. - tui.draw(&mut app)?; - // Handle events. - match tui.events.next().await? { - Event::Tick => app.tick(), - Event::Key(key_event) => handle_key_events(key_event, &mut app), - Event::Mouse(_) | Event::Resize(_, _) => {} - } - } - - // Exit the user interface. - tui.exit()?; - app.exit()?; + let args = core::cli::Cli::parse(); + let mut app = App::new(args.tick_rate, args.frame_rate)?; + app.run().await?; Ok(()) } diff --git a/pe_menu/src/tui.rs b/pe_menu/src/tui.rs deleted file mode 100644 index 313108c..0000000 --- a/pe_menu/src/tui.rs +++ /dev/null @@ -1,111 +0,0 @@ -// 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 crate::app::{App, AppResult}; -use crate::event::Handler; -use crate::ui; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use ratatui::backend::Backend; -use ratatui::Terminal; -use std::io; -use std::panic; - -/// Representation of a terminal user interface. -/// -/// It is responsible for setting up the terminal, -/// initializing the interface and handling the draw events. -#[derive(Debug)] -pub struct Tui { - /// Interface to the Terminal. - terminal: Terminal, - /// Terminal event handler. - pub events: Handler, -} - -impl Tui { - /// Constructs a new instance of [`Tui`]. - pub fn new(terminal: Terminal, events: Handler) -> Self { - Self { terminal, events } - } - - /// Initializes the terminal interface. - /// - /// It enables the raw mode and sets terminal properties. - /// - /// # Errors - /// - /// Will return error if `enable_raw_mode` fails - /// - /// # Panics - /// - /// Will panic if `reset` fails - pub fn init(&mut self) -> AppResult<()> { - terminal::enable_raw_mode()?; - crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; - - // Define a custom panic hook to reset the terminal properties. - // This way, you won't have your terminal messed up if an unexpected error happens. - let panic_hook = panic::take_hook(); - panic::set_hook(Box::new(move |panic| { - Self::reset().expect("failed to reset the terminal"); - panic_hook(panic); - })); - - self.terminal.hide_cursor()?; - self.terminal.clear()?; - Ok(()) - } - - /// [`Draw`] the terminal interface by [`rendering`] the widgets. - /// - /// [`Draw`]: ratatui::Terminal::draw - /// [`rendering`]: crate::ui::render - /// - /// # Errors - /// - /// Will return error if `draw` fails - pub fn draw(&mut self, app: &mut App) -> AppResult<()> { - self.terminal.draw(|frame| ui::render(app, frame))?; - Ok(()) - } - - /// Resets the terminal interface. - /// - /// This function is also used for the panic hook to revert - /// the terminal properties if unexpected errors occur. - /// - /// # Errors - /// - /// Will return error if `disable_raw_mode` fails - fn reset() -> AppResult<()> { - terminal::disable_raw_mode()?; - crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; - Ok(()) - } - - /// Exits the terminal interface. - /// - /// It disables the raw mode and reverts back the terminal properties. - /// - /// # Errors - /// - /// Will return error if either `reset` or `show_cursor` fails - pub fn exit(&mut self) -> AppResult<()> { - Self::reset()?; - self.terminal.show_cursor()?; - Ok(()) - } -} diff --git a/pe_menu/src/ui.rs b/pe_menu/src/ui.rs deleted file mode 100644 index 3613173..0000000 --- a/pe_menu/src/ui.rs +++ /dev/null @@ -1,127 +0,0 @@ -// 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 crate::app::App; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style, Stylize}, - text::{Line, Span}, - widgets::{Block, Borders, Clear, HighlightSpacing, List, ListItem, Padding, Paragraph, Wrap}, - Frame, -}; - -/// Renders the user interface widgets. -pub fn render(app: &mut App, frame: &mut Frame) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Min(1), - Constraint::Length(3), - ]) - .split(frame.area()); - - // Title Block - let title_text = Span::styled("Deja-vu: PE Menu", Style::default().fg(Color::LightCyan)); - let title = Paragraph::new(Line::from(title_text).centered()) - .block(Block::default().borders(Borders::ALL)); - frame.render_widget(title, chunks[0]); - - // Main Block - let main_chunk = centered_rect(65, 90, chunks[1]); - render_main_pane(frame, app, main_chunk); - - // Bottom Block - let footer_text = Span::styled( - "(Enter) to select / (p) to poweroff / (r) to restart / (t) for terminal", - Style::default().fg(Color::DarkGray), - ); - let footer = Paragraph::new(Line::from(footer_text).centered()) - .block(Block::default().borders(Borders::ALL)); - frame.render_widget(footer, chunks[2]); - - // Popup blocks - if app.popup.is_some() { - render_popup_pane(frame, app); - } -} - -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - // Cut the given rectangle into three vertical pieces - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - // Then cut the middle vertical piece into three width-wise pieces - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] // Return the middle chunk -} - -fn render_main_pane(frame: &mut Frame, app: &mut App, chunk: Rect) { - let mut list_items = Vec::::new(); - for entry in &app.main_menu.items { - let text = if entry.separator { - if entry.name.is_empty() { - String::from("....................") - } else { - entry.name.clone() - } - } else { - entry.name.clone() - }; - list_items.push(ListItem::new(format!(" {text}\n\n\n"))); - } - let list = List::new(list_items) - .block( - Block::default() - .borders(Borders::ALL) - .padding(Padding::new(20, 20, 5, 5)), - ) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().green().bold()) - .highlight_symbol(" --> ") - .repeat_highlight_symbol(false); - frame.render_stateful_widget(list, chunk, &mut app.main_menu.state); -} - -fn render_popup_pane(frame: &mut Frame, app: &mut App) { - let popup_block = Block::default() - .borders(Borders::ALL) - .style(Style::default().red().bold()); - if let Some(popup) = &app.popup { - let scan_paragraph = Paragraph::new(vec![ - Line::from(Span::raw(&popup.title)), - Line::default(), - Line::from(Span::raw(&popup.body)), - ]) - .block(popup_block) - .centered() - .wrap(Wrap { trim: false }); - let area = centered_rect(60, 25, frame.area()); - frame.render_widget(Clear, area); - frame.render_widget(scan_paragraph, area); - } -}