diff --git a/.envrc b/.envrc deleted file mode 100644 index b672b19..0000000 --- a/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -export DEJA_VU_CONFIG=`pwd`/.config -export DEJA_VU_DATA=`pwd`/.data -export DEJA_VU_LOG_LEVEL=debug diff --git a/Cargo.lock b/Cargo.lock index ee8bcdf..835fa64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 7e4c451..3861750 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,54 +13,6 @@ # You should have received a copy of the GNU General Public License # along with Deja-vu. If not, see . -[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" diff --git a/README.md b/README.md index 631c85a..e1e76f6 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/build_win_release.sh b/build_win_release.sh new file mode 100755 index 0000000..4f0ae2d --- /dev/null +++ b/build_win_release.sh @@ -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 . +# +## 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}" . diff --git a/deja_vu/.envrc b/deja_vu/.envrc new file mode 100644 index 0000000..dd7b181 --- /dev/null +++ b/deja_vu/.envrc @@ -0,0 +1,3 @@ +export DEJA_VU_CONFIG=`pwd`/config +export DEJA_VU_DATA=`pwd`/data +export DEJA_VU_LOG_LEVEL=debug diff --git a/deja_vu/Cargo.toml b/deja_vu/Cargo.toml new file mode 100644 index 0000000..7e4c451 --- /dev/null +++ b/deja_vu/Cargo.toml @@ -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 . + +[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"] } diff --git a/build.rs b/deja_vu/build.rs similarity index 100% rename from build.rs rename to deja_vu/build.rs diff --git a/config/config.json5 b/deja_vu/config/config.json5 similarity index 100% rename from config/config.json5 rename to deja_vu/config/config.json5 diff --git a/src/action.rs b/deja_vu/src/action.rs similarity index 100% rename from src/action.rs rename to deja_vu/src/action.rs diff --git a/src/app.rs b/deja_vu/src/app.rs similarity index 100% rename from src/app.rs rename to deja_vu/src/app.rs diff --git a/src/cli.rs b/deja_vu/src/cli.rs similarity index 100% rename from src/cli.rs rename to deja_vu/src/cli.rs diff --git a/src/components.rs b/deja_vu/src/components.rs similarity index 100% rename from src/components.rs rename to deja_vu/src/components.rs diff --git a/src/components/footer.rs b/deja_vu/src/components/footer.rs similarity index 100% rename from src/components/footer.rs rename to deja_vu/src/components/footer.rs diff --git a/src/components/fps.rs b/deja_vu/src/components/fps.rs similarity index 100% rename from src/components/fps.rs rename to deja_vu/src/components/fps.rs diff --git a/src/components/left.rs b/deja_vu/src/components/left.rs similarity index 100% rename from src/components/left.rs rename to deja_vu/src/components/left.rs diff --git a/src/components/popup.rs b/deja_vu/src/components/popup.rs similarity index 100% rename from src/components/popup.rs rename to deja_vu/src/components/popup.rs diff --git a/src/components/right.rs b/deja_vu/src/components/right.rs similarity index 100% rename from src/components/right.rs rename to deja_vu/src/components/right.rs diff --git a/src/components/state.rs b/deja_vu/src/components/state.rs similarity index 100% rename from src/components/state.rs rename to deja_vu/src/components/state.rs diff --git a/src/components/title.rs b/deja_vu/src/components/title.rs similarity index 100% rename from src/components/title.rs rename to deja_vu/src/components/title.rs diff --git a/src/config.rs b/deja_vu/src/config.rs similarity index 100% rename from src/config.rs rename to deja_vu/src/config.rs diff --git a/src/errors.rs b/deja_vu/src/errors.rs similarity index 100% rename from src/errors.rs rename to deja_vu/src/errors.rs diff --git a/src/logging.rs b/deja_vu/src/logging.rs similarity index 100% rename from src/logging.rs rename to deja_vu/src/logging.rs diff --git a/src/main.rs b/deja_vu/src/main.rs similarity index 100% rename from src/main.rs rename to deja_vu/src/main.rs diff --git a/src/system.rs b/deja_vu/src/system.rs similarity index 100% rename from src/system.rs rename to deja_vu/src/system.rs diff --git a/src/system/cpu.rs b/deja_vu/src/system/cpu.rs similarity index 100% rename from src/system/cpu.rs rename to deja_vu/src/system/cpu.rs diff --git a/src/system/disk.rs b/deja_vu/src/system/disk.rs similarity index 100% rename from src/system/disk.rs rename to deja_vu/src/system/disk.rs diff --git a/src/system/diskpart.rs b/deja_vu/src/system/diskpart.rs similarity index 100% rename from src/system/diskpart.rs rename to deja_vu/src/system/diskpart.rs diff --git a/src/system/drivers.rs b/deja_vu/src/system/drivers.rs similarity index 100% rename from src/system/drivers.rs rename to deja_vu/src/system/drivers.rs diff --git a/src/tasks.rs b/deja_vu/src/tasks.rs similarity index 100% rename from src/tasks.rs rename to deja_vu/src/tasks.rs diff --git a/src/tests/mod.rs b/deja_vu/src/tests/mod.rs similarity index 100% rename from src/tests/mod.rs rename to deja_vu/src/tests/mod.rs diff --git a/src/tests/sample_output.rs b/deja_vu/src/tests/sample_output.rs similarity index 100% rename from src/tests/sample_output.rs rename to deja_vu/src/tests/sample_output.rs diff --git a/src/tui.rs b/deja_vu/src/tui.rs similarity index 100% rename from src/tui.rs rename to deja_vu/src/tui.rs diff --git a/include/drivers/README.md b/include/drivers/README.md new file mode 100644 index 0000000..84a33ad --- /dev/null +++ b/include/drivers/README.md @@ -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.. diff --git a/include/menu_entries/01_deja-vu.toml b/include/menu_entries/01_deja-vu.toml new file mode 100644 index 0000000..6899622 --- /dev/null +++ b/include/menu_entries/01_deja-vu.toml @@ -0,0 +1,4 @@ +name = 'Deja-Vu' +command = 'X:\tools\deja-vu.exe' +use_conemu = true +separator = false diff --git a/include/menu_entries/02_separator.toml b/include/menu_entries/02_separator.toml new file mode 100644 index 0000000..ed50f1c --- /dev/null +++ b/include/menu_entries/02_separator.toml @@ -0,0 +1,4 @@ +name = '' +command = '' +use_conemu = false +separator = true diff --git a/include/menu_entries/03_ntpwedit.toml b/include/menu_entries/03_ntpwedit.toml new file mode 100644 index 0000000..9b07df9 --- /dev/null +++ b/include/menu_entries/03_ntpwedit.toml @@ -0,0 +1,4 @@ +name = 'NTPWEdit' +command = 'X:\Program Files\NTPWEdit\ntpwedit.exe' +use_conemu = false +separator = false diff --git a/include/menu_entries/04_clone-tool.toml b/include/menu_entries/04_clone-tool.toml new file mode 100644 index 0000000..1fe2dd3 --- /dev/null +++ b/include/menu_entries/04_clone-tool.toml @@ -0,0 +1,4 @@ +name = 'Some Clone Tool' +command = 'X:\Program Files\Some\Tool.exe' +use_conemu = false +separator = false diff --git a/include/menu_entries/05_taskmgr.toml b/include/menu_entries/05_taskmgr.toml new file mode 100644 index 0000000..c33d623 --- /dev/null +++ b/include/menu_entries/05_taskmgr.toml @@ -0,0 +1,4 @@ +name = 'Task Manager' +command = 'X:\Windows\System32\taskmgr.exe' +use_conemu = false +separator = false diff --git a/include/pe-menu.cmd b/include/pe-menu.cmd new file mode 100644 index 0000000..eaf1715 --- /dev/null +++ b/include/pe-menu.cmd @@ -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 . + +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 diff --git a/include/pe-menu.toml b/include/pe-menu.toml new file mode 100644 index 0000000..45f8b6b --- /dev/null +++ b/include/pe-menu.toml @@ -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 . + +con_emu = 'X:\Program Files\ConEmu\ConEmu64.exe' +tools = [] diff --git a/pe_menu/Cargo.toml b/pe_menu/Cargo.toml new file mode 100644 index 0000000..fbe0de1 --- /dev/null +++ b/pe_menu/Cargo.toml @@ -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 . + +[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" diff --git a/pe_menu/src/app.rs b/pe_menu/src/app.rs new file mode 100644 index 0000000..fbe5c23 --- /dev/null +++ b/pe_menu/src/app.rs @@ -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 . +// +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); + } +} diff --git a/pe_menu/src/event.rs b/pe_menu/src/event.rs new file mode 100644 index 0000000..340e426 --- /dev/null +++ b/pe_menu/src/event.rs @@ -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 . +// +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 new file mode 100644 index 0000000..fed2d63 --- /dev/null +++ b/pe_menu/src/handler.rs @@ -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 . +// +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/lib.rs b/pe_menu/src/lib.rs new file mode 100644 index 0000000..02aac4f --- /dev/null +++ b/pe_menu/src/lib.rs @@ -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 . +// +/// 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; diff --git a/pe_menu/src/main.rs b/pe_menu/src/main.rs new file mode 100644 index 0000000..b7fab7a --- /dev/null +++ b/pe_menu/src/main.rs @@ -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 . +// +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(()) +} diff --git a/pe_menu/src/tui.rs b/pe_menu/src/tui.rs new file mode 100644 index 0000000..313108c --- /dev/null +++ b/pe_menu/src/tui.rs @@ -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 . +// +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 new file mode 100644 index 0000000..bdfdedf --- /dev/null +++ b/pe_menu/src/ui.rs @@ -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 . +// +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::::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); + } +}