Convert to workspace with both deja-vu and pe-menu

This commit is contained in:
2Shirt 2024-11-21 05:34:10 -08:00
parent 83badb9c43
commit d7c070aad8
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
49 changed files with 1193 additions and 68 deletions

3
.envrc
View file

@ -1,3 +0,0 @@
export DEJA_VU_CONFIG=`pwd`/.config
export DEJA_VU_DATA=`pwd`/.data
export DEJA_VU_LOG_LEVEL=debug

112
Cargo.lock generated
View file

@ -338,6 +338,19 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "compact_str"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"ryu",
"static_assertions",
]
[[package]]
name = "compact_str"
version = "0.8.0"
@ -432,6 +445,23 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags",
"crossterm_winapi",
"futures-core",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.28.1"
@ -441,7 +471,7 @@ dependencies = [
"bitflags",
"crossterm_winapi",
"futures-core",
"mio",
"mio 1.0.2",
"parking_lot",
"rustix",
"serde",
@ -519,7 +549,7 @@ dependencies = [
"clap",
"color-eyre",
"config",
"crossterm",
"crossterm 0.28.1",
"derive_deref",
"directories",
"futures",
@ -529,7 +559,7 @@ dependencies = [
"libc",
"once_cell",
"pretty_assertions",
"ratatui",
"ratatui 0.28.1",
"raw-cpuid",
"regex",
"serde",
@ -1465,6 +1495,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
@ -1624,6 +1663,18 @@ dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.2"
@ -1761,6 +1812,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
[[package]]
name = "pe-menu"
version = "0.1.0"
dependencies = [
"crossterm 0.27.0",
"futures",
"ratatui 0.26.3",
"serde",
"tokio",
"toml",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -1864,6 +1927,26 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "ratatui"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef"
dependencies = [
"bitflags",
"cassowary",
"compact_str 0.7.1",
"crossterm 0.27.0",
"itertools 0.12.1",
"lru",
"paste",
"stability",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.1.14",
]
[[package]]
name = "ratatui"
version = "0.28.1"
@ -1872,10 +1955,10 @@ checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
dependencies = [
"bitflags",
"cassowary",
"compact_str",
"crossterm",
"compact_str 0.8.0",
"crossterm 0.28.1",
"instability",
"itertools",
"itertools 0.13.0",
"lru",
"paste",
"serde",
@ -2126,7 +2209,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"mio 0.8.11",
"mio 1.0.2",
"signal-hook",
]
@ -2164,6 +2248,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "stability"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac"
dependencies = [
"quote",
"syn 2.0.82",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -2348,7 +2442,7 @@ dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"mio 1.0.2",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@ -2544,7 +2638,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools",
"itertools 0.13.0",
"unicode-segmentation",
"unicode-width 0.1.14",
]

View file

@ -13,54 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
[package]
name = "deja-vu"
version = "0.2.0"
edition = "2021"
description = "Clone/Install Windows, create/edit boot files, and troubleshoot boot issues."
authors = ["2Shirt <2xShirt@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
better-panic = "0.3.0"
clap = { version = "4.4.5", features = [
"derive",
"cargo",
"wrap_help",
"unicode",
"string",
"unstable-styles",
] }
color-eyre = "0.6.3"
config = "0.14.0"
crossterm = { version = "0.28.1", features = ["serde", "event-stream"] }
derive_deref = "1.1.1"
directories = "5.0.1"
futures = "0.3.30"
human-panic = "2.0.1"
json5 = "0.4.1"
lazy_static = "1.5.0"
libc = "0.2.158"
once_cell = "1.20.2"
pretty_assertions = "1.4.0"
ratatui = { version = "0.28.1", features = ["serde", "macros"] }
raw-cpuid = "11.2.0"
regex = "1.11.1"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
signal-hook = "0.3.17"
strip-ansi-escapes = "0.2.0"
strum = { version = "0.26.3", features = ["derive"] }
tempfile = "3.13.0"
tokio = { version = "1.39.3", features = ["full"] }
tokio-util = "0.7.11"
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
walkdir = "2.5.0"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }
[workspace]
members = ["deja_vu", "pe_menu"]
resolver = "2"

View file

@ -1,11 +1,11 @@
# Deja-vu #
# deja-vu #
Clone/Install Windows, create/edit boot files, and troubleshoot boot issues. This tool can and will delete data on selected destination devices; care should be taken to avoid unintended data loss.
Clone/Install Windows, create/edit boot files, and troubleshoot boot issues. This tool can and will delete data; care should be taken to avoid unintended data loss.
## deva-vu.toml ##
# pe-menu #
Please update this configuration file with the full path to the cloning tool of your choice.
Menu to launch various applications (like deja-vu).
### NOTES ###
## NOTES ##
This tool is under active development and is not considered ready for production use!

30
build_win_release.sh Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
#
# 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/>.
#
## Helper script to build and assemble the items needed for a release
set -o errexit
set -o errtrace
set -o nounset
set -o pipefail
TEMP_DIR="$(mktemp -d)"
cargo build --release --target=x86_64-pc-windows-gnu
cp -nv target/x86_64-pc-windows-gnu/release/*exe "${TEMP_DIR}"/
cp -nrv include/* "${TEMP_DIR}"/
tar cavf "deja-vu_$(date +%Y-%m-%d).txz" -C "${TEMP_DIR}" .

3
deja_vu/.envrc Normal file
View file

@ -0,0 +1,3 @@
export DEJA_VU_CONFIG=`pwd`/config
export DEJA_VU_DATA=`pwd`/data
export DEJA_VU_LOG_LEVEL=debug

66
deja_vu/Cargo.toml Normal file
View file

@ -0,0 +1,66 @@
# 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/>.
[package]
name = "deja-vu"
version = "0.2.0"
edition = "2021"
description = "Clone/Install Windows, create/edit boot files, and troubleshoot boot issues."
authors = ["2Shirt <2xShirt@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
better-panic = "0.3.0"
clap = { version = "4.4.5", features = [
"derive",
"cargo",
"wrap_help",
"unicode",
"string",
"unstable-styles",
] }
color-eyre = "0.6.3"
config = "0.14.0"
crossterm = { version = "0.28.1", features = ["serde", "event-stream"] }
derive_deref = "1.1.1"
directories = "5.0.1"
futures = "0.3.30"
human-panic = "2.0.1"
json5 = "0.4.1"
lazy_static = "1.5.0"
libc = "0.2.158"
once_cell = "1.20.2"
pretty_assertions = "1.4.0"
ratatui = { version = "0.28.1", features = ["serde", "macros"] }
raw-cpuid = "11.2.0"
regex = "1.11.1"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
signal-hook = "0.3.17"
strip-ansi-escapes = "0.2.0"
strum = { version = "0.26.3", features = ["derive"] }
tempfile = "3.13.0"
tokio = { version = "1.39.3", features = ["full"] }
tokio-util = "0.7.11"
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
walkdir = "2.5.0"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

23
include/drivers/README.md Normal file
View file

@ -0,0 +1,23 @@
## Drivers ##
This tool expects there to be zero or more folders here. Each folder can contain multiple files/folders within.
### Example ###
- Intel RST - 13th Gen/
- HsaComponent/
- iaStorHsaComponent.cat
- iaStorHsaComponent.inf
- HsaExtension/
- iaStorHsa_Ext.cat
- iaStorHsa_Ext.inf
- iaStorVD.cat
- iaStorVD.inf
- iaStorVD.sys
- RstMwEventLogMsg.dll
- RstMwService.exe
- VirtIO /
- Some.cat
- Some.inf
- Some.sys
- etc..

View file

@ -0,0 +1,4 @@
name = 'Deja-Vu'
command = 'X:\tools\deja-vu.exe'
use_conemu = true
separator = false

View file

@ -0,0 +1,4 @@
name = ''
command = ''
use_conemu = false
separator = true

View file

@ -0,0 +1,4 @@
name = 'NTPWEdit'
command = 'X:\Program Files\NTPWEdit\ntpwedit.exe'
use_conemu = false
separator = false

View file

@ -0,0 +1,4 @@
name = 'Some Clone Tool'
command = 'X:\Program Files\Some\Tool.exe'
use_conemu = false
separator = false

View file

@ -0,0 +1,4 @@
name = 'Task Manager'
command = 'X:\Windows\System32\taskmgr.exe'
use_conemu = false
separator = false

32
include/pe-menu.cmd Normal file
View file

@ -0,0 +1,32 @@
@echo off
:: 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/>.
pushd %~dp0
wpeutil EnableFirewall
:loop
cls
pe-menu.exe
if %errorlevel% NEQ 0 (
echo.
echo "ERROR: pe-menu crashed"
echo.
pause
)
goto loop
:done
popd

17
include/pe-menu.toml Normal file
View file

@ -0,0 +1,17 @@
# 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/>.
con_emu = 'X:\Program Files\ConEmu\ConEmu64.exe'
tools = []

29
pe_menu/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
# 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/>.
[package]
name = "pe-menu"
version = "0.1.0"
authors = ["2Shirt <2xShirt@gmail.com>"]
license = "GPL"
edition = "2021"
[dependencies]
crossterm = { version = "0.27.0", features = ["event-stream"] }
futures = "0.3.30"
ratatui = "0.26.0"
serde = { version = "1.0.202", features = ["derive"] }
tokio = { version = "1.35.1", features = ["full"] }
toml = "0.8.13"

366
pe_menu/src/app.rs Normal file
View file

@ -0,0 +1,366 @@
// 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);
}
}

116
pe_menu/src/event.rs Normal file
View file

@ -0,0 +1,116 @@
// 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 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>,
/// Event receiver channel.
receiver: mpsc::UnboundedReceiver<Event>,
/// 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<Event> {
self.receiver
.recv()
.await
.ok_or(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"This is an IO error",
)))
}
}

61
pe_menu/src/handler.rs Normal file
View file

@ -0,0 +1,61 @@
// 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 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;
}
_ => {}
}
}

29
pe_menu/src/lib.rs Normal file
View file

@ -0,0 +1,29 @@
// 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/>.
//
/// Application.
pub mod app;
/// Terminal events handler.
pub mod event;
/// Widget renderer.
pub mod ui;
/// Terminal user interface.
pub mod tui;
/// Event handler.
pub mod handler;

52
pe_menu/src/main.rs Normal file
View file

@ -0,0 +1,52 @@
// 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 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;
#[tokio::main]
async fn main() -> AppResult<()> {
// Create an application.
let mut app = App::new();
// 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()?;
Ok(())
}

111
pe_menu/src/tui.rs Normal file
View file

@ -0,0 +1,111 @@
// 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 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<B: Backend> {
/// Interface to the Terminal.
terminal: Terminal<B>,
/// Terminal event handler.
pub events: Handler,
}
impl<B: Backend> Tui<B> {
/// Constructs a new instance of [`Tui`].
pub fn new(terminal: Terminal<B>, 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(())
}
}

127
pe_menu/src/ui.rs Normal file
View file

@ -0,0 +1,127 @@
// 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 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.size());
// Title Block
let title_text = Span::styled("WizardKit: 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::<ListItem>::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.size());
frame.render_widget(Clear, area);
frame.render_widget(scan_paragraph, area);
}
}