From d7c070aad8be4af456631aef89731180bdd1de64 Mon Sep 17 00:00:00 2001
From: 2Shirt <2xShirt@gmail.com>
Date: Thu, 21 Nov 2024 05:34:10 -0800
Subject: [PATCH] Convert to workspace with both deja-vu and pe-menu
---
.envrc | 3 -
Cargo.lock | 112 +++++-
Cargo.toml | 54 +--
README.md | 10 +-
build_win_release.sh | 30 ++
deja_vu/.envrc | 3 +
deja_vu/Cargo.toml | 66 ++++
build.rs => deja_vu/build.rs | 0
{config => deja_vu/config}/config.json5 | 0
{src => deja_vu/src}/action.rs | 0
{src => deja_vu/src}/app.rs | 0
{src => deja_vu/src}/cli.rs | 0
{src => deja_vu/src}/components.rs | 0
{src => deja_vu/src}/components/footer.rs | 0
{src => deja_vu/src}/components/fps.rs | 0
{src => deja_vu/src}/components/left.rs | 0
{src => deja_vu/src}/components/popup.rs | 0
{src => deja_vu/src}/components/right.rs | 0
{src => deja_vu/src}/components/state.rs | 0
{src => deja_vu/src}/components/title.rs | 0
{src => deja_vu/src}/config.rs | 0
{src => deja_vu/src}/errors.rs | 0
{src => deja_vu/src}/logging.rs | 0
{src => deja_vu/src}/main.rs | 0
{src => deja_vu/src}/system.rs | 0
{src => deja_vu/src}/system/cpu.rs | 0
{src => deja_vu/src}/system/disk.rs | 0
{src => deja_vu/src}/system/diskpart.rs | 0
{src => deja_vu/src}/system/drivers.rs | 0
{src => deja_vu/src}/tasks.rs | 0
{src => deja_vu/src}/tests/mod.rs | 0
{src => deja_vu/src}/tests/sample_output.rs | 0
{src => deja_vu/src}/tui.rs | 0
include/drivers/README.md | 23 ++
include/menu_entries/01_deja-vu.toml | 4 +
include/menu_entries/02_separator.toml | 4 +
include/menu_entries/03_ntpwedit.toml | 4 +
include/menu_entries/04_clone-tool.toml | 4 +
include/menu_entries/05_taskmgr.toml | 4 +
include/pe-menu.cmd | 32 ++
include/pe-menu.toml | 17 +
pe_menu/Cargo.toml | 29 ++
pe_menu/src/app.rs | 366 ++++++++++++++++++++
pe_menu/src/event.rs | 116 +++++++
pe_menu/src/handler.rs | 61 ++++
pe_menu/src/lib.rs | 29 ++
pe_menu/src/main.rs | 52 +++
pe_menu/src/tui.rs | 111 ++++++
pe_menu/src/ui.rs | 127 +++++++
49 files changed, 1193 insertions(+), 68 deletions(-)
delete mode 100644 .envrc
create mode 100755 build_win_release.sh
create mode 100644 deja_vu/.envrc
create mode 100644 deja_vu/Cargo.toml
rename build.rs => deja_vu/build.rs (100%)
rename {config => deja_vu/config}/config.json5 (100%)
rename {src => deja_vu/src}/action.rs (100%)
rename {src => deja_vu/src}/app.rs (100%)
rename {src => deja_vu/src}/cli.rs (100%)
rename {src => deja_vu/src}/components.rs (100%)
rename {src => deja_vu/src}/components/footer.rs (100%)
rename {src => deja_vu/src}/components/fps.rs (100%)
rename {src => deja_vu/src}/components/left.rs (100%)
rename {src => deja_vu/src}/components/popup.rs (100%)
rename {src => deja_vu/src}/components/right.rs (100%)
rename {src => deja_vu/src}/components/state.rs (100%)
rename {src => deja_vu/src}/components/title.rs (100%)
rename {src => deja_vu/src}/config.rs (100%)
rename {src => deja_vu/src}/errors.rs (100%)
rename {src => deja_vu/src}/logging.rs (100%)
rename {src => deja_vu/src}/main.rs (100%)
rename {src => deja_vu/src}/system.rs (100%)
rename {src => deja_vu/src}/system/cpu.rs (100%)
rename {src => deja_vu/src}/system/disk.rs (100%)
rename {src => deja_vu/src}/system/diskpart.rs (100%)
rename {src => deja_vu/src}/system/drivers.rs (100%)
rename {src => deja_vu/src}/tasks.rs (100%)
rename {src => deja_vu/src}/tests/mod.rs (100%)
rename {src => deja_vu/src}/tests/sample_output.rs (100%)
rename {src => deja_vu/src}/tui.rs (100%)
create mode 100644 include/drivers/README.md
create mode 100644 include/menu_entries/01_deja-vu.toml
create mode 100644 include/menu_entries/02_separator.toml
create mode 100644 include/menu_entries/03_ntpwedit.toml
create mode 100644 include/menu_entries/04_clone-tool.toml
create mode 100644 include/menu_entries/05_taskmgr.toml
create mode 100644 include/pe-menu.cmd
create mode 100644 include/pe-menu.toml
create mode 100644 pe_menu/Cargo.toml
create mode 100644 pe_menu/src/app.rs
create mode 100644 pe_menu/src/event.rs
create mode 100644 pe_menu/src/handler.rs
create mode 100644 pe_menu/src/lib.rs
create mode 100644 pe_menu/src/main.rs
create mode 100644 pe_menu/src/tui.rs
create mode 100644 pe_menu/src/ui.rs
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);
+ }
+}