Compare commits

...

78 commits

Author SHA1 Message Date
9919a993a0
Merge branch 'win-installer' into dev 2025-12-15 13:38:25 -08:00
03030427e9
Add new fortune saying 2025-12-15 13:37:10 -08:00
3b975af2ba
Fix network share connection 2025-12-13 16:59:29 -08:00
b8fe43fd8f
Lock is Safe Mode only for backup restores 2025-12-13 16:59:19 -08:00
8495d62a06
Add unattend.xml sections 2025-12-13 16:58:47 -08:00
4a306b56d9
Fix zero-based vs one-based indexing 2025-12-13 16:57:10 -08:00
8a65313039
Fix bug dropping selected disk info
We don't need to reset disk_index_dest to None since it would be reset
before it's used anyway
2025-12-13 08:10:38 -08:00
e5f476f48d
Add more log/display info 2025-12-13 08:09:49 -08:00
a75911cb32
Add win-installer partitioning/formatting logic 2025-12-13 08:09:12 -08:00
cb7ba1a285
Invert backup/setup bool 2025-12-13 08:07:53 -08:00
87ccc80602
Track WIM scans to avoid stacking scans 2025-12-04 22:36:38 -08:00
c4c174b546
Handle missing WIM build/spbuild info
Needed for manual backup captures
2025-11-30 21:03:57 -08:00
4658624988
Add missing back keys and footer text 2025-11-30 21:03:24 -08:00
c572716ef9
Include size in WimImage 2025-11-30 20:46:55 -08:00
e873ec9602
Refactor WIM setup-image logic 2025-11-30 20:46:02 -08:00
09b204c0b0
Add option to include local backup WIMs 2025-11-29 05:41:08 -08:00
dd4733c991
Add username section 2025-11-29 03:25:22 -08:00
88dfe52b07
Add WIM Image selection section 2025-11-29 02:33:24 -08:00
89e768e3a4
Add WIM file selection section 2025-11-29 01:52:23 -08:00
70525ae6e0
Implement network WIM scan 2025-11-28 23:41:35 -08:00
a760773269
Add initial network scan code 2025-11-28 19:25:53 -08:00
cf3d71567e
Refactor scan_local_drives() 2025-11-28 19:25:18 -08:00
2aed8d130b
Make progress 2025-11-16 20:39:43 -08:00
3ce36c5a0f
Misc
???
2025-11-08 22:32:41 -08:00
e0932c7b48
Add first few screens 2025-11-08 21:46:31 -08:00
f51a4e85c4
Add more framework for workflow 2025-11-08 18:43:50 -08:00
cf87ac32af
Update Modes for win-installer 2025-11-08 17:48:17 -08:00
69c3feb838
Update WIM structs 2025-11-08 17:48:00 -08:00
94faae27ac
Add initial base for win-installer 2025-11-08 16:54:07 -08:00
7e12223344
Use separate State structs per app 2025-11-08 16:17:34 -08:00
ee7de8f355
Combine FPS and Title components 2025-11-01 22:22:58 -07:00
25b6fe4b7e
Add logo 2025-11-01 21:08:42 -07:00
b777a94c98
Apply fixes suggested by rust-analyzer
Apply more fixes
2025-11-01 21:08:42 -07:00
c789f51bac
Move boot scan queue code to new file
app.rs was just too long, no other reason for this.
2025-07-06 20:13:08 -07:00
827451322d
Update boot-diags menus 2025-07-05 17:38:44 -07:00
46e53ef105
Fix typo 2025-07-04 23:47:08 -07:00
3340114522
Expand LogView key handling 2025-07-04 23:46:53 -07:00
3fafcc4292
Remove debug code 2025-07-04 19:49:04 -07:00
1d0b692028
Prevent downgrading DiagResult in progress screen 2025-06-13 19:01:05 -07:00
185ddf3103
Show which registry hives are OK in summary 2025-06-13 19:00:04 -07:00
561d57d9a2
Allow returning to menu from BootSetup 2025-06-04 22:08:36 -07:00
fe12b3c4e2
Add logic to return to DiagMenu 2025-06-04 21:58:01 -07:00
710b9d7c16
Show disk details in DiagMenu 2025-06-04 21:52:40 -07:00
e55a254be9
Add registry section 2025-06-04 21:38:10 -07:00
b63927d3ef
Drop section used for debugging 2025-06-04 20:43:51 -07:00
9f0ecb9baf
Add BCD parsing logic 2025-06-04 20:38:04 -07:00
5ba30e626f
Show better results in the progress window 2025-06-04 20:37:39 -07:00
ee7147a187
Add parse functions for DISM and System Files 2025-06-04 19:52:35 -07:00
4fca7d6696
Add Bitlocker result parsing logic 2025-06-04 19:25:07 -07:00
0084b5968d
Move carriage return removal to its own function
Pretty sure I'll need it elsewhere
2025-06-04 19:24:22 -07:00
92dcfc2592
Clean up CHKDSK log to prevent rendering issues
CR escapes were causing text to render well outside the intended area.
2025-06-02 20:15:33 -07:00
92f7874584
Replace Results component with LogView
Required ignoring KeyUp/KeyDown handling in some cases so a new Mode was
added to accomodate
2025-06-01 17:25:47 -07:00
a9e929585f
Refactor diagnostic groups (yet again) 2025-06-01 16:07:51 -07:00
dc49006af8
Move set/refresh diskpart logic to Disk 2025-06-01 09:37:25 -07:00
88f1234dd0
Adjust look of progress window 2025-05-31 14:42:35 -07:00
718e4061f8
Add option to return to main menu from results 2025-05-31 14:42:08 -07:00
dfc861cc09
Reset progress/results before BootScan 2025-05-31 14:41:38 -07:00
52ccc154cc
Reset results key/line index on Action::SetMode 2025-05-27 19:18:01 -07:00
e0823293e2
Add initial results UI layout 2025-05-26 21:49:09 -07:00
2296b8f274
Major refactor around task groups 2025-05-25 01:11:24 -07:00
081fd22de1
Comment out or remove unused sections 2025-05-20 20:25:01 -07:00
b06c0db27d
Add fn add_group() to tasks
Allows for a better tracking of group start and end to update the
progress window
2025-05-20 20:18:45 -07:00
a2f61e310f
Refactor task sections
Still very much WIP
2025-05-20 19:24:19 -07:00
7b0deb4cc7
Add boot diagnostic sections
Still very much WIP
2025-05-11 20:20:08 -07:00
8f8f78a161
Add drive letters for fake disks
Should help with testing
2025-05-07 20:17:12 -07:00
b9ade066e7
Show current mode in title row
This may be later isolated to debug builds
2025-05-05 22:31:39 -07:00
713dc8c2de
Use Enum for left selection type 2025-05-05 22:06:49 -07:00
2aede33db6
Improve handling of pe tool toml files 2025-05-05 21:15:50 -07:00
15b5d5e131
Bug fix for get_disks()
In rare circumstances we could be omitting drives from the list
2025-04-21 00:13:04 -07:00
0a00e970aa
Drop once_cell dependency
Use OnceLock from std instead
2025-04-21 00:00:21 -07:00
f556703baf
Replace magic number with DEFAULT_MAX_DISCS 2025-04-20 23:36:34 -07:00
d0eecc36c5
Include core::tests by default 2025-04-20 23:33:12 -07:00
59b9ddbee6
Misc fixes
Code was written several weeks ago
2025-04-05 15:17:10 -07:00
7e08ca5f72
Add get_system32_path function 2025-04-05 15:12:30 -07:00
31d1391925
Address Clippy pedantic warnings for core 2025-03-22 21:23:03 -07:00
3c4603dc7b
Address Clippy warnings 2025-03-22 20:32:51 -07:00
5028e16c5b
Update Rust to 2024 version 2025-03-22 20:03:46 -07:00
6970f2db3e
Merge branch 'boot-diags' into dev 2025-03-22 19:59:26 -07:00
56 changed files with 4880 additions and 1300 deletions

476
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,9 @@
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
[workspace]
members = ["core", "boot_diags", "deja_vu", "pe_menu"]
default-members = ["boot_diags", "deja_vu", "pe_menu"]
members = ["core", "boot_diags", "deja_vu", "pe_menu", "win_installer"]
default-members = ["core", "boot_diags", "deja_vu", "pe_menu", "win_installer"]
resolver = "2"
[profile.release]
lto = true

View file

@ -16,7 +16,7 @@
[package]
name = "boot-diags"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2021"
edition = "2024"
license = "GPL"
version = "0.1.0"
@ -41,6 +41,7 @@ tracing = "0.1.41"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
check_elevation = "0.2.4"
regex = "1.11.1"
[build-dependencies]
anyhow = "1.0.86"

File diff suppressed because it is too large Load diff

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/>.
//
pub mod logview;
pub mod progress;

View file

@ -0,0 +1,168 @@
// 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 color_eyre::Result;
use ratatui::{
Frame,
layout::Rect,
widgets::{Block, Clear, Padding, Paragraph, Wrap},
};
use tokio::sync::mpsc::UnboundedSender;
use core::{
action::Action,
components::{Component, state::StatefulList},
config::Config,
state::Mode,
};
use std::{
cmp::min,
sync::{Arc, Mutex},
};
use crate::diags::DiagGroup;
#[derive(Default, Debug, Clone)]
pub struct LogView {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
line_index: u16,
list: StatefulList<String>,
mode: Mode,
diag_groups: Arc<Mutex<Vec<DiagGroup>>>,
}
impl LogView {
#[must_use]
pub fn new(diag_groups: Arc<Mutex<Vec<DiagGroup>>>) -> Self {
LogView {
diag_groups,
..Default::default()
}
}
}
impl Component for LogView {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::KeyUp => {
if self.mode == Mode::LogView {
if self.line_index > 0 {
self.line_index -= 1;
}
} else {
self.list.previous();
}
}
Action::KeyDown => {
if self.mode == Mode::LogView {
let new_index = self.line_index + 1;
if let Some(log_text) = self.list.get_selected() {
let lines: Vec<&str> = log_text.split('\n').collect();
if new_index as usize > lines.len() {
self.line_index = lines.len() as u16;
} else {
self.line_index = new_index;
}
}
} else {
self.list.next();
}
}
Action::KeyPageUp => {
if self.mode == Mode::LogView {
if self.line_index > 10 {
self.line_index -= 10;
} else {
self.line_index = 0;
}
}
}
Action::KeyPageDown => {
if self.mode == Mode::LogView {
let new_index = self.line_index + 10;
if let Some(log_text) = self.list.get_selected() {
let lines: Vec<&str> = log_text.split('\n').collect();
let new_index: u16 = min((lines.len() - 3) as u16, new_index);
self.line_index = new_index;
}
}
}
Action::KeyHome => {
if self.mode == Mode::LogView {
self.line_index = 0;
}
}
Action::KeyEnd => {
if self.mode == Mode::LogView
&& let Some(log_text) = self.list.get_selected()
{
let lines: Vec<&str> = log_text.split('\n').collect();
self.line_index = (lines.len() - 3) as u16;
}
}
Action::Process => {
if self.mode == Mode::LogView
&& let Some(command_tx) = self.command_tx.clone()
{
command_tx.send(Action::SetMode(Mode::BootDiags))?;
}
}
Action::SetMode(new_mode) => {
self.line_index = 0;
self.mode = new_mode;
if self.mode == Mode::BootDiags {
self.list.clear_items();
if let Ok(diag_groups) = self.diag_groups.lock() {
let raw_logs = diag_groups
.iter()
.map(|group| group.get_logs_raw())
.collect();
self.list.set_items(raw_logs);
}
}
}
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, rect: Rect) -> Result<()> {
if self.mode != Mode::LogView {
return Ok(());
}
if let Some(log_text) = self.list.get_selected() {
let paragraph = Paragraph::new(log_text)
.wrap(Wrap { trim: true })
.scroll((self.line_index, 0))
.block(Block::bordered().padding(Padding::horizontal(1)));
frame.render_widget(Clear, rect);
frame.render_widget(paragraph, rect);
}
Ok(())
}
}

View file

@ -0,0 +1,172 @@
// 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 color_eyre::Result;
use ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
};
use tracing::info;
use core::{
action::{Action, DiagResult},
components::Component,
state::Mode,
};
#[derive(Debug, Clone)]
struct ProgressLine {
name: String,
text: String,
result: DiagResult,
running: bool,
}
impl ProgressLine {
pub fn len_name(&self) -> usize {
self.name.chars().count()
}
}
#[derive(Default, Debug, Clone)]
pub struct Progress {
// command_tx: Option<UnboundedSender<Action>>,
// config: Config,
lines: Vec<ProgressLine>,
mode: Mode,
}
impl Progress {
#[must_use]
pub fn new() -> Self {
Self::default()
}
// fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
// self.command_tx = Some(tx);
// Ok(())
// }
// fn register_config_handler(&mut self, config: Config) -> Result<()> {
// self.config = config;
// Ok(())
// }
}
impl Component for Progress {
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::DiagLineStart { text } => {
info!("Caught Action::DiagLineStart {{ \"{}\" }}", &text);
self.lines.push(ProgressLine {
name: text,
text: String::from("OK"),
result: DiagResult::Pass,
running: true,
});
}
Action::DiagLineUpdate { result, text } => {
info!(
"Caught Action::DiagLineUpdate {{ {}, \"{}\" }}",
&result, &text
);
if let Some(line) = self.lines.last_mut() {
let old_result = line.result.clone();
match (old_result, result.clone()) {
(DiagResult::Pass, _) => {
line.result = result;
line.text = text;
}
(DiagResult::Warn, DiagResult::Pass | DiagResult::Warn) => {
line.text = String::from("Unknown");
}
(DiagResult::Warn, DiagResult::Fail) => {
line.result = DiagResult::Fail;
line.text = String::from("Unknown");
}
(DiagResult::Fail, _) => {
line.text = String::from("Unknown");
}
};
}
}
Action::DiagLineEnd { text } => {
info!("Caught Action::DiagLineEnd {{ \"{}\" }}", &text);
if let Some(line) = self.lines.last_mut() {
line.running = false;
}
}
Action::SetMode(mode) => {
self.mode = mode;
self.lines.clear();
}
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::BootScan {
return Ok(());
}
let mut body_text = Vec::with_capacity(20); // TODO: Needs fine tuning?
body_text.push(Line::default());
// Add line(s)
self.lines.iter().for_each(|line| {
//.for_each(|(part, color)| spans.push(Span::styled(part, Style::default().fg(color))));
//Line::from(spans)
let color = match line.result {
DiagResult::Pass => Color::Green,
DiagResult::Fail => Color::Red,
DiagResult::Warn => Color::Yellow,
};
let text = if line.running || line.text.is_empty() {
String::from("..")
} else {
line.text.clone()
};
let text = format!(" [ {text} ] ");
let width = area.width as usize - line.len_name() - 1 - text.chars().count() - 7; // ' [ ]'
let spans = vec![
Span::raw(format!("{} {:.<width$}", &line.name, "")),
Span::styled(text, Style::default().fg(color)),
];
body_text.push(Line::from(spans).centered());
body_text.push(Line::default());
body_text.push(Line::default());
});
// Build block
let outer_block = Block::default()
.borders(Borders::ALL)
.style(Style::default().bold());
let inner_block = Block::default()
.borders(Borders::NONE)
.style(Style::default().bold())
.title("Progress")
.title_alignment(Alignment::Center);
let inner = outer_block.inner(area);
let body = Paragraph::new(body_text).block(inner_block);
frame.render_widget(Clear, area);
frame.render_widget(outer_block, area);
frame.render_widget(body, inner);
Ok(())
}
}

View file

@ -13,90 +13,514 @@
// 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::collections::HashMap;
use core::{action::DiagResult, line::DVLine, tasks::TaskResult};
use std::{fmt, sync::OnceLock, thread::sleep, time::Duration};
use ratatui::style::Color;
use regex::Regex;
use tracing::{info, warn};
pub struct RegexList {
bitlocker_locked: OnceLock<Regex>,
bitlocker_off: OnceLock<Regex>,
carriage_returns: OnceLock<Regex>,
chkdsk_splits: OnceLock<Regex>,
}
impl RegexList {
fn bitlocker_locked(&self) -> &Regex {
self.bitlocker_locked
.get_or_init(|| Regex::new(r"Lock Status:\s+Locked").unwrap())
}
fn bitlocker_off(&self) -> &Regex {
self.bitlocker_off
.get_or_init(|| Regex::new(r"Protection Status:\s+Protection Off").unwrap())
}
fn carriage_returns(&self) -> &Regex {
self.carriage_returns
.get_or_init(|| Regex::new(r"^.*\r").unwrap())
}
fn chkdsk_splits(&self) -> &Regex {
self.chkdsk_splits.get_or_init(|| {
Regex::new(
r"(?m)^(WARNING|Stage |Windows |\d+.* total disk space|.*each allocation unit|Total duration)",
)
.unwrap()
})
}
}
static REGEXES: RegexList = RegexList {
bitlocker_locked: OnceLock::new(),
bitlocker_off: OnceLock::new(),
carriage_returns: OnceLock::new(),
chkdsk_splits: OnceLock::new(),
};
#[derive(Clone, Copy, Debug)]
pub enum Type {
Bitlocker,
BootConfigData,
FileSystem,
FilesAndFolders,
CheckDisk,
ComponentStore,
Registry,
SystemFiles,
Unknown,
}
#[derive(Debug)]
pub struct Groups {
items: HashMap<String, Line>,
order: Vec<String>,
}
impl Groups {
pub fn new() -> Self {
Groups {
items: HashMap::new(),
order: Vec::new(),
}
}
pub fn get(&self) -> Vec<&Line> {
let mut lines = Vec::new();
self.order.iter().for_each(|key| {
if let Some(line) = self.items.get(key) {
lines.push(line);
}
});
lines
}
pub fn update(&mut self, title: String, passed: bool, info: String) {
if let Some(line) = self.items.get_mut(&title) {
line.update(passed, info);
} else {
self.order.push(title.clone());
self.items.insert(
title.clone(),
Line {
title,
passed,
info: vec![info],
},
);
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Type::Bitlocker => write!(f, "Bitlocker"),
Type::BootConfigData => write!(f, "Boot Files"),
Type::CheckDisk => write!(f, "CHKDSK"),
Type::ComponentStore => write!(f, "DISM ScanHealth"),
Type::Registry => write!(f, "Registry"),
Type::SystemFiles => write!(f, "System Files"),
Type::Unknown => write!(f, "Unknown Type"),
}
}
}
#[derive(Clone, Debug)]
pub struct Line {
pub title: String,
pub passed: bool,
pub info: Vec<String>,
pub struct Log {
pub label: String,
pub summary: Vec<DVLine>,
pub raw: String,
}
impl Line {
pub fn update(&mut self, passed: bool, info: String) {
self.passed &= passed; // We fail if any tests in this group fail
self.info.push(info);
#[derive(Clone, Debug)]
pub struct DiagGroup {
pub diag_type: Type,
pub passed: Vec<bool>,
pub logs: Vec<Log>,
pub result: String,
}
impl DiagGroup {
pub fn new(diag_type: Type) -> Self {
DiagGroup {
diag_type,
passed: Vec::new(),
logs: Vec::new(),
result: String::new(),
}
}
pub fn get_type(cmd_name: &str) -> Type {
if cmd_name == "exa" {
return Type::BootConfigData;
pub fn get_logs_raw(&self) -> String {
let raw_logs: Vec<String> = self
.logs
.iter()
.map(|log| format!("-- {} --\n{}", &log.label, &log.raw))
.collect();
raw_logs.join("\n\n\n")
}
if cmd_name == "bcdedit.exe" {
return Type::BootConfigData;
pub fn get_logs_summary(&self) -> Vec<DVLine> {
let mut summaries: Vec<DVLine> = Vec::new();
self.logs
.iter()
.for_each(|log| summaries.extend(log.summary.clone()));
summaries
}
if cmd_name == "dir" {
return Type::FilesAndFolders;
pub fn get_pass_fail_warn(&self) -> DiagResult {
let all_passed = self.passed.iter().all(|result| *result);
let all_failed = self.passed.iter().all(|result| !*result);
if all_passed {
DiagResult::Pass
} else if all_failed {
DiagResult::Fail
} else {
DiagResult::Warn
}
if cmd_name == "reg.exe" {
return Type::Registry;
}
if cmd_name == "chkdsk.exe" {
return Type::FileSystem;
}
if cmd_name == "manage-bde.exe" {
return Type::Bitlocker;
}
pub fn get_diag_type(label: &str) -> Type {
info!("Getting Diag type for {label}");
match label {
"Bitlocker" => Type::Bitlocker,
"Boot Files" => Type::BootConfigData,
"DISM ScanHealth" => Type::ComponentStore,
"CHKDSK" => Type::CheckDisk,
"Registry" => Type::Registry,
"System Files" => Type::SystemFiles,
_ => {
warn!("Failed to determine type");
Type::Unknown
}
}
}
pub fn parse_bcd(diag_group: &mut DiagGroup, task_result: TaskResult) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return;
}
match task_result {
TaskResult::Error(err) => {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("BCD"),
summary: vec![DVLine {
line_parts: vec![String::from("BCD: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: err,
});
}
TaskResult::Output(stdout, stderr, passed) => {
let output_text = if stderr.is_empty() {
stdout.clone()
} else {
format!("{stdout}\n\n-------\n\n{stderr}")
};
if passed {
diag_group.passed.push(true);
diag_group.result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("BCD"),
summary: vec![DVLine {
line_parts: vec![String::from("BCD: "), String::from("OK")],
line_colors: vec![Color::Reset, Color::Green],
}],
raw: output_text,
});
} else {
diag_group.passed.push(false);
diag_group.result = String::from("Unknown");
diag_group.logs.push(Log {
label: String::from("BCD"),
summary: vec![DVLine {
line_parts: vec![String::from("BCD: "), String::from("Unknown")],
line_colors: vec![Color::Reset, Color::Yellow],
}],
raw: output_text,
});
}
}
}
}
pub fn parse_bitlocker(diag_group: &mut DiagGroup, task_result: TaskResult) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return;
}
let re_bitlocker_locked = REGEXES.bitlocker_locked();
let re_bitlocker_off = REGEXES.bitlocker_off();
match task_result {
TaskResult::Error(err) => {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("Bitlocker"),
summary: vec![DVLine {
line_parts: vec![String::from("Bitlocker: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: err,
});
}
TaskResult::Output(stdout, stderr, _) => {
let output_text = if stderr.is_empty() {
stdout.clone()
} else {
format!("{stdout}\n\n-------\n\n{stderr}")
};
let result: String;
let set_result_text = !output_text.contains("All Key Protectors");
if re_bitlocker_off.is_match(&output_text) {
diag_group.passed.push(true);
result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("Bitlocker"),
summary: vec![DVLine {
line_parts: vec![String::from("Bitlocker: "), String::from("OFF")],
line_colors: vec![Color::Reset, Color::Green],
}],
raw: output_text,
});
} else if re_bitlocker_locked.is_match(&output_text) {
diag_group.passed.push(false);
result = String::from("Locked");
diag_group.logs.push(Log {
label: String::from("Bitlocker"),
summary: vec![DVLine {
line_parts: vec![String::from("Bitlocker: "), String::from("Locked")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: output_text,
});
} else {
diag_group.passed.push(true);
result = String::from("Unknown");
diag_group.logs.push(Log {
label: String::from("Bitlocker"),
summary: Vec::new(),
raw: output_text,
});
}
if set_result_text {
diag_group.result = result;
}
}
}
}
pub fn parse_chkdsk(diag_group: &mut DiagGroup, task_result: TaskResult) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return;
}
match task_result {
TaskResult::Error(err) => {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("CHKDSK"),
summary: vec![DVLine {
line_parts: vec![String::from("CHKDSK: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: err,
});
}
TaskResult::Output(stdout, stderr, _) => {
let re_chkdsk_splits = REGEXES.chkdsk_splits();
let output_text = if stderr.is_empty() {
stdout.clone()
} else {
format!("{stdout}\n\n-------\n\n{stderr}")
};
let output_text = remove_carriage_returns(&output_text);
let parsed_output = re_chkdsk_splits
.replace_all(output_text.as_str(), "\n $1")
.to_string();
if parsed_output.contains("Windows has scanned the file system and found no problems.")
{
diag_group.passed.push(true);
diag_group.result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("CHKDSK"),
summary: vec![DVLine {
line_parts: vec![String::from("CHKDSK: "), String::from("OK")],
line_colors: vec![Color::Reset, Color::Green],
}],
raw: parsed_output,
});
} else {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("CHKDSK"),
summary: vec![DVLine {
line_parts: vec![String::from("CHKDSK: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: parsed_output,
});
}
}
}
}
pub fn parse_dism(diag_group: &mut DiagGroup, task_result: TaskResult) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return;
}
match task_result {
TaskResult::Error(err) => {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("DISM"),
summary: vec![DVLine {
line_parts: vec![String::from("DISM: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: err,
});
}
TaskResult::Output(stdout, stderr, _) => {
let output_text = if stderr.is_empty() {
stdout.clone()
} else {
format!("{stdout}\n\n-------\n\n{stderr}")
};
if output_text.contains("No component store corruption detected") {
diag_group.passed.push(true);
diag_group.result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("DISM"),
summary: vec![DVLine {
line_parts: vec![String::from("DISM: "), String::from("OK")],
line_colors: vec![Color::Reset, Color::Green],
}],
raw: output_text,
});
} else {
diag_group.passed.push(false);
diag_group.result = String::from("Unknown");
diag_group.logs.push(Log {
label: String::from("DISM"),
summary: vec![DVLine {
line_parts: vec![String::from("DISM: "), String::from("Unknown")],
line_colors: vec![Color::Reset, Color::Yellow],
}],
raw: output_text,
});
}
}
}
}
pub fn parse_registry_hives(
diag_group: &mut DiagGroup,
task_result: TaskResult,
cmd_args: Vec<String>,
) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return;
}
let hive = cmd_args.get(2);
match task_result {
TaskResult::Error(err) => {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("Registry"),
summary: if hive.is_some() {
vec![DVLine {
line_parts: vec![
format!("Registry ({}): ", hive.unwrap()),
String::from("Error"),
],
line_colors: vec![Color::Reset, Color::Red],
}]
} else {
Vec::new()
},
raw: err,
});
}
TaskResult::Output(stdout, stderr, passed) => {
let output_text = if stderr.is_empty() {
stdout.clone()
} else {
format!("{stdout}\n\n{stderr}")
};
if passed {
diag_group.passed.push(true);
diag_group.result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("Registry"),
summary: if hive.is_some() {
vec![DVLine {
line_parts: vec![
format!("Registry ({}): ", hive.unwrap()),
String::from("OK"),
],
line_colors: vec![Color::Reset, Color::Green],
}]
} else {
Vec::new()
},
raw: output_text,
});
} else {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("Registry"),
summary: if hive.is_some() {
vec![DVLine {
line_parts: vec![
format!("Registry ({}): ", hive.unwrap()),
String::from("Error"),
],
line_colors: vec![Color::Reset, Color::Red],
}]
} else {
Vec::new()
},
raw: output_text,
});
}
}
}
}
pub fn parse_system_files(diag_group: &mut DiagGroup, task_result: TaskResult) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return;
}
match task_result {
TaskResult::Error(err) => {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("System Files"),
summary: vec![DVLine {
line_parts: vec![String::from("System Files: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: err,
});
}
TaskResult::Output(stdout, stderr, passed) => {
let output_text = if stderr.is_empty() {
stdout.clone()
} else {
format!("{stdout}\n\n{stderr}")
};
if passed {
diag_group.passed.push(true);
diag_group.result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("System Files"),
summary: vec![DVLine {
line_parts: vec![String::from("System Files: "), String::from("OK")],
line_colors: vec![Color::Reset, Color::Green],
}],
raw: output_text,
});
} else {
diag_group.passed.push(false);
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("System Files"),
summary: vec![DVLine {
line_parts: vec![String::from("System Files: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: output_text,
});
}
}
}
}
pub fn remove_carriage_returns(text: &str) -> String {
let re_carriage_returns = REGEXES.carriage_returns();
let parsed_lines: Vec<String> = text
.split("\r\n")
.filter_map(|line| {
let line = re_carriage_returns.replace_all(line, "").trim().to_string();
if line.is_empty() { None } else { Some(line) }
})
.collect();
parsed_lines.join("\n")
}

View file

@ -15,12 +15,14 @@
//
use clap::Parser;
use color_eyre::Result;
use core;
use crate::app::App;
mod app;
mod components;
mod diags;
mod scan;
mod state;
#[tokio::main]
async fn main() -> Result<()> {

184
boot_diags/src/scan.rs Normal file
View file

@ -0,0 +1,184 @@
// 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 color_eyre::Result;
use core::system::disk::PartitionTableType;
use core::tasks::Tasks;
use core::{action::Action, tasks::TaskType};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use crate::diags::{DiagGroup, Type as DiagType};
use crate::state::State;
pub fn queue_boot_scan_tasks(
action_tx: mpsc::UnboundedSender<Action>,
diag_groups: Arc<Mutex<Vec<DiagGroup>>>,
state: &State,
system32: String,
tasks: &mut Tasks,
) -> Result<()> {
if let Ok(mut diag_groups) = diag_groups.lock() {
diag_groups.clear();
}
let disk_list = state.disk_list.lock().unwrap();
if let Some(disk_index) = state.disk_index_dest
&& let Some(disk) = disk_list.get(disk_index)
{
let table_type = disk.part_type.clone();
let letter_boot = disk.get_part_letter(state.part_index_boot.unwrap());
let letter_os = disk.get_part_letter(state.part_index_os.unwrap());
// Safety check
if letter_os.is_empty() {
if letter_boot.is_empty() {
action_tx.send(Action::Error(String::from(
"ERROR\n\n\nFailed to get drive letters for the boot and OS volumes",
)))?;
} else {
action_tx.send(Action::Error(String::from(
"ERROR\n\n\nFailed to get drive letter for the OS volume",
)))?;
}
return Ok(());
}
// BCD
if !letter_boot.is_empty() {
tasks.add_group(
DiagType::BootConfigData.to_string().as_str(),
vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\bcdedit.exe", &system32)),
vec![
String::from("/store"),
format!(
"{letter_boot}:{}\\Boot\\BCD",
if table_type == PartitionTableType::Guid {
"\\EFI\\Microsoft"
} else {
""
}
),
String::from("/enum"),
],
)],
);
}
// Bitlocker
tasks.add_group(
DiagType::Bitlocker.to_string().as_str(),
vec![
TaskType::CommandWait(
PathBuf::from(format!("{}\\manage-bde.exe", &system32)),
vec![String::from("-status"), format!("{letter_os}:")],
),
TaskType::CommandWait(
PathBuf::from(format!("{}\\manage-bde.exe", &system32)),
vec![
String::from("-protectors"),
String::from("-get"),
format!("{letter_os}:"),
],
),
],
);
// Filesystem Health
tasks.add_group(
DiagType::CheckDisk.to_string().as_str(),
vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\chkdsk.exe", &system32)),
vec![format!("{letter_os}:")],
)],
);
// DISM Health
tasks.add_group(
DiagType::ComponentStore.to_string().as_str(),
vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\dism.exe", &system32)),
vec![
format!("/Image:{letter_os}:"),
String::from("/Cleanup-Image"),
String::from("/ScanHealth"),
],
)],
);
// Critical Files/Folders
let paths: Vec<PathBuf> = [
// Files/Folders
"Users",
"Program Files",
"Program Files (x86)",
"ProgramData",
"Windows\\System32\\config",
]
.iter()
.map(|s| PathBuf::from(format!("{letter_os}:\\{s}")))
.collect();
tasks.add_group(
DiagType::SystemFiles.to_string().as_str(),
vec![TaskType::TestPaths(paths)],
);
// Registry
tasks.add_group(
DiagType::Registry.to_string().as_str(),
vec![
TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &system32)),
vec![
String::from("load"),
String::from("HKLM\\TmpSoftware"),
format!("{letter_os}:\\Windows\\System32\\config\\SOFTWARE"),
],
),
TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &system32)),
vec![
String::from("load"),
String::from("HKLM\\TmpSystem"),
format!("{letter_os}:\\Windows\\System32\\config\\SYSTEM"),
],
),
TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &system32)),
vec![
String::from("load"),
String::from("HKU\\TmpDefault"),
format!("{letter_os}:\\Windows\\System32\\config\\DEFAULT"),
],
),
TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &system32)),
vec![String::from("unload"), String::from("HKLM\\TmpSoftware")],
),
TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &system32)),
vec![String::from("unload"), String::from("HKLM\\TmpSystem")],
),
TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &system32)),
vec![String::from("unload"), String::from("HKU\\TmpDefault")],
),
],
);
tasks.add(TaskType::Sleep); // NOTE: DELETEME
}
Ok(())
}

42
boot_diags/src/state.rs Normal file
View file

@ -0,0 +1,42 @@
// 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::sync::{Arc, Mutex};
use core::system::{disk::Disk, drivers};
#[derive(Debug, Default)]
pub struct State {
pub disk_index_dest: Option<usize>,
pub disk_list: Arc<Mutex<Vec<Disk>>>,
pub driver_list: Vec<drivers::Driver>,
pub part_index_boot: Option<usize>,
pub part_index_os: Option<usize>,
pub driver: Option<drivers::Driver>,
}
impl State {
pub fn new(disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
State {
disk_list,
..Default::default()
}
}
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
}

View file

@ -109,7 +109,9 @@
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<m>": "DiagMainMenu",
"<r>": "BootScan",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
@ -125,6 +127,22 @@
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<m>": "DiagMainMenu",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"LogView": {
"<Enter>": "Process",
"<Esc>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<PageUp>": "KeyPageUp",
"<PageDown>": "KeyPageDown",
"<Home>": "KeyHome",
"<End>": "KeyEnd",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
@ -134,6 +152,14 @@
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<m>": "DiagMainMenu",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"Process": {
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
@ -143,6 +169,8 @@
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<m>": "DiagMainMenu",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
@ -160,5 +188,44 @@
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"ScanWinSources": {
"<Enter>": "Process",
"<b>": "FindWimBackups",
"<n>": "FindWimNetwork",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SelectWinSource": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<b>": "PrevScreen",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SelectWinImage": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<b>": "PrevScreen",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SetUserName": {
"<Esc>": "PrevScreen",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
},
"network_server": "SERVER",
"network_share": "SHARE",
"network_user": "USER",
"network_pass": "PASS"
}

83
config/unattend.xml Executable file
View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="disabled">
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" language="neutral" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS">
<UserData>
<ProductKey>
<Key />
</ProductKey>
</UserData>
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>2</Order>
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>3</Order>
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" language="neutral" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" language="neutral" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS">
<OOBE>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<UserAccounts>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Name>NEWUSERNAME</Name>
<DisplayName>NEWUSERNAME</DisplayName>
<Group>Administrators;Power Users</Group>
<Password>
<Value>UABhAHMAcwB3AG8AcgBkAA==</Value>
<PlainText>false</PlainText>
</Password>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>net user &quot;NEWUSERNAME&quot; /expires:never</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<CommandLine>net user &quot;NEWUSERNAME&quot; /passwordchg:yes</CommandLine>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<CommandLine>net user &quot;NEWUSERNAME&quot; /passwordreq:no</CommandLine>
</SynchronousCommand>
</FirstLogonCommands>
</component>
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" language="neutral" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS">
<InputLocale>00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UserLocale>en-US</UserLocale>
<UILanguage>en-US</UILanguage>
<UILanguageFallback></UILanguageFallback>
</component>
<component name="Microsoft-Windows-SecureStartup-FilterDriver" processorArchitecture="amd64" language="neutral" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS">
<PreventDeviceEncryption>true</PreventDeviceEncryption>
</component>
<component name="Microsoft-Windows-EnhancedStorage-Adm" processorArchitecture="amd64" language="neutral" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS">
<TCGSecurityActivationDisabled>1</TCGSecurityActivationDisabled>
</component>
</settings>
</unattend>

View file

@ -16,7 +16,7 @@
[package]
name = "core"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2021"
edition = "2024"
license = "GPL"
version = "0.2.0"
@ -40,7 +40,6 @@ 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"
rand = "0.9.0"
ratatui = { version = "0.29.0", features = ["serde", "macros"] }

View file

@ -16,12 +16,28 @@
use serde::{Deserialize, Serialize};
use strum::Display;
use crate::{components::popup::Type, line::DVLine, state::Mode, system::disk::Disk};
use crate::{
components::{left::SelectionType, popup::Type as PopupType},
line::DVLine,
state::Mode,
system::disk::Disk,
};
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum DiagResult {
Pass,
Fail,
Warn,
}
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum Action {
// App (Boot-Diags)
BootScan,
DiagLineStart { text: String },
DiagLineUpdate { result: DiagResult, text: String },
DiagLineEnd { text: String },
DiagMainMenu,
// App (Clone)
Highlight(usize),
InstallDriver,
@ -29,22 +45,23 @@ pub enum Action {
ScanDisks,
Select(Option<usize>, Option<usize>), // indicies for (source, dest) etc
SelectRight(Option<usize>, Option<usize>), // indicies for right info pane
TaskGroupStart(String),
TasksComplete,
UpdateDiskList(Vec<Disk>),
UpdateFooter(String),
UpdateLeft(String, Vec<String>, Vec<String>, usize), // (title, labels, items, select_num)
// NOTE: select_num should be set to 0, 1, or 2.
// 0: For repeating selections
// 1: For a single choice
// 2: For two selections (obviously)
UpdateLeft(String, Vec<String>, Vec<String>, SelectionType), // (title, labels, items, select_type)
UpdateRight(Vec<Vec<DVLine>>, usize, Vec<Vec<DVLine>>), // (labels, start_index, items) - items before start are always shown
// App (PE-Menu)
OpenTerminal,
Restart,
Shutdown,
// App (Win-Installer)
FindWimBackups,
FindWimNetwork,
SetUserName(String),
// Screens
DismissPopup,
DisplayPopup(Type, String),
DisplayPopup(PopupType, String),
NextScreen,
PrevScreen,
SetMode(Mode),
@ -54,6 +71,12 @@ pub enum Action {
Help,
KeyDown,
KeyUp,
KeyLeft,
KeyRight,
KeyPageUp,
KeyPageDown,
KeyHome,
KeyEnd,
Quit,
Render,
Resize(u16, u16),

View file

@ -38,6 +38,7 @@ const VERSION_MESSAGE: &str = concat!(
")"
);
#[must_use]
pub fn version() -> String {
let author = clap::crate_authors!();

View file

@ -12,19 +12,19 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
use color_eyre::Result;
use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{
layout::{Rect, Size},
Frame,
layout::{Rect, Size},
};
use tokio::sync::mpsc::UnboundedSender;
use crate::{action::Action, config::Config, tui::Event};
pub mod footer;
pub mod fps;
pub mod left;
pub mod popup;
pub mod right;

View file

@ -31,6 +31,7 @@ pub struct Footer {
}
impl Footer {
#[must_use]
pub fn new() -> Self {
Self {
text: String::from("(q) to quit"),
@ -51,6 +52,7 @@ impl Component for Footer {
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
#[allow(clippy::single_match)]
match action {
Action::UpdateFooter(text) => self.text = text,
_ => {}

View file

@ -1,113 +0,0 @@
// This file is part of Deja-Vu.
//
// Deja-Vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-Vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
use std::time::Instant;
use color_eyre::Result;
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Style, Stylize},
text::Span,
widgets::Paragraph,
Frame,
};
use super::Component;
use crate::action::Action;
#[derive(Debug, Clone, PartialEq)]
pub struct FpsCounter {
last_tick_update: Instant,
tick_count: u32,
ticks_per_second: f64,
last_frame_update: Instant,
frame_count: u32,
frames_per_second: f64,
}
impl Default for FpsCounter {
fn default() -> Self {
Self::new()
}
}
impl FpsCounter {
pub fn new() -> Self {
Self {
last_tick_update: Instant::now(),
tick_count: 0,
ticks_per_second: 0.0,
last_frame_update: Instant::now(),
frame_count: 0,
frames_per_second: 0.0,
}
}
fn app_tick(&mut self) -> Result<()> {
self.tick_count += 1;
let now = Instant::now();
let elapsed = (now - self.last_tick_update).as_secs_f64();
if elapsed >= 1.0 {
self.ticks_per_second = self.tick_count as f64 / elapsed;
self.last_tick_update = now;
self.tick_count = 0;
}
Ok(())
}
fn render_tick(&mut self) -> Result<()> {
self.frame_count += 1;
let now = Instant::now();
let elapsed = (now - self.last_frame_update).as_secs_f64();
if elapsed >= 1.0 {
self.frames_per_second = self.frame_count as f64 / elapsed;
self.last_frame_update = now;
self.frame_count = 0;
}
Ok(())
}
}
impl Component for FpsCounter {
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::Tick => self.app_tick()?,
Action::Render => self.render_tick()?,
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [column, _] =
Layout::horizontal([Constraint::Min(1), Constraint::Length(2)]).areas(area);
let [_, row, _] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.areas(column);
let message = format!(
"{:.2} ticks/sec, {:.2} FPS",
self.ticks_per_second, self.frames_per_second
);
let span = Span::styled(message, Style::new().dim());
let paragraph = Paragraph::new(span).right_aligned();
frame.render_widget(paragraph, row);
Ok(())
}
}

View file

@ -19,10 +19,19 @@ use ratatui::{
prelude::*,
widgets::{Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph},
};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender;
use super::{state::StatefulList, Component};
use crate::{action::Action, config::Config};
use super::{Component, state::StatefulList};
use crate::{action::Action, config::Config, state::Mode};
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum SelectionType {
#[default]
Loop,
One,
Two,
}
#[derive(Default)]
pub struct Left {
@ -30,17 +39,19 @@ pub struct Left {
config: Config,
labels: Vec<String>,
list: StatefulList<String>,
select_num: usize,
mode: Mode,
select_type: SelectionType,
selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>,
title_text: String,
}
impl Left {
#[must_use]
pub fn new() -> Self {
Self {
select_num: 0,
labels: vec![String::from("one"), String::from("two")],
select_type: SelectionType::Loop,
selections: vec![None, None],
selections_saved: vec![None, None],
title_text: String::from("Home"),
@ -72,17 +83,29 @@ impl Component for Left {
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::Highlight(index) => self.set_highlight(index),
Action::KeyUp => self.list.previous(),
Action::KeyDown => self.list.next(),
Action::KeyUp => {
if self.mode != Mode::LogView {
self.list.previous();
}
}
Action::KeyDown => {
if self.mode != Mode::LogView {
self.list.next();
}
}
Action::Process => {
if self.select_num == 0 {
if self.mode == Mode::LogView {
// Avoid updating selections/etc while log is open
return Ok(None);
}
if self.select_type == SelectionType::Loop {
// Selections aren't being used so this is a no-op
} else if let Some(command_tx) = self.command_tx.clone() {
match (self.selections[0], self.selections[1]) {
(None, None) => {
// Making first selection
command_tx.send(Action::Select(self.list.selected(), None))?;
if self.select_num == 1 {
if self.select_type == SelectionType::One {
// Confirm selection
command_tx.send(Action::NextScreen)?;
}
@ -113,18 +136,19 @@ impl Component for Left {
self.selections_saved[0] = one;
self.selections_saved[1] = two;
}
Action::SetMode(_) => {
Action::SetMode(new_mode) => {
self.mode = new_mode;
self.selections[0] = None;
self.selections[1] = None;
}
Action::UpdateLeft(title, labels, items, select_num) => {
Action::UpdateLeft(title, labels, items, select_type) => {
self.title_text = title;
self.labels = labels
.iter()
.map(|label| format!(" ~{}~", label.to_lowercase()))
.collect();
self.list.set_items(items);
self.select_num = select_num;
self.select_type = select_type;
}
_ => {}
}
@ -167,7 +191,7 @@ impl Component for Left {
.map(|(index, item)| {
let mut style = Style::default();
let text = if self.selections[0].is_some_and(|first_index| first_index == index) {
if let Some(label) = self.labels.get(0) {
if let Some(label) = self.labels.first() {
style = style.yellow();
label.as_str()
} else {

View file

@ -22,6 +22,7 @@ use ratatui::{
use serde::{Deserialize, Serialize};
use strum::Display;
use tokio::sync::mpsc::UnboundedSender;
use tracing::info;
use super::Component;
use crate::{action::Action, config::Config};
@ -43,6 +44,7 @@ pub struct Popup {
}
impl Popup {
#[must_use]
pub fn new() -> Self {
Self::default()
}
@ -63,6 +65,7 @@ impl Component for Popup {
match action {
Action::DismissPopup => self.popup_text.clear(),
Action::DisplayPopup(new_type, new_text) => {
info!("Show Popup ({new_type}): {new_text}");
self.popup_type = new_type;
self.popup_text = format!("\n{new_text}");
}
@ -97,6 +100,7 @@ impl Component for Popup {
}
}
#[must_use]
pub fn fortune() -> String {
String::from(match random::<u8>() / 4 {
0 => "FUN FACT\n\n\nComputers barely work.",
@ -109,6 +113,7 @@ pub fn fortune() -> String {
7 => "TIP OF THE DAY\n\n\nNever go full Snappy!",
8 => "WORDS OF WISDOM\n\n\n\nIts not DNS,\n\nTheres no way its DNS,\n\nIt was DNS.",
9 => "HAL 9000\n\n\n\"I'm sorry Dave, I'm afraid I can't do that.\"",
10 => "\n\n\nIt's now safe to turn off your computer.",
_ => "COMPLETE\n\n\nThank you for using this tool!",
})
}

View file

@ -21,8 +21,8 @@ use ratatui::{
};
use tokio::sync::mpsc::UnboundedSender;
use super::{state::StatefulList, Component};
use crate::{action::Action, config::Config, line::DVLine};
use super::{Component, state::StatefulList};
use crate::{action::Action, config::Config, line::DVLine, state::Mode};
#[derive(Default)]
pub struct Right {
@ -31,12 +31,14 @@ pub struct Right {
list_header: Vec<DVLine>,
list_labels: Vec<Vec<DVLine>>,
list: StatefulList<Vec<DVLine>>,
mode: Mode,
selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>,
title: String,
}
impl Right {
#[must_use]
pub fn new() -> Self {
Self {
selections: vec![None, None],
@ -92,8 +94,16 @@ impl Component for Right {
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::Highlight(index) => self.set_highlight(index),
Action::KeyUp => self.list.previous(),
Action::KeyDown => self.list.next(),
Action::KeyUp => {
if self.mode != Mode::LogView {
self.list.previous();
}
}
Action::KeyDown => {
if self.mode != Mode::LogView {
self.list.next();
}
}
Action::Select(one, two) => {
self.selections[0] = one;
self.selections[1] = two;
@ -108,7 +118,8 @@ impl Component for Right {
self.selections_saved[0] = one;
self.selections_saved[1] = two;
}
Action::SetMode(_) => {
Action::SetMode(new_mode) => {
self.mode = new_mode;
self.selections[0] = None;
self.selections[1] = None;
self.selections_saved[0] = None;
@ -148,9 +159,10 @@ impl Component for Right {
}
// First selection
if let Some(first_index) = self.get_first() {
if let Some(first_desc) = self.list.get(first_index) {
if let Some(label) = self.list_labels.get(0) {
if let Some(first_index) = self.get_first()
&& let Some(first_desc) = self.list.get(first_index)
{
if let Some(label) = self.list_labels.first() {
label
.iter()
.for_each(|dv| body_text.push(dv.as_line().bold()));
@ -160,11 +172,11 @@ impl Component for Right {
.iter()
.for_each(|dv| body_text.push(dv.as_line()));
}
}
// Second selection
if let Some(second_index) = self.get_second() {
if let Some(second_desc) = self.list.get(second_index) {
if let Some(second_index) = self.get_second()
&& let Some(second_desc) = self.list.get(second_index)
{
// Divider
body_text.push(Line::from(""));
body_text.push(Line::from(str::repeat("", (body_area.width - 4) as usize)));
@ -179,7 +191,6 @@ impl Component for Right {
.iter()
.for_each(|dv| body_text.push(dv.as_line()));
}
}
// Build Paragraph
let body = Paragraph::new(body_text)

View file

@ -31,14 +31,17 @@ impl<T: Clone> StatefulList<T> {
self.items.clear();
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&T> {
self.items.get(index)
}
#[must_use]
pub fn get_selected(&self) -> Option<T> {
self.state.selected().map(|i| self.items[i].clone())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
@ -51,6 +54,7 @@ impl<T: Clone> StatefulList<T> {
}
}
#[must_use]
pub fn selected(&self) -> Option<usize> {
self.state.selected()
}

View file

@ -15,57 +15,126 @@
//
use color_eyre::Result;
use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
prelude::*,
style::{Style, Stylize},
text::Span,
widgets::{Block, Borders, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use std::time::Instant;
use super::Component;
use crate::{action::Action, config::Config};
use crate::action::Action;
#[derive(Default)]
#[derive(Debug, Clone, PartialEq)]
pub struct Title {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
text: String,
mode: String,
title: String,
last_tick_update: Instant,
tick_count: u32,
ticks_per_second: f64,
last_frame_update: Instant,
frame_count: u32,
frames_per_second: f64,
}
impl Title {
pub fn new(text: &str) -> Self {
#[must_use]
pub fn new(title: &str) -> Self {
Self {
text: String::from(text),
..Default::default()
mode: String::new(),
title: String::from(title),
last_tick_update: Instant::now(),
tick_count: 0,
ticks_per_second: 0.0,
last_frame_update: Instant::now(),
frame_count: 0,
frames_per_second: 0.0,
}
}
#[allow(clippy::unnecessary_wraps)]
fn app_tick(&mut self) -> Result<()> {
self.tick_count += 1;
let now = Instant::now();
let elapsed = (now - self.last_tick_update).as_secs_f64();
if elapsed >= 1.0 {
self.ticks_per_second = f64::from(self.tick_count) / elapsed;
self.last_tick_update = now;
self.tick_count = 0;
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn render_tick(&mut self) -> Result<()> {
self.frame_count += 1;
let now = Instant::now();
let elapsed = (now - self.last_frame_update).as_secs_f64();
if elapsed >= 1.0 {
self.frames_per_second = f64::from(self.frame_count) / elapsed;
self.last_frame_update = now;
self.frame_count = 0;
}
Ok(())
}
}
impl Component for Title {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
#[allow(clippy::match_single_binding)]
match action {
Action::SetMode(mode) => {
self.mode = format!("{mode:?}");
}
Action::Render => self.render_tick()?,
Action::Tick => self.app_tick()?,
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, row, _] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.areas(area);
let [_, left, right, _] = Layout::horizontal([
Constraint::Length(2),
Constraint::Min(1),
Constraint::Min(1),
Constraint::Length(2),
])
.areas(row);
// Title Block
let title_text = Span::styled(
format!("{}: {}", self.config.app_title.as_str(), self.text),
format!("Deja-Vu: {}", self.title),
Style::default().fg(Color::LightCyan),
);
let title = Paragraph::new(Line::from(title_text).centered())
.block(Block::default().borders(Borders::ALL));
frame.render_widget(title, area);
// Mode
let span = Span::styled(format!("Mode: {}", &self.mode), Style::new().dim());
let paragraph = Paragraph::new(span).left_aligned();
frame.render_widget(paragraph, left);
// FPS
let message = format!(
"{:.2} ticks/sec, {:.2} FPS",
self.ticks_per_second, self.frames_per_second
);
let span = Span::styled(message, Style::new().dim());
let paragraph = Paragraph::new(span).right_aligned();
frame.render_widget(paragraph, right);
Ok(())
}
}

View file

@ -12,7 +12,9 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::ref_option)]
#![allow(dead_code)] // Remove this once you start using the code
use std::{collections::HashMap, env, path::PathBuf};
@ -23,7 +25,7 @@ use derive_deref::{Deref, DerefMut};
use directories::ProjectDirs;
use lazy_static::lazy_static;
use ratatui::style::{Color, Modifier, Style};
use serde::{de::Deserializer, Deserialize};
use serde::{Deserialize, de::Deserializer};
use tracing::error;
use crate::{action::Action, state::Mode};
@ -52,17 +54,25 @@ pub struct Config {
pub keybindings: KeyBindings,
#[serde(default)]
pub styles: Styles,
#[serde(default)]
pub network_server: String,
#[serde(default)]
pub network_share: String,
#[serde(default)]
pub network_user: String,
#[serde(default)]
pub network_pass: String,
}
pub static PROJECT_NAME: &'static str = "DEJA-VU";
pub static PROJECT_NAME: &str = "DEJA-VU";
lazy_static! {
//pub static ref PROJECT_NAME: String = env!("CARGO_PKG_NAME").to_uppercase().to_string();
pub static ref DATA_FOLDER: Option<PathBuf> =
env::var(format!("{}_DATA", PROJECT_NAME))
env::var(format!("{PROJECT_NAME}_DATA"))
.ok()
.map(PathBuf::from);
pub static ref CONFIG_FOLDER: Option<PathBuf> =
env::var(format!("{}_CONFIG", PROJECT_NAME))
env::var(format!("{PROJECT_NAME}_CONFIG"))
.ok()
.map(PathBuf::from);
}
@ -74,16 +84,14 @@ impl Config {
let config_dir = get_config_dir();
let mut builder = config::Config::builder()
.set_default("app_title", default_config.app_title.as_str())?
.set_default(
"clone_app_path",
String::from("C:\\Program Files\\Some Clone Tool\\app.exe"),
)?
.set_default(
"conemu_path",
String::from("C:\\Program Files\\ConEmu\\ConEmu64.exe"),
)?
.set_default("clone_app_path", default_config.app_title.as_str())?
.set_default("conemu_path", default_config.app_title.as_str())?
.set_default("config_dir", config_dir.to_str().unwrap())?
.set_default("data_dir", data_dir.to_str().unwrap())?;
.set_default("data_dir", data_dir.to_str().unwrap())?
.set_default("network_server", default_config.app_title.as_str())?
.set_default("network_share", default_config.app_title.as_str())?
.set_default("network_user", default_config.app_title.as_str())?
.set_default("network_pass", default_config.app_title.as_str())?;
let config_files = [
("config.json5", config::FileFormat::Json5),
@ -126,26 +134,26 @@ impl Config {
}
}
#[must_use]
pub fn get_data_dir() -> PathBuf {
let directory = if let Some(s) = DATA_FOLDER.clone() {
if let Some(s) = DATA_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
proj_dirs.data_local_dir().to_path_buf()
} else {
PathBuf::from(".").join(".data")
};
directory
}
}
#[must_use]
pub fn get_config_dir() -> PathBuf {
let directory = if let Some(s) = CONFIG_FOLDER.clone() {
if let Some(s) = CONFIG_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
proj_dirs.config_local_dir().to_path_buf()
} else {
PathBuf::from(".").join(".config")
};
directory
}
}
fn project_directory() -> Option<ProjectDirs> {
@ -258,6 +266,7 @@ fn parse_key_code_with_modifiers(
Ok(KeyEvent::new(c, modifiers))
}
#[must_use]
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
let char;
let key_code = match key_event.code {
@ -328,8 +337,8 @@ pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
let raw = if raw.contains("><") {
raw
} else {
let raw = raw.strip_prefix('<').unwrap_or(raw);
let raw = raw.strip_prefix('>').unwrap_or(raw);
let mut raw = raw.strip_prefix('<').unwrap_or(raw);
raw = raw.strip_prefix('>').unwrap_or(raw);
raw
};
let sequences = raw
@ -373,6 +382,7 @@ impl<'de> Deserialize<'de> for Styles {
}
}
#[must_use]
pub fn parse_style(line: &str) -> Style {
let (foreground, background) =
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
@ -435,8 +445,11 @@ fn parse_color(s: &str) -> Option<Color> {
.unwrap_or_default();
Some(Color::Indexed(c))
} else if s.contains("rgb") {
#[allow(clippy::cast_possible_truncation)]
let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
#[allow(clippy::cast_possible_truncation)]
let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
#[allow(clippy::cast_possible_truncation)]
let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
let c = 16 + red * 36 + green * 6 + blue;
Some(Color::Indexed(c))

View file

@ -12,7 +12,8 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
use std::env;
use color_eyre::Result;
@ -30,11 +31,11 @@ pub fn init() -> Result<()> {
.into_hooks();
eyre_hook.install()?;
std::panic::set_hook(Box::new(move |panic_info| {
if let Ok(mut t) = crate::tui::Tui::new() {
if let Err(r) = t.exit() {
if let Ok(mut t) = crate::tui::Tui::new()
&& let Err(r) = t.exit()
{
error!("Unable to exit Terminal: {:?}", r);
}
}
#[cfg(not(debug_assertions))]
{

View file

@ -23,4 +23,5 @@ pub mod logging;
pub mod state;
pub mod system;
pub mod tasks;
pub mod tests;
pub mod tui;

View file

@ -30,13 +30,15 @@ pub struct DVLine {
impl DVLine {
/// Convert to Line with colored span(s)
pub fn as_line(&self) -> Line {
#[must_use]
pub fn as_line(&self) -> Line<'_> {
let mut spans = Vec::new();
zip(self.line_parts.clone(), self.line_colors.clone())
.for_each(|(part, color)| spans.push(Span::styled(part, Style::default().fg(color))));
Line::from(spans)
}
#[must_use]
pub fn blank() -> Self {
Self {
line_parts: vec![String::new()],
@ -45,9 +47,10 @@ impl DVLine {
}
}
#[must_use]
pub fn get_disk_description_right(
disk: &Disk,
boot_os_indicies: Option<Vec<usize>>,
boot_os_indicies: &Option<Vec<usize>>,
) -> Vec<DVLine> {
let mut description: Vec<DVLine> = vec![
DVLine {
@ -76,8 +79,8 @@ pub fn get_disk_description_right(
.for_each(|(index, line)| {
let mut line_parts = vec![line.clone()];
let mut line_colors = vec![Color::Reset];
if let Some(indicies) = &boot_os_indicies {
let boot_index = indicies.get(0);
if let Some(indicies) = boot_os_indicies {
let boot_index = indicies.first();
if boot_index.is_some_and(|i| i == &index) {
line_parts.push(String::from(" <-- Boot Partition"));
line_colors.push(Color::Cyan);
@ -93,9 +96,16 @@ pub fn get_disk_description_right(
line_colors,
});
});
if disk.parts_description.is_empty() {
description.push(DVLine {
line_parts: vec![String::from("-None-")],
line_colors: vec![Color::Reset],
});
}
description
}
#[must_use]
pub fn get_part_description(part: &Partition) -> Vec<DVLine> {
let description: Vec<DVLine> = vec![
DVLine {

View file

@ -12,10 +12,11 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
use color_eyre::Result;
use tracing_error::ErrorLayer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use crate::config;

View file

@ -14,15 +14,8 @@
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use crate::system::{
disk::{Disk, PartitionTableType},
drivers,
};
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mode {
// Core
@ -35,6 +28,8 @@ pub enum Mode {
BootDiags,
BootScan,
BootSetup,
LogView,
Process,
InjectDrivers,
SetBootMode,
// Clone
@ -47,31 +42,11 @@ pub enum Mode {
Clone,
SelectParts,
PostClone,
// Windows Installer
ScanWinSources,
SelectWinSource,
SelectWinImage,
SetUserName,
// WinPE
PEMenu,
}
#[derive(Debug, Default)]
pub struct CloneSettings {
pub disk_index_dest: Option<usize>,
pub disk_index_source: Option<usize>,
pub disk_list: Arc<Mutex<Vec<Disk>>>,
pub driver_list: Vec<drivers::Driver>,
pub part_index_boot: Option<usize>,
pub part_index_os: Option<usize>,
pub driver: Option<drivers::Driver>,
pub table_type: Option<PartitionTableType>,
}
impl CloneSettings {
pub fn new(disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
CloneSettings {
disk_list,
..Default::default()
}
}
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
}

View file

@ -12,24 +12,28 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
use super::{disk::PartitionTableType, drivers::Driver};
use crate::tasks::TaskType;
use color_eyre::Result;
use std::path::PathBuf;
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub enum SafeMode {
#[default]
Disable,
Enable,
}
#[must_use]
pub fn configure_disk(
letter_boot: &str,
letter_os: &str,
safe_mode: SafeMode,
system32: &str,
table_type: PartitionTableType,
table_type: &PartitionTableType,
) -> Vec<TaskType> {
let mut tasks = Vec::new();
@ -49,7 +53,7 @@ pub fn configure_disk(
));
// Update boot sector (for legacy setups)
if table_type == PartitionTableType::Legacy {
if *table_type == PartitionTableType::Legacy {
tasks.push(TaskType::CommandWait(
PathBuf::from(format!("{system32}/bootsect.exe")),
vec![
@ -61,11 +65,13 @@ pub fn configure_disk(
));
}
// Lock in safe mode
// Lock in safe mode (if needed)
if safe_mode == SafeMode::Enable {
tasks.push(
set_mode(letter_boot, SafeMode::Enable, system32, &table_type)
set_mode(letter_boot, &SafeMode::Enable, system32, table_type)
.expect("Failed to create set_mode task."),
);
}
// Done
tasks
@ -79,7 +85,7 @@ pub fn inject_driver(driver: &Driver, letter_os: &str, system32: &str) -> Result
vec![
format!("/image:{letter_os}:\\"),
String::from("/add-driver"),
format!("/driver:\"{}\"", driver_path,),
format!("/driver:{driver_path}"),
String::from("/recurse"),
],
))
@ -87,7 +93,7 @@ pub fn inject_driver(driver: &Driver, letter_os: &str, system32: &str) -> Result
pub fn set_mode(
letter_boot: &str,
mode: SafeMode,
mode: &SafeMode,
system32: &str,
table_type: &PartitionTableType,
) -> Result<TaskType> {

View file

@ -17,15 +17,15 @@ use serde::{Deserialize, Serialize};
use std::{
fmt,
process::{Command, Stdio},
sync::OnceLock,
thread::sleep,
time::Duration,
};
use tracing::info;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::system::diskpart;
use crate::system::diskpart::{self, REGEXES};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Disk {
@ -36,7 +36,6 @@ pub struct Disk {
pub part_type: PartitionTableType,
pub parts: Vec<Partition>,
pub parts_description: Vec<String>,
pub sector_size: usize,
pub serial: String,
pub size: u64, // In bytes
}
@ -67,6 +66,7 @@ impl Disk {
}
}
#[must_use]
pub fn get_part_letter(&self, part_index: usize) -> String {
// Used to get Boot and OS letters
if let Some(part) = self.parts.get(part_index) {
@ -76,13 +76,53 @@ impl Disk {
}
}
#[must_use]
pub fn get_parts(&self) -> Vec<Partition> {
self.parts.clone()
}
#[must_use]
pub fn num_parts(&self) -> usize {
self.parts.len()
}
pub fn refresh_disk_info(&mut self, details_str: Option<&str>) {
let re_details = REGEXES.detail_disk();
let re_uuid = REGEXES.uuid();
if cfg!(windows) {
info!("Refresh disk info via Diskpart");
// Get details
let details: String;
if let Some(s) = details_str {
details = String::from(s);
} else {
let script = format!("select disk {}\r\ndetail disk", self.id);
details = diskpart::run_script(&script);
};
// Parse details
for (_, [model, part_type, conn_type]) in
re_details.captures_iter(&details).map(|c| c.extract())
{
self.model = String::from(model);
self.conn_type = String::from(conn_type);
if re_uuid.is_match(part_type) {
self.part_type = PartitionTableType::Guid;
} else {
self.part_type = PartitionTableType::Legacy;
}
self.serial = get_disk_serial_number(self.id);
}
// Partition details
self.parts = diskpart::get_partitions(self.id, None, None);
self.generate_descriptions();
} else {
info!("Refresh fake disk info");
self.parts = refresh_fake_disk_info();
self.generate_descriptions();
}
}
}
impl fmt::Display for Disk {
@ -103,6 +143,24 @@ impl fmt::Display for Disk {
}
}
impl Partition {
pub fn set_details(&mut self, part_type: &str, vol_line: &str) {
let re_list_volume = REGEXES.list_volumes();
// Partition info
self.part_type = String::from(part_type.trim());
// Volume info
for (_, [_id, letter, label, fs_type]) in
re_list_volume.captures_iter(vol_line).map(|c| c.extract())
{
self.label = String::from(label.trim());
self.letter = String::from(letter.trim());
self.fs_type = String::from(fs_type.trim());
}
}
}
impl fmt::Display for Partition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s: String;
@ -160,9 +218,9 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1,
fs_type: String::from("NTFS"),
label: String::from("System Reserved"),
letter: String::from("C"),
part_type: String::from("7"),
size: 104_857_600,
..Default::default()
},
Partition {
id: 2,
@ -174,17 +232,17 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 5,
fs_type: String::from("NTFS"),
label: String::from("Win7"),
letter: String::from("D"),
part_type: String::from("7"),
size: 267_701_452_800,
..Default::default()
},
Partition {
id: 6,
fs_type: String::from("NTFS"),
label: String::from("Tools"),
letter: String::from("E"),
part_type: String::from("7"),
size: 524_288_000,
..Default::default()
},
],
serial: "MDZ1243".to_string(),
@ -200,9 +258,9 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1,
fs_type: String::from("NTFS"),
label: String::from("Scratch"),
letter: String::from("G"),
part_type: String::from("7"),
size: 249_998_951_424,
..Default::default()
}],
serial: "000010000".to_string(),
size: 250_000_000_000,
@ -218,7 +276,7 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1,
fs_type: String::from("FAT32"),
label: String::from("ESP"),
letter: String::from("Q"),
letter: String::from("J"),
part_type: String::from("EFI"),
size: 272_629_760,
},
@ -232,7 +290,7 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 4,
fs_type: String::from("NTFS"),
label: String::from("Win10"),
letter: String::from("V"),
letter: String::from("K"),
part_type: String::from("MS Basic Data"),
size: 824_340_119_552,
},
@ -251,9 +309,9 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1,
fs_type: String::from("FAT32"),
label: String::from("EFI Boot"),
letter: String::from("I"),
part_type: String::from("EFI"),
size: 209_715_200,
..Default::default()
},
Partition {
id: 2,
@ -312,20 +370,6 @@ pub fn get_disk_serial_number(id: usize) -> String {
serial
}
pub fn refresh_disk_info(disk: &mut Disk) -> Disk {
let mut new_disk: Disk;
if cfg!(windows) {
info!("Refresh disk via Diskpart");
new_disk = diskpart::refresh_disk_info(disk);
} else {
info!("Refresh fake disk");
new_disk = disk.clone();
new_disk.parts = refresh_fake_disk_info();
new_disk.generate_descriptions();
}
new_disk
}
fn refresh_fake_disk_info() -> Vec<Partition> {
vec![
Partition {
@ -382,10 +426,11 @@ pub fn bytes_to_string(size: u64) -> String {
///
/// Will panic if s is not simliar to 32B, 64MB, etc...
pub fn string_to_bytes(s: &str) -> u64 {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s+(\w+)B").unwrap());
static RE_BYTES: OnceLock<Regex> = OnceLock::new();
let re_bytes = RE_BYTES.get_or_init(|| Regex::new(r"(\d+)\s+(\w+)B").unwrap());
let base: u64 = 1024;
let mut size: u64 = 0;
for (_, [size_str, suffix]) in RE.captures_iter(s).map(|c| c.extract()) {
for (_, [size_str, suffix]) in re_bytes.captures_iter(s).map(|c| c.extract()) {
let x: u64 = size_str.parse().unwrap();
size += x;
match suffix {

View file

@ -12,32 +12,114 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_panics_doc)]
use std::{
collections::HashMap,
fs::File,
io::Write,
process::{Command, Output, Stdio},
sync::OnceLock,
};
use once_cell::sync::Lazy;
use regex::Regex;
use tempfile::tempdir;
use tracing::{info, warn};
use crate::system::disk::{
get_disk_serial_number, string_to_bytes, Disk, Partition, PartitionTableType,
Disk, Partition, PartitionTableType, get_disk_serial_number, string_to_bytes,
};
static DEFAULT_MAX_DISKS: usize = 8;
pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&str>) -> Disk {
static RE_DETAILS: Lazy<Regex> = Lazy::new(|| {
#[derive(Debug, PartialEq)]
pub enum FormatUseCase {
ApplyWimImage,
Clone,
}
pub struct RegexList {
detail_all_disks: OnceLock<Regex>,
detail_disk: OnceLock<Regex>,
detail_partition: OnceLock<Regex>,
disk_numbers: OnceLock<Regex>,
list_disk: OnceLock<Regex>,
list_partition: OnceLock<Regex>,
list_volumes: OnceLock<Regex>,
split_all_disks: OnceLock<Regex>,
uuid: OnceLock<Regex>,
}
impl RegexList {
pub fn detail_all_disks(&self) -> &Regex {
self.detail_all_disks.get_or_init(|| {
Regex::new(r"(?s)Disk (\d+) is now the selected disk.*?\r?\n\s*\r?\n(.*)").unwrap()
})
}
pub fn detail_disk(&self) -> &Regex {
self.detail_disk.get_or_init(|| {
Regex::new(r"(.*?)\r?\nDisk ID\s*:\s+(.*?)\r?\nType\s*:\s+(.*?)\r?\n").unwrap()
});
static RE_UUID: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}$").unwrap()
});
})
}
pub fn detail_partition(&self) -> &Regex {
self.detail_partition
.get_or_init(|| Regex::new(r"Partition (\d+)\r?\nType\s*: (\S+)(\r?\n.*){5}\s*(Volume.*\r?\n.*\r?\n|There is no volume)(.*)").unwrap())
}
pub fn disk_numbers(&self) -> &Regex {
self.disk_numbers
.get_or_init(|| Regex::new(r"\s+Disk\s+(\d+).*\n.*\n.*\nDisk ID:").unwrap())
}
pub fn list_disk(&self) -> &Regex {
self.list_disk
.get_or_init(|| Regex::new(r"Disk\s+(\d+)\s+(\w+)\s+(\d+\s+\w+B)").unwrap())
}
pub fn list_partition(&self) -> &Regex {
self.list_partition
.get_or_init(|| Regex::new(r"Partition\s+(\d+)\s+\w+\s+(\d+\s+\w+B)").unwrap())
}
pub fn split_all_disks(&self) -> &Regex {
self.split_all_disks
.get_or_init(|| Regex::new(r"Disk \d+ is now the selected disk").unwrap())
}
pub fn list_volumes(&self) -> &Regex {
self.list_volumes.get_or_init(|| {
// Volume ### Ltr Label Fs Type Size Status Info
// ---------- --- ----------- ----- ---------- ------- --------- --------
// * Volume 1 S ESP FAT32 Partition 100 MB Healthy Hidden
Regex::new(r"..Volume (\d.{2}) (.{3}) (.{11}) (.{5})").unwrap()
})
}
pub fn uuid(&self) -> &Regex {
self.uuid.get_or_init(|| {
Regex::new(r"^\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}$")
.unwrap()
})
}
}
pub static REGEXES: RegexList = RegexList {
detail_all_disks: OnceLock::new(),
detail_disk: OnceLock::new(),
detail_partition: OnceLock::new(),
disk_numbers: OnceLock::new(),
list_disk: OnceLock::new(),
list_partition: OnceLock::new(),
list_volumes: OnceLock::new(),
split_all_disks: OnceLock::new(),
uuid: OnceLock::new(),
};
pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&str>) -> Disk {
let detail_disk = REGEXES.detail_disk();
let re_uuid = REGEXES.uuid();
let mut disk = Disk {
id: disk_id,
size: disk_size,
@ -55,11 +137,11 @@ pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&st
// Parse details
for (_, [model, part_type, conn_type]) in
RE_DETAILS.captures_iter(&details).map(|c| c.extract())
detail_disk.captures_iter(&details).map(|c| c.extract())
{
disk.model = String::from(model);
disk.conn_type = String::from(conn_type);
if RE_UUID.is_match(part_type) {
if re_uuid.is_match(part_type) {
disk.part_type = PartitionTableType::Guid;
} else {
disk.part_type = PartitionTableType::Legacy;
@ -71,13 +153,12 @@ pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&st
disk
}
pub fn get_partition_details(
pub fn get_partitions(
disk_id: usize,
disk_details: Option<&str>,
part_details: Option<&str>,
) -> Vec<Partition> {
static RE_LIS: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Partition\s+(\d+)\s+\w+\s+(\d+\s+\w+B)").unwrap());
let list_partitions = REGEXES.list_partition();
let mut parts = Vec::new();
// List partition
@ -88,7 +169,10 @@ pub fn get_partition_details(
let script = format!("select disk {disk_id}\r\nlist partition");
contents = run_script(&script);
};
for (_, [number, size]) in RE_LIS.captures_iter(&contents).map(|c| c.extract()) {
for (_, [number, size]) in list_partitions
.captures_iter(&contents)
.map(|c| c.extract())
{
let part_num = number.parse().unwrap();
if part_num != 0 {
// part_num == 0 is reserved for extended partition "containers" so we can exclude them
@ -119,7 +203,7 @@ pub fn get_partition_details(
part_contents = String::from(details);
} else {
part_contents = run_script(script.join("\r\n").as_str());
};
}
parse_partition_details(&mut parts, &part_contents);
// Done
@ -127,7 +211,11 @@ pub fn get_partition_details(
}
#[must_use]
pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType) -> String {
pub fn build_dest_format_script(
disk_id: usize,
part_type: &PartitionTableType,
format_use_case: FormatUseCase,
) -> String {
let disk_id = format!("{disk_id}");
let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"];
match part_type {
@ -143,18 +231,21 @@ pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType)
script.push("format fs=ntfs quick label=System");
}
}
if format_use_case == FormatUseCase::ApplyWimImage {
script.push("create partition primary");
script.push("format fs=ntfs quick label=Windows");
}
script.join("\r\n").replace("{disk_id}", &disk_id)
}
#[must_use]
pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
let capacity = DEFAULT_MAX_DISKS * 3 + 1;
let script: String;
// Get disk and partition details
if let Some(disks) = disk_nums {
// (Slower case)
let mut script_parts = Vec::with_capacity(capacity);
let mut script_parts = Vec::with_capacity(DEFAULT_MAX_DISKS * 3 + 1);
// Get list of disks
script_parts.push(String::from("list disk"));
@ -170,7 +261,7 @@ pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
script = script_parts.join("\n");
} else {
// (Best case)
let mut script_parts = Vec::with_capacity(capacity);
let mut script_parts = Vec::with_capacity(DEFAULT_MAX_DISKS + 1);
// Get list of disks
script_parts.push("list disk");
@ -180,9 +271,9 @@ pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
script_parts.push("detail disk");
script_parts.push("list partition");
// Limit to 8 disks (if there's more the manual "worst" case will be used)
// Limit to DEFAULT_MAX_DISKS (if there's more the manual "worst" case will be used)
let mut i = 0;
while i < 8 {
while i < DEFAULT_MAX_DISKS {
script_parts.push("select disk next");
script_parts.push("detail disk");
script_parts.push("list partition");
@ -197,11 +288,8 @@ pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
}
pub fn get_disks() -> Vec<Disk> {
static RE_DIS_DET: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?s)Disk (\d+) is now the selected disk.*?\r?\n\s*\r?\n(.*)").unwrap()
});
static RE_DIS_LIS: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Disk\s+(\d+)\s+(\w+)\s+(\d+\s+\w+B)").unwrap());
let detail_all_disks = REGEXES.detail_all_disks();
let list_disk = REGEXES.list_disk();
let mut contents: String;
let mut output;
let mut script: String;
@ -212,7 +300,7 @@ pub fn get_disks() -> Vec<Disk> {
contents = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(return_code) = output.status.code() {
let disk_nums = parse_disk_numbers(&contents);
if return_code != 0 && !disk_nums.is_empty() {
if return_code != 0 && !disk_nums.is_empty() && disk_nums.len() != DEFAULT_MAX_DISKS {
// The base assumptions were correct! skipping fallback method
//
// Since the return_code was not zero, and at least one disk was detected, that
@ -243,7 +331,7 @@ pub fn get_disks() -> Vec<Disk> {
// i.e. 0, 1, 3, 4
// For instance, this can happen if a drive is disconnected after startup
let mut disks_map: HashMap<&str, Disk> = HashMap::with_capacity(DEFAULT_MAX_DISKS);
for (_, [number, _status, size]) in RE_DIS_LIS
for (_, [number, _status, size]) in list_disk
.captures_iter(dp_sections.remove(0)) // This is the "list disk" section
.map(|c| c.extract())
{
@ -260,11 +348,11 @@ pub fn get_disks() -> Vec<Disk> {
// Add Disk details
let mut disks_raw: Vec<Disk> = Vec::with_capacity(DEFAULT_MAX_DISKS);
for section in dp_sections {
for (_, [id, details]) in RE_DIS_DET.captures_iter(section).map(|c| c.extract()) {
for (_, [id, details]) in detail_all_disks.captures_iter(section).map(|c| c.extract()) {
if let Some(disk) = disks_map.remove(id) {
// We remove the disk from the HashMap because we're moving it to the Vec
let mut disk = get_disk_details(disk.id, disk.size, Some(details));
disk.parts = get_partition_details(disk.id, Some(details), None);
disk.parts = get_partitions(disk.id, Some(details), None);
disk.generate_descriptions();
disks_raw.push(disk);
}
@ -280,58 +368,27 @@ pub fn parse_disk_numbers(contents: &str) -> Vec<&str> {
//
//Red Hat VirtIO SCSI Disk Device
//Disk ID: {E9CE8DFA-46B2-43C1-99BB-850C661CEE6B}
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\s+Disk\s+(\d+).*\n.*\n.*\nDisk ID:").unwrap());
let disk_numbers = REGEXES.disk_numbers();
let mut disk_nums = Vec::new();
for (_, [number]) in RE.captures_iter(contents).map(|c| c.extract()) {
for (_, [number]) in disk_numbers.captures_iter(contents).map(|c| c.extract()) {
disk_nums.push(number);
}
disk_nums
}
pub fn parse_partition_details(parts: &mut [Partition], contents: &str) {
static RE_PAR: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"Partition (\d+)\r?\nType\s*: (\S+)(\r?\n.*){5}\s*(Volume.*\r?\n.*\r?\n|There is no volume)(.*)",
)
.unwrap()
});
static RE_VOL: Lazy<Regex> = Lazy::new(|| {
// Volume ### Ltr Label Fs Type Size Status Info
// ---------- --- ----------- ----- ---------- ------- --------- --------
// * Volume 1 S ESP FAT32 Partition 100 MB Healthy Hidden
Regex::new(r"..Volume (\d.{2}) (.{3}) (.{11}) (.{5})").unwrap()
});
let detail_partition = REGEXES.detail_partition();
for (part_index, (_, [_part_id, part_type, _, _vol_header, vol_line])) in RE_PAR
for (part_index, (_, [_part_id, part_type, _, _vol_header, vol_line])) in detail_partition
.captures_iter(contents)
.map(|c| c.extract())
.enumerate()
{
if let Some(part) = parts.get_mut(part_index) {
// Partition info
part.part_type = String::from(part_type.trim());
// Volume info
for (_, [_id, letter, label, fs_type]) in
RE_VOL.captures_iter(vol_line).map(|c| c.extract())
{
part.label = String::from(label.trim());
part.letter = String::from(letter.trim());
part.fs_type = String::from(fs_type.trim());
part.set_details(part_type, vol_line);
}
}
}
}
pub fn refresh_disk_info(disk: &Disk) -> Disk {
info!("Refresh disk info");
let mut disk = get_disk_details(disk.id, disk.size, None);
disk.parts = get_partition_details(disk.id, None, None);
disk.generate_descriptions();
disk
}
/// # Panics
///
@ -344,12 +401,11 @@ pub fn run_script_raw(script: &str) -> Output {
script_file
.write_all(script.as_bytes())
.expect("Failed to write script to disk");
let output = Command::new("diskpart")
Command::new("diskpart")
.args(["/s", format!("{}", script_path.display()).as_str()])
.stdout(Stdio::piped())
.output()
.expect("Failed to execute Diskpart script");
output
.expect("Failed to execute Diskpart script")
}
#[must_use]
@ -360,12 +416,11 @@ pub fn run_script(script: &str) -> String {
pub fn split_diskpart_disk_output(contents: &str) -> Vec<&str> {
// NOTE: A simple split isn't helpful since we want to include the matching lines
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Disk \d+ is now the selected disk").unwrap());
let split_all_disks = REGEXES.split_all_disks();
let mut sections = Vec::new();
let mut starts: Vec<usize> = vec![0];
let mut ends: Vec<usize> = Vec::new();
let _: Vec<_> = RE
let _: Vec<_> = split_all_disks
.find_iter(contents)
.map(|m| {
ends.push(m.start() - 1);

View file

@ -73,8 +73,9 @@ pub fn scan() -> Vec<Driver> {
let driver_path = exe_path.with_file_name("drivers");
if let Ok(dir_entry) = read_dir(driver_path) {
for entry in dir_entry.flatten() {
if entry.path().is_dir() {
if let Ok(name) = entry.file_name().into_string() {
if entry.path().is_dir()
&& let Ok(name) = entry.file_name().into_string()
{
drivers.push(Driver {
name,
path: entry.path(),
@ -84,7 +85,6 @@ pub fn scan() -> Vec<Driver> {
}
}
}
}
drivers.sort();
drivers.reverse();
for driver in &mut drivers {
@ -95,12 +95,12 @@ pub fn scan() -> Vec<Driver> {
.into_iter()
.filter_map(Result::ok)
{
if let Some(ext) = entry.path().extension() {
if ext == "inf" {
if let Some(ext) = entry.path().extension()
&& ext == "inf"
{
driver.inf_paths.push(entry.into_path());
}
}
}
}
drivers
}

View file

@ -12,7 +12,8 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
use std::{
collections::VecDeque,
@ -20,11 +21,12 @@ use std::{
path::PathBuf,
process::{Command, Stdio},
sync::{Arc, Mutex},
thread::{self, sleep, JoinHandle},
thread::{self, JoinHandle, sleep},
time::Duration,
};
use color_eyre::Result;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tracing::info;
@ -40,15 +42,18 @@ pub enum TaskResult {
Output(String, String, bool), // stdout, stderr, success
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum TaskType {
CommandNoWait(PathBuf, Vec<String>), // (command, args)
CommandWait(PathBuf, Vec<String>), // (command, args)
Diskpart(String), // (script_as_string)
ScanDisks,
Sleep,
TestPaths(Vec<PathBuf>),
UpdateDestDisk(usize), // (disk_index)
UpdateDiskList,
GroupStart { label: String },
GroupEnd { label: String },
}
impl fmt::Display for TaskType {
@ -66,8 +71,11 @@ impl fmt::Display for TaskType {
}
TaskType::ScanDisks => write!(f, "ScanDisks"),
TaskType::Sleep => write!(f, "Sleep"),
TaskType::TestPaths(_) => write!(f, "TestPaths"),
TaskType::UpdateDestDisk(_) => write!(f, "UpdateDestDisk"),
TaskType::UpdateDiskList => write!(f, "UpdateDiskList"),
TaskType::GroupStart { label } => write!(f, "GroupStart({})", &label),
TaskType::GroupEnd { label } => write!(f, "GroupEnd({})", &label),
}
}
}
@ -80,6 +88,7 @@ pub struct Task {
}
impl Task {
#[must_use]
pub fn new(task_type: TaskType) -> Task {
Task {
handle: None,
@ -122,6 +131,20 @@ impl Tasks {
self.task_list.push_back(Task::new(task_type));
}
pub fn add_group(&mut self, group_label: &str, group_tasks: Vec<TaskType>) {
info!("Adding task group: {group_label}");
self.task_list.push_back(Task::new(TaskType::GroupStart {
label: group_label.to_string(),
}));
for task in group_tasks {
self.task_list.push_back(Task::new(task));
}
self.task_list.push_back(Task::new(TaskType::GroupEnd {
label: group_label.to_string(),
}));
}
#[must_use]
pub fn idle(&self) -> bool {
self.cur_handle.is_none()
}
@ -129,12 +152,12 @@ impl Tasks {
pub fn poll(&mut self) -> Result<Option<Task>> {
let mut return_task: Option<Task> = None;
// Handle task channel item(s)
if let Ok(result) = self.task_rx.try_recv() {
if let Some(mut task) = self.cur_task.take() {
if let Ok(result) = self.task_rx.try_recv()
&& let Some(mut task) = self.cur_task.take()
{
task.result.replace(result);
self.cur_task.replace(task);
}
}
// Check status of current task (if one is running).
// NOTE: Action::TasksComplete is sent once all tasks are complete
@ -180,7 +203,7 @@ impl Tasks {
));
}
TaskType::Diskpart(ref script) => {
self.cur_handle = Some(run_task_diskpart(&script, task_tx));
self.cur_handle = Some(run_task_diskpart(script, task_tx));
}
TaskType::ScanDisks => {
let disk_list_arc = self.disk_list.clone();
@ -190,12 +213,15 @@ impl Tasks {
self.cur_handle = Some(thread::spawn(move || {
let mut disks = disk_list_arc.lock().unwrap();
*disks = disk::get_disks()
*disks = disk::get_disks();
}));
}
TaskType::Sleep => {
self.cur_handle = Some(thread::spawn(|| sleep(Duration::from_millis(250))));
}
TaskType::TestPaths(ref list) => {
self.cur_handle = Some(test_paths(list.clone(), task_tx.clone()));
}
TaskType::UpdateDestDisk(index) => {
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
@ -210,8 +236,7 @@ impl Tasks {
let disk_list_arc = self.disk_list.clone();
self.cur_handle = Some(thread::spawn(move || {
let mut disks = disk_list_arc.lock().unwrap();
let old_disk = &mut disks[index];
disks[index] = disk::refresh_disk_info(old_disk);
disks[index].refresh_disk_info(None);
}));
}
TaskType::UpdateDiskList => {
@ -224,6 +249,14 @@ impl Tasks {
}
}));
}
TaskType::GroupStart { ref label } => {
self.action_tx.send(Action::TaskGroupStart(label.clone()))?;
}
TaskType::GroupEnd { ref label } => {
self.action_tx.send(Action::DiagLineEnd {
text: label.clone(),
})?;
}
}
// Done
self.cur_task.replace(task);
@ -257,17 +290,19 @@ fn run_task_command(
.expect("Failed to propegate error?");
}
Ok(output) => {
let stderr = parse_bytes_as_str(output.stderr.to_owned());
let stdout = parse_bytes_as_str(output.stdout.to_owned());
let stderr = parse_bytes_as_str(output.stderr.clone());
let stdout = parse_bytes_as_str(output.stdout.clone());
let task_result = TaskResult::Output(stdout, stderr, output.status.success());
let err_str = format!("Failed to send TaskResult: {:?}", &task_result);
task_tx.send(task_result).expect(err_str.as_str());
task_tx
.send(task_result)
.unwrap_or_else(|_| panic!("{}", err_str));
}
}
})
} else {
// Simulate task if not running under Windows
thread::spawn(|| sleep(Duration::from_millis(250)))
thread::spawn(|| sleep(Duration::from_millis(500)))
}
}
@ -276,14 +311,46 @@ fn run_task_diskpart(script: &str, task_tx: mpsc::UnboundedSender<TaskResult>) -
let script = script.to_owned();
thread::spawn(move || {
let output = diskpart::run_script_raw(&script);
let stderr = parse_bytes_as_str(output.stderr.to_owned());
let stdout = parse_bytes_as_str(output.stdout.to_owned());
let stderr = parse_bytes_as_str(output.stderr.clone());
let stdout = parse_bytes_as_str(output.stdout.clone());
let task_result = TaskResult::Output(stdout, stderr, output.status.success());
let err_str = format!("Failed to send TaskResult: {:?}", &task_result);
task_tx.send(task_result).expect(err_str.as_str());
task_tx
.send(task_result)
.unwrap_or_else(|_| panic!("{}", err_str));
})
} else {
// Simulate task if not running under Windows
thread::spawn(|| sleep(Duration::from_millis(250)))
}
}
fn test_paths(
path_list: Vec<PathBuf>,
task_tx: mpsc::UnboundedSender<TaskResult>,
) -> JoinHandle<()> {
thread::spawn(move || {
let mut missing_paths = Vec::new();
for path in path_list {
if !path.exists() {
missing_paths.push(String::from(path.to_string_lossy()));
}
}
let task_result = if missing_paths.is_empty() {
// No missing paths
TaskResult::Output(String::from("OK"), String::new(), true)
} else {
TaskResult::Output(
String::from("Missing item(s)"),
missing_paths.join(",\n"),
false,
)
};
let err_str = format!("Failed to send TaskResult: {:?}", &task_result);
task_tx
.send(task_result)
.unwrap_or_else(|_| panic!("{}", err_str));
})
}

View file

@ -37,7 +37,7 @@ mod diskpart {
}
#[test]
fn get_partition_details() {
fn get_partitions() {
// Left
let partition_1 = system::disk::Partition {
id: 1,
@ -60,7 +60,6 @@ mod diskpart {
letter: String::from("C"),
part_type: String::from("ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"),
size: 50465865728,
..Default::default()
};
// Right
@ -77,7 +76,7 @@ mod diskpart {
id: 4,
..Default::default()
});
disk.parts = system::diskpart::get_partition_details(
disk.parts = system::diskpart::get_partitions(
2,
Some(sample_output::DETAIL_DISK_GPT),
Some(sample_output::SELECT_PART_DETAIL_PARTS),
@ -92,7 +91,7 @@ mod diskpart {
#[test]
fn parse_disk_numbers() {
let disk_numbers =
system::diskpart::parse_disk_numbers(&sample_output::LIST_DISK_DETAIL_DISKS);
system::diskpart::parse_disk_numbers(sample_output::LIST_DISK_DETAIL_DISKS);
assert_eq!(vec!["0", "2"], disk_numbers);
}
@ -111,7 +110,7 @@ mod diskpart {
system::diskpart::parse_partition_details(
&mut disk.parts,
&sample_output::SELECT_PART_DETAIL_ONE_PART,
sample_output::SELECT_PART_DETAIL_ONE_PART,
);
assert_eq!(partition_1, disk.parts[0]);
}

View file

@ -12,11 +12,11 @@
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
#![allow(clippy::missing_errors_doc)]
#![allow(dead_code)] // Remove this once you start using the code
use std::{
io::{stdout, Stdout},
io::{Stdout, stdout},
ops::{Deref, DerefMut},
time::Duration,
};
@ -85,21 +85,25 @@ impl Tui {
})
}
#[must_use]
pub fn tick_rate(mut self, tick_rate: f64) -> Self {
self.tick_rate = tick_rate;
self
}
#[must_use]
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
self.frame_rate = frame_rate;
self
}
#[must_use]
pub fn mouse(mut self, mouse: bool) -> Self {
self.mouse = mouse;
self
}
#[must_use]
pub fn paste(mut self, paste: bool) -> Self {
self.paste = paste;
self
@ -135,7 +139,7 @@ impl Tui {
.expect("failed to send init event");
loop {
let event = tokio::select! {
_ = cancellation_token.cancelled() => {
() = cancellation_token.cancelled() => {
break;
}
_ = tick_interval.tick() => Event::Tick,
@ -148,7 +152,7 @@ impl Tui {
CrosstermEvent::FocusLost => Event::FocusLost,
CrosstermEvent::FocusGained => Event::FocusGained,
CrosstermEvent::Paste(s) => Event::Paste(s),
_ => continue, // ignore other events
CrosstermEvent::Key(_) => continue, // ignore other events
}
Some(Err(_)) => Event::Error,
None => break, // the event stream has stopped and will not produce any more events

View file

@ -16,7 +16,7 @@
[package]
name = "deja-vu"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2021"
edition = "2024"
license = "GPL"
version = "0.2.0"

View file

@ -16,14 +16,22 @@
use core::{
action::Action,
components::{
footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, state::StatefulList,
title::Title, Component,
Component,
footer::Footer,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
},
config::Config,
line::{get_disk_description_right, get_part_description, DVLine},
state::{CloneSettings, Mode},
line::{DVLine, get_disk_description_right, get_part_description},
state::Mode,
system::{
boot, cpu::get_cpu_name, disk::PartitionTableType, diskpart::build_dest_format_script,
boot,
cpu::get_cpu_name,
disk::PartitionTableType,
diskpart::{FormatUseCase, build_dest_format_script},
drivers,
},
tasks::{Task, TaskResult, TaskType, Tasks},
@ -46,6 +54,8 @@ use ratatui::{
use tokio::sync::mpsc;
use tracing::{debug, info};
use crate::state::State;
pub struct App {
// TUI
action_rx: mpsc::UnboundedReceiver<Action>,
@ -58,11 +68,11 @@ pub struct App {
should_suspend: bool,
tick_rate: f64,
// App
clone: CloneSettings,
cur_mode: Mode,
list: StatefulList<usize>,
prev_mode: Mode,
selections: Vec<Option<usize>>,
state: State,
tasks: Tasks,
}
@ -77,7 +87,6 @@ impl App {
action_tx,
components: vec![
Box::new(Title::new("Clone Tool")),
Box::new(FpsCounter::new()),
Box::new(Left::new()),
Box::new(Right::new()),
Box::new(Footer::new()),
@ -90,7 +99,7 @@ impl App {
should_suspend: false,
tick_rate,
// App
clone: CloneSettings::new(disk_list_arc),
state: State::new(disk_list_arc),
cur_mode: Mode::default(),
list: StatefulList::default(),
prev_mode: Mode::default(),
@ -100,7 +109,7 @@ impl App {
}
pub fn prev_mode(&mut self) -> Option<Mode> {
let new_mode = match self.cur_mode {
match self.cur_mode {
Mode::Home => Some(Mode::Home),
Mode::Failed => Some(Mode::Failed),
Mode::Done => Some(Mode::Done),
@ -120,16 +129,20 @@ impl App {
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"),
};
new_mode
| Mode::Process
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}
}
pub fn next_mode(&mut self) -> Option<Mode> {
let new_mode = match self.cur_mode {
Mode::Home => Mode::ScanDisks,
Mode::InstallDrivers => Mode::ScanDisks,
Mode::Home | Mode::InstallDrivers => Mode::ScanDisks,
Mode::ScanDisks => Mode::SelectDisks,
Mode::SelectDisks => Mode::SelectTableType,
Mode::SelectTableType => Mode::Confirm,
@ -145,8 +158,14 @@ impl App {
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::Process
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
};
if new_mode == self.cur_mode {
@ -161,7 +180,7 @@ impl App {
info!("Setting mode to {new_mode:?}");
self.cur_mode = new_mode;
match new_mode {
Mode::InstallDrivers => self.clone.scan_drivers(),
Mode::InstallDrivers => self.state.scan_drivers(),
Mode::ScanDisks => {
self.prev_mode = self.cur_mode;
if self.tasks.idle() {
@ -179,18 +198,7 @@ impl App {
))?;
// Get System32 path
let system32 = if cfg!(windows) {
if let Ok(path) = env::var("SYSTEMROOT") {
format!("{path}/System32")
} else {
self.action_tx.send(Action::Error(String::from(
"ERROR\n\n\nFailed to find SYSTEMROOT",
)))?;
return Ok(());
}
} else {
String::from(".")
};
let system32 = get_system32_path(&self.action_tx);
// (Re)Enable volume mounting
self.tasks.add(TaskType::CommandWait(
@ -199,15 +207,16 @@ impl App {
));
// Build Diskpart script to format destination disk
let disk_list = self.clone.disk_list.lock().unwrap();
if let Some(disk_index) = self.clone.disk_index_dest {
if let Some(disk) = disk_list.get(disk_index) {
let table_type = self.clone.table_type.clone().unwrap();
let diskpart_script = build_dest_format_script(disk.id, &table_type);
let disk_list = self.state.disk_list.lock().unwrap();
if let Some(disk_index) = self.state.disk_index_dest
&& let Some(disk) = disk_list.get(disk_index)
{
let table_type = self.state.table_type.clone().unwrap();
let diskpart_script =
build_dest_format_script(disk.id, &table_type, FormatUseCase::Clone);
self.tasks.add(TaskType::Diskpart(diskpart_script));
}
}
}
Mode::Clone => {
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
@ -217,7 +226,7 @@ impl App {
self.config.clone_app_path.clone(),
Vec::new(),
));
if let Some(dest_index) = self.clone.disk_index_dest {
if let Some(dest_index) = self.state.disk_index_dest {
self.tasks.add(TaskType::UpdateDestDisk(dest_index));
}
}
@ -228,26 +237,16 @@ impl App {
))?;
// Get System32 path
let system32 = if cfg!(windows) {
if let Ok(path) = env::var("SYSTEMROOT") {
format!("{path}/System32")
} else {
self.action_tx.send(Action::Error(String::from(
"ERROR\n\n\nFailed to find SYSTEMROOT",
)))?;
return Ok(());
}
} else {
String::from(".")
};
let system32 = get_system32_path(&self.action_tx);
// Add actions
let disk_list = self.clone.disk_list.lock().unwrap();
if let Some(disk_index) = self.clone.disk_index_dest {
if let Some(disk) = disk_list.get(disk_index) {
let table_type = self.clone.table_type.clone().unwrap();
let letter_boot = disk.get_part_letter(self.clone.part_index_boot.unwrap());
let letter_os = disk.get_part_letter(self.clone.part_index_os.unwrap());
let disk_list = self.state.disk_list.lock().unwrap();
if let Some(disk_index) = self.state.disk_index_dest
&& let Some(disk) = disk_list.get(disk_index)
{
let table_type = self.state.table_type.clone().unwrap();
let letter_boot = disk.get_part_letter(self.state.part_index_boot.unwrap());
let letter_os = disk.get_part_letter(self.state.part_index_os.unwrap());
// Safety check
if letter_boot.is_empty() || letter_os.is_empty() {
@ -258,14 +257,18 @@ impl App {
}
// Create boot files
for task in
boot::configure_disk(&letter_boot, &letter_os, &system32, table_type)
{
for task in boot::configure_disk(
&letter_boot,
&letter_os,
boot::SafeMode::Enable,
&system32,
&table_type,
) {
self.tasks.add(task);
}
// Inject driver(s) (if selected)
if let Some(driver) = &self.clone.driver {
if let Some(driver) = &self.state.driver {
if let Ok(task) = boot::inject_driver(driver, &letter_os, &system32) {
self.tasks.add(task);
} else {
@ -277,7 +280,6 @@ impl App {
}
}
}
}
Mode::Done => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?;
@ -425,29 +427,29 @@ impl App {
Action::Select(one, two) => {
match self.cur_mode {
Mode::InstallDrivers => {
if let Some(index) = one {
if let Some(driver) = self.clone.driver_list.get(index).cloned() {
if let Some(index) = one
&& let Some(driver) = self.state.driver_list.get(index).cloned()
{
drivers::load(&driver.inf_paths);
self.clone.driver = Some(driver);
}
self.state.driver = Some(driver);
}
}
Mode::SelectDisks => {
self.clone.disk_index_source = one;
self.clone.disk_index_dest = two;
self.state.disk_index_source = one;
self.state.disk_index_dest = two;
}
Mode::SelectParts => {
self.clone.part_index_boot = one;
self.clone.part_index_os = two;
self.state.part_index_boot = one;
self.state.part_index_os = two;
}
Mode::SelectTableType => {
self.clone.table_type = {
self.state.table_type = {
if let Some(index) = one {
match index {
0 => Some(PartitionTableType::Guid),
1 => Some(PartitionTableType::Legacy),
index => {
panic!("Failed to select PartitionTableType: {}", index)
panic!("Failed to select PartitionTableType: {index}")
}
}
} else {
@ -464,22 +466,21 @@ impl App {
// Clear TableType selection
match new_mode {
Mode::SelectDisks | Mode::SelectTableType => {
self.clone.table_type = None;
self.state.table_type = None;
}
_ => {}
}
self.set_mode(new_mode)?;
self.action_tx
.send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?;
self.action_tx.send(build_left_items(self, self.cur_mode))?;
self.action_tx
.send(build_right_items(self, self.cur_mode))?;
self.action_tx.send(build_left_items(self))?;
self.action_tx.send(build_right_items(self))?;
match new_mode {
Mode::SelectTableType | Mode::Confirm => {
// Select source/dest disks
self.action_tx.send(Action::SelectRight(
self.clone.disk_index_source,
self.clone.disk_index_dest,
self.state.disk_index_source,
self.state.disk_index_dest,
))?;
}
Mode::SelectParts => {
@ -487,7 +488,7 @@ impl App {
self.action_tx.send(Action::Select(Some(0), None))?;
// Highlight 2nd or 3rd partition as OS partition
let index = if let Some(table_type) = &self.clone.table_type {
let index = if let Some(table_type) = &self.state.table_type {
match table_type {
PartitionTableType::Guid => 2,
PartitionTableType::Legacy => 1,
@ -554,11 +555,7 @@ impl App {
fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] {
let component_areas = vec![
header, // Title Bar
header, // FPS Counter
left, right, footer, popup,
];
let component_areas = vec![header, left, right, footer, popup];
for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) {
let _ = self
@ -645,69 +642,75 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::Process
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}
}
fn build_left_items(app: &App, cur_mode: Mode) -> Action {
let select_num: usize;
fn build_left_items(app: &App) -> Action {
let select_type: SelectionType;
let title: String;
let mut items = Vec::new();
let mut labels: Vec<String> = Vec::new();
match cur_mode {
match app.cur_mode {
Mode::Home => {
select_num = 0;
select_type = SelectionType::Loop;
title = String::from("Home");
}
Mode::InstallDrivers => {
select_num = 1;
select_type = SelectionType::One;
title = String::from("Install Drivers");
app.clone
app.state
.driver_list
.iter()
.for_each(|driver| items.push(driver.to_string()));
}
Mode::SelectDisks => {
select_num = 2;
select_type = SelectionType::Two;
title = String::from("Select Source and Destination Disks");
labels.push(String::from("source"));
labels.push(String::from("dest"));
let disk_list = app.clone.disk_list.lock().unwrap();
let disk_list = app.state.disk_list.lock().unwrap();
disk_list
.iter()
.for_each(|disk| items.push(disk.description.to_string()));
}
Mode::SelectTableType => {
select_num = 1;
select_type = SelectionType::One;
title = String::from("Select Partition Table Type");
items.push(format!("{}", PartitionTableType::Guid));
items.push(format!("{}", PartitionTableType::Legacy));
}
Mode::Confirm => {
select_num = 0;
select_type = SelectionType::Loop;
title = String::from("Confirm Selections");
}
Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => {
select_num = 0;
select_type = SelectionType::Loop;
title = String::from("Processing");
}
Mode::SelectParts => {
select_num = 2;
select_type = SelectionType::Two;
title = String::from("Select Boot and OS Partitions");
labels.push(String::from("boot"));
labels.push(String::from("os"));
let disk_list = app.clone.disk_list.lock().unwrap();
if let Some(index) = app.clone.disk_index_dest {
if let Some(disk) = disk_list.get(index) {
let disk_list = app.state.disk_list.lock().unwrap();
if let Some(index) = app.state.disk_index_dest
&& let Some(disk) = disk_list.get(index)
{
disk.get_parts().iter().for_each(|part| {
items.push(part.to_string());
});
}
}
}
Mode::Done | Mode::Failed => {
select_num = 0;
select_type = SelectionType::Loop;
title = String::from("Done");
}
// Invalid states
@ -716,17 +719,23 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::Process
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
};
Action::UpdateLeft(title, labels, items, select_num)
Action::UpdateLeft(title, labels, items, select_type)
}
fn build_right_items(app: &App, cur_mode: Mode) -> Action {
fn build_right_items(app: &App) -> Action {
let mut items = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0;
match cur_mode {
match app.cur_mode {
Mode::InstallDrivers => {
items.push(vec![DVLine {
line_parts: vec![String::from("CPU")],
@ -751,7 +760,7 @@ fn build_right_items(app: &App, cur_mode: Mode) -> Action {
],
line_colors: vec![Color::Cyan, Color::Red],
};
if let Some(table_type) = &app.clone.table_type {
if let Some(table_type) = &app.state.table_type {
// Show table type
let type_str = match table_type {
PartitionTableType::Guid => "GPT",
@ -767,29 +776,29 @@ fn build_right_items(app: &App, cur_mode: Mode) -> Action {
} else {
labels.push(vec![dest_dv_line]);
}
let disk_list = app.clone.disk_list.lock().unwrap();
let disk_list = app.state.disk_list.lock().unwrap();
disk_list
.iter()
.for_each(|disk| items.push(get_disk_description_right(&disk, None)));
.for_each(|disk| items.push(get_disk_description_right(disk, &None)));
}
Mode::SelectParts => {
vec!["Boot", "OS"].iter().for_each(|s| {
for s in &["Boot", "OS"] {
labels.push(vec![DVLine {
line_parts: vec![String::from(*s)],
line_colors: vec![Color::Cyan],
}])
});
if let Some(index) = app.clone.disk_index_dest {
}]);
}
if let Some(index) = app.state.disk_index_dest {
start_index = 1;
let disk_list = app.clone.disk_list.lock().unwrap();
let disk_list = app.state.disk_list.lock().unwrap();
if let Some(disk) = disk_list.get(index) {
// Disk Details
items.push(get_disk_description_right(&disk, None));
items.push(get_disk_description_right(disk, &None));
// Partition Details
disk.parts
.iter()
.for_each(|part| items.push(get_part_description(&part)));
.for_each(|part| items.push(get_part_description(part)));
}
}
}
@ -797,3 +806,19 @@ fn build_right_items(app: &App, cur_mode: Mode) -> Action {
}
Action::UpdateRight(labels, start_index, items)
}
pub fn get_system32_path(action_tx: &mpsc::UnboundedSender<Action>) -> String {
let mut system32_path = String::from(".");
if cfg!(windows) {
if let Ok(path) = env::var("SYSTEMROOT") {
system32_path = format!("{path}/System32");
} else {
action_tx
.send(Action::Error(String::from(
"ERROR\n\n\nFailed to find SYSTEMROOT",
)))
.expect("Failed to find SYSTEMROOT and then failed to send action");
}
}
system32_path
}

View file

@ -15,11 +15,11 @@
//
use clap::Parser;
use color_eyre::Result;
use core;
use crate::app::App;
mod app;
mod state;
#[tokio::main]
async fn main() -> Result<()> {
@ -30,11 +30,9 @@ async fn main() -> Result<()> {
msg.replace("Administrator privedges required for Deja-Vu.");
}
};
match msg {
Some(text) => {
if let Some(text) = msg {
println!("{text}");
}
None => {
} else {
core::errors::init()?;
core::logging::init()?;
@ -42,6 +40,5 @@ async fn main() -> Result<()> {
let mut app = App::new(args.tick_rate, args.frame_rate)?;
app.run().await?;
}
}
Ok(())
}

47
deja_vu/src/state.rs Normal file
View file

@ -0,0 +1,47 @@
// 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::sync::{Arc, Mutex};
use core::system::{
disk::{Disk, PartitionTableType},
drivers,
};
#[derive(Debug, Default)]
pub struct State {
pub disk_index_dest: Option<usize>,
pub disk_index_source: Option<usize>,
pub disk_list: Arc<Mutex<Vec<Disk>>>,
pub driver_list: Vec<drivers::Driver>,
pub part_index_boot: Option<usize>,
pub part_index_os: Option<usize>,
pub driver: Option<drivers::Driver>,
pub table_type: Option<PartitionTableType>,
}
impl State {
pub fn new(disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
State {
disk_list,
..Default::default()
}
}
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
}

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
logo.xcf Normal file

Binary file not shown.

View file

@ -16,7 +16,7 @@
[package]
name = "pe-menu"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2021"
edition = "2024"
license = "GPL"
version = "0.2.0"

View file

@ -16,8 +16,13 @@
use core::{
action::Action,
components::{
footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, state::StatefulList,
title::Title, Component,
Component,
footer::Footer,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
},
config::Config,
line::DVLine,
@ -76,14 +81,13 @@ impl App {
let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
let mut list = StatefulList::default();
list.set_items(load_tools());
list.set_items(load_tools(action_tx.clone()));
Ok(Self {
// TUI
action_rx,
action_tx,
components: vec![
Box::new(Title::new("PE Menu")),
Box::new(FpsCounter::new()),
Box::new(Left::new()),
Box::new(Right::new()),
Box::new(Footer::new()),
@ -199,8 +203,9 @@ impl App {
Action::ClearScreen => tui.terminal.clear()?,
Action::KeyUp => {
self.list.previous();
if let Some(tool) = self.list.get_selected() {
if tool.separator {
if let Some(tool) = self.list.get_selected()
&& tool.separator
{
// Skip over separator
self.list.previous();
if let Some(index) = self.list.selected() {
@ -208,11 +213,11 @@ impl App {
}
}
}
}
Action::KeyDown => {
self.list.next();
if let Some(tool) = self.list.get_selected() {
if tool.separator {
if let Some(tool) = self.list.get_selected()
&& tool.separator
{
// Skip over separator
self.list.next();
if let Some(index) = self.list.selected() {
@ -220,7 +225,6 @@ impl App {
}
}
}
}
Action::Error(ref msg) => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?;
@ -230,7 +234,7 @@ impl App {
// Run selected tool
if let Some(tool) = self.list.get_selected() {
info!("Run tool: {:?}", &tool);
self.tasks.add(build_tool_command(&self, &tool));
self.tasks.add(build_tool_command(self, &tool));
}
}
Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
@ -299,11 +303,7 @@ impl App {
fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] {
let component_areas = vec![
header, // Title Bar
header, // FPS Counter
left, right, footer, popup,
];
let component_areas = vec![header, left, right, footer, popup];
for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) {
let _ = self
@ -385,13 +385,13 @@ pub fn build_tool_command(app: &App, tool: &Tool) -> TaskType {
cmd_path = PathBuf::from(tool.command.clone());
start_index = 0;
}
if let Some(args) = &tool.args {
if args.len() > start_index {
if let Some(args) = &tool.args
&& args.len() > start_index
{
args[start_index..].iter().for_each(|a| {
cmd_args.push(a.clone());
});
}
}
TaskType::CommandNoWait(cmd_path, cmd_args)
}
@ -411,7 +411,7 @@ fn build_left_items(app: &App) -> Action {
})
// ─
.collect();
Action::UpdateLeft(title, labels, items, 0)
Action::UpdateLeft(title, labels, items, SelectionType::Loop)
}
fn build_right_items(app: &App) -> Action {
@ -441,21 +441,45 @@ fn build_right_items(app: &App) -> Action {
Action::UpdateRight(labels, start_index, items)
}
pub fn load_tools() -> Vec<Tool> {
pub fn load_tools(action_tx: mpsc::UnboundedSender<Action>) -> Vec<Tool> {
let mut entries: Vec<PathBuf>;
let mut tools: Vec<Tool> = Vec::new();
let exe_path = env::current_exe().expect("Failed to find main executable");
let tool_config_path = exe_path.parent().unwrap().join("menu_entries");
let mut entries: Vec<PathBuf> = std::fs::read_dir(tool_config_path)
.expect("Failed to find any tool configs")
if let Ok(read_dir) = std::fs::read_dir(tool_config_path) {
entries = read_dir
.map(|res| res.map(|e| e.path()))
.filter_map(Result::ok)
.collect();
entries.sort();
entries
.iter()
.map(|entry| {
let contents = fs::read_to_string(&entry).expect("Failed to read tool config file");
let tool: Tool = toml::from_str(&contents).expect("Failed to parse tool config file");
tool
})
.collect()
} else {
action_tx
.send(Action::Error(String::from(
"Failed to find any tool configs",
)))
.unwrap();
entries = Vec::new();
}
entries.iter().for_each(|entry| {
if let Ok(toml_str) = fs::read_to_string(entry) {
if let Ok(tool) = toml::from_str(&toml_str) {
tools.push(tool);
} else {
action_tx
.send(Action::Error(format!(
"Failed to parse tool config file: {:?}",
&entry,
)))
.unwrap();
}
} else {
action_tx
.send(Action::Error(format!(
"Failed to read tool config file: {:?}",
&entry,
)))
.unwrap();
}
});
tools
}

View file

@ -15,7 +15,6 @@
//
use clap::Parser;
use color_eyre::Result;
use core;
use crate::app::App;

50
win_installer/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
# 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 = "win-installer"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024"
license = "GPL"
version = "0.1.0"
[dependencies]
core = { path = "../core" }
clap = { version = "4.4.5", features = [
"derive",
"cargo",
"wrap_help",
"unicode",
"string",
"unstable-styles",
] }
color-eyre = "0.6.3"
crossterm = { version = "0.28.1", features = ["event-stream"] }
futures = "0.3.30"
ratatui = "0.29.0"
serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] }
toml = "0.8.13"
tracing = "0.1.41"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tempfile = "3.23.0"
windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] }
xml = "1.1.0"
tui-input = "0.14.0"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

28
win_installer/build.rs Normal file
View file

@ -0,0 +1,28 @@
// 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 anyhow::Result;
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};
fn main() -> Result<()> {
let build = BuildBuilder::all_build()?;
let gix = GixBuilder::all_git()?;
let cargo = CargoBuilder::all_cargo()?;
Emitter::default()
.add_instructions(&build)?
.add_instructions(&gix)?
.add_instructions(&cargo)?
.emit()
}

1082
win_installer/src/app.rs Normal file

File diff suppressed because it is too large Load diff

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/>.
//
pub mod set_username;
pub mod wim_scan;

View file

@ -0,0 +1,135 @@
// 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 color_eyre::Result;
use core::{action::Action, components::Component, config::Config, state::Mode, tui::Event};
use crossterm::event::KeyCode;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use tui_input::{Input, InputRequest};
#[derive(Default)]
pub struct InputUsername {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
input: Input,
mode: Mode,
}
impl InputUsername {
#[must_use]
pub fn new() -> Self {
Self {
input: Input::new(String::from("")),
..Default::default()
}
}
}
impl Component for InputUsername {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
if self.mode != Mode::SetUserName {
return Ok(None);
}
let action = match event {
Some(Event::Key(key_event)) => match key_event.code {
KeyCode::Backspace => {
self.input.handle(InputRequest::DeletePrevChar);
None
}
KeyCode::Char(c) => {
let ok_chars: Vec<char> = vec![' ', '-', '_'];
if c.is_ascii_alphanumeric() || ok_chars.contains(&c) {
self.input.handle(InputRequest::InsertChar(c));
}
None
}
KeyCode::Enter => {
let username = self.input.value();
Some(Action::SetUserName(String::from(username)))
}
KeyCode::Esc => Some(Action::SetMode(Mode::Home)),
_ => None,
},
Some(Event::Mouse(_)) => None,
_ => None,
};
Ok(action)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::SetMode(mode) => self.mode = mode.clone(),
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::SetUserName {
// Bail early
return Ok(());
}
// Set areas
let [_, center_area, _] = Layout::horizontal([
Constraint::Length(1),
Constraint::Min(1),
Constraint::Length(1),
])
.areas(area);
let [_, input_area, _] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Length(1),
])
.areas(center_area);
frame.render_widget(Clear, area);
let outer_block = Block::bordered().cyan().bold();
frame.render_widget(outer_block, area);
// Input Box
let width = input_area.width.max(3) - 3; // keep 2 for borders and 1 for cursor
let scroll = self.input.visual_scroll(width as usize);
let input = Paragraph::new(self.input.value())
.scroll((0, scroll as u16))
.white()
.block(Block::new().borders(Borders::ALL).title("Enter Username"));
frame.render_widget(input, input_area);
// Ratatui hides the cursor unless it's explicitly set. Position the cursor past the
// end of the input text and one line down from the border to the input line
let x = self.input.visual_cursor().max(scroll) - scroll + 1;
frame.set_cursor_position((input_area.x + x as u16, input_area.y + 1));
// Done
Ok(())
}
}

View file

@ -0,0 +1,154 @@
// 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 core::{action::Action, components::Component, config::Config, state::Mode};
use std::{
iter::zip,
sync::{Arc, Mutex},
};
use color_eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, List, ListItem, Padding, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use crate::wim::WimSources;
#[derive(Default)]
pub struct WimScan {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
mode: Mode,
scan_network: bool,
wim_sources: Arc<Mutex<WimSources>>,
}
impl WimScan {
#[must_use]
pub fn new(wim_sources: Arc<Mutex<WimSources>>) -> Self {
let wim_sources = wim_sources.clone();
Self {
wim_sources,
..Default::default()
}
}
}
impl Component for WimScan {
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
let _ = key; // to appease clippy
Ok(None)
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::FindWimNetwork => self.scan_network = true,
Action::SetMode(new_mode) => {
self.mode = new_mode;
}
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::ScanWinSources {
return Ok(());
}
frame.render_widget(Clear, area);
// Prep
let [left, right] = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(area);
let [left_title, left_body] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.areas(left);
let [right_title, right_body] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.areas(right);
// Titles
let titles = vec![
Paragraph::new(Line::from("Local").centered())
.block(Block::default().borders(Borders::NONE)),
Paragraph::new(Line::from("Network").centered())
.block(Block::default().borders(Borders::NONE)),
];
for (title, area) in zip(titles, [left_title, right_title]) {
frame.render_widget(title, area);
}
// WIM Info
if let Ok(wim_sources) = self.wim_sources.lock() {
// Local
let mut left_list = Vec::new();
if wim_sources.thread_local.is_some() {
left_list.push(ListItem::new("Scanning..."));
} else {
left_list.extend(
wim_sources
.local
.iter()
.map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))),
);
}
let left_list = List::new(left_list).block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(1, 1, 1, 1)),
);
frame.render_widget(left_list, left_body);
// Network
let mut right_list = Vec::new();
if wim_sources.thread_network.is_some() {
right_list.push(ListItem::new("Scanning..."));
} else {
right_list.extend(wim_sources.network.iter().map(|wimfile| {
ListItem::new(format!(
"{}\n\n",
wimfile.path.split("\\").last().unwrap_or(&wimfile.path)
))
}));
}
let right_list = List::new(right_list).block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(1, 1, 1, 1)),
);
frame.render_widget(right_list, right_body);
}
// Done
Ok(())
}
}

36
win_installer/src/main.rs Normal file
View file

@ -0,0 +1,36 @@
// 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 clap::Parser;
use color_eyre::Result;
use crate::app::App;
mod app;
mod components;
mod net;
mod state;
mod wim;
#[tokio::main]
async fn main() -> Result<()> {
core::errors::init()?;
core::logging::init()?;
let args = core::cli::Cli::parse();
let mut app = App::new(args.tick_rate, args.frame_rate)?;
app.run().await?;
Ok(())
}

69
win_installer/src/net.rs Normal file
View file

@ -0,0 +1,69 @@
// 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/>.
//
//#![windows_subsystem = "windows"]
use std::ffi::CString;
use windows_sys::Win32::Foundation::NO_ERROR;
use windows_sys::Win32::NetworkManagement::WNet;
fn to_cstr(s: &str) -> CString {
CString::new(s).unwrap()
}
pub fn connect_network_share(
server: &str,
share: &str,
username: &str,
password: &str,
) -> Result<(), u32> {
let remote_name = to_cstr(&format!("\\\\{server}\\{share}"));
// init resources
let mut resources = WNet::NETRESOURCEA {
dwDisplayType: WNet::RESOURCEDISPLAYTYPE_SHAREADMIN,
dwScope: WNet::RESOURCE_GLOBALNET,
dwType: WNet::RESOURCETYPE_DISK,
dwUsage: WNet::RESOURCEUSAGE_ALL,
lpComment: std::ptr::null_mut(),
lpLocalName: std::ptr::null_mut(), // PUT a volume here if you want to mount as a windows volume
lpProvider: std::ptr::null_mut(),
lpRemoteName: remote_name.as_c_str().as_ptr() as *mut u8,
};
let username = format!("{server}\\{username}");
let username = to_cstr(&username);
let password = to_cstr(password);
// mount
let result = unsafe {
let username_ptr = username.as_ptr();
let password_ptr = password.as_ptr();
WNet::WNetAddConnection2A(
&mut resources as *mut WNet::NETRESOURCEA,
password_ptr as *const u8,
username_ptr as *const u8,
//WNet::CONNECT_INTERACTIVE, // Interactive will show a system dialog in case credentials are wrong to retry with the password. Put 0 if you don't want it
0,
)
};
if result == NO_ERROR {
Ok(())
} else {
Err(result)
}
}

215
win_installer/src/state.rs Normal file
View file

@ -0,0 +1,215 @@
// 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::{
fs::read_dir,
sync::{Arc, Mutex},
};
use core::{
config::Config,
system::{
disk::{Disk, PartitionTableType},
drivers,
},
};
use crate::{
net::connect_network_share,
wim::{WimFile, WimSources, parse_wim_file},
};
pub enum ScanType {
GeneralWimFiles, // Includes Windows installer WIMs
WindowsInstallers,
}
#[derive(Debug, Default)]
pub struct State {
pub config: Config,
pub disk_index_dest: Option<usize>,
pub disk_list: Arc<Mutex<Vec<Disk>>>,
pub driver: Option<drivers::Driver>,
pub driver_list: Vec<drivers::Driver>,
pub table_type: Option<PartitionTableType>,
pub username: Option<String>,
pub wim_file_index: Option<usize>,
pub wim_image_index: Option<usize>,
pub wim_sources: Arc<Mutex<WimSources>>,
}
impl State {
pub fn new(config: Config, disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
let wim_sources = Arc::new(Mutex::new(WimSources::new()));
State {
config,
disk_list,
wim_sources,
..Default::default()
}
}
pub fn reset_all(&mut self) {
self.wim_file_index = None;
self.wim_image_index = None;
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset_all();
}
}
pub fn reset_local(&mut self) {
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset_local();
}
}
pub fn reset_network(&mut self) {
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset_network();
}
}
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
pub fn scan_wim_local(&mut self, scan_type: ScanType) {
let disk_list_arc = self.disk_list.clone();
let wim_sources_arc = self.wim_sources.clone();
let wim_sources_arc_inner = self.wim_sources.clone();
if let Ok(mut wim_sources) = wim_sources_arc.lock()
&& wim_sources.thread_local.is_none()
{
wim_sources.thread_local = Some(tokio::task::spawn(async move {
scan_local_drives(disk_list_arc, wim_sources_arc_inner, scan_type);
}));
}
}
pub fn scan_wim_network(&mut self) {
let wim_sources_arc = self.wim_sources.clone();
let wim_sources_arc_inner = self.wim_sources.clone();
if let Ok(mut wim_sources) = wim_sources_arc.lock()
&& wim_sources.thread_network.is_none()
{
let config = self.config.clone();
wim_sources.thread_network = Some(tokio::task::spawn(async move {
scan_network_share(config, wim_sources_arc_inner);
}));
}
}
}
fn get_subfolders(path_str: &str) -> Vec<String> {
if let Ok(read_dir) = read_dir(path_str) {
read_dir
.filter_map(|item| item.ok())
.map(|item| item.path().to_string_lossy().into_owned())
.collect()
} else {
// TODO: Use better error handling here?
Vec::new()
}
}
pub fn scan_local_drives(
disk_list_arc: Arc<Mutex<Vec<Disk>>>,
wim_sources_arc: Arc<Mutex<WimSources>>,
scan_type: ScanType,
) {
let mut to_check: Vec<String> = Vec::new();
let mut wim_files: Vec<WimFile> = Vec::new();
// Get drive letters
if let Ok(disk_list) = disk_list_arc.lock() {
disk_list.iter().for_each(|d| {
d.parts.iter().for_each(|p| {
if !p.letter.is_empty() {
match scan_type {
ScanType::GeneralWimFiles => {
to_check.append(&mut get_subfolders(&format!("{}:\\", &p.letter)));
}
ScanType::WindowsInstallers => {
to_check.push(format!("{}:\\Images", &p.letter));
}
}
}
});
})
}
// Scan drives
to_check.iter().for_each(|scan_path| {
let is_backup = !scan_path.ends_with("\\Images");
if let Ok(read_dir) = read_dir(scan_path) {
read_dir.for_each(|item| {
if let Ok(item) = item
&& item.file_name().to_string_lossy().ends_with(".wim")
&& let Some(path_str) = item.path().to_str()
&& let Ok(new_source) = parse_wim_file(path_str, is_backup)
{
wim_files.push(new_source);
}
});
}
});
// Done
wim_files.sort();
if let Ok(mut wim_sources) = wim_sources_arc.lock() {
wim_files
.into_iter()
.for_each(|file| wim_sources.add_local(file));
}
}
pub fn scan_network_share(config: Config, wim_sources_arc: Arc<Mutex<WimSources>>) {
let result = connect_network_share(
&config.network_server,
&config.network_share,
&config.network_user,
&config.network_pass,
);
let mut wim_files: Vec<WimFile> = Vec::new();
// Connect to share
if result.is_err() {
return;
}
// Scan share
let share_dir = format!("\\\\{}\\{}", &config.network_server, &config.network_share);
if let Ok(read_dir) = read_dir(share_dir) {
read_dir.for_each(|item| {
if let Ok(item) = item
&& item.file_name().to_string_lossy().ends_with(".wim")
&& let Some(path_str) = item.path().to_str()
&& let Ok(new_source) = parse_wim_file(path_str, false)
// Assuming all network sources are installers
{
wim_files.push(new_source);
}
});
}
// Done
wim_files.sort();
if let Ok(mut wim_sources) = wim_sources_arc.lock() {
wim_files
.into_iter()
.for_each(|file| wim_sources.add_network(file));
}
}

336
win_installer/src/wim.rs Normal file
View file

@ -0,0 +1,336 @@
// 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::{
cmp::Ordering,
collections::HashMap,
env, fmt,
fs::File,
io::BufReader,
path::{Path, PathBuf},
process::Command,
sync::LazyLock,
};
use tempfile::NamedTempFile;
use tokio::task::JoinHandle;
use xml::reader::{EventReader, XmlEvent};
use core::system::disk::bytes_to_string;
const UNATTEND_XML: &str = include_str!("../../config/unattend.xml");
static WIMINFO_EXE: LazyLock<String> = LazyLock::new(|| {
let program_files =
PathBuf::from(env::var("PROGRAMFILES").expect("Failed to resolve %PROGRAMFILES%"));
program_files
.join("wimlib/wiminfo.cmd")
.to_string_lossy()
.into_owned()
});
static WIN_BUILDS: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from([
// Windows 10
("10240", "1507 \"Threshold 1\""),
("10586", "1511 \"Threshold 2\""),
("14393", "1607 \"Redstone 1\""),
("15063", "1703 \"Redstone 2\""),
("16299", "1709 \"Redstone 3\""),
("17134", "1803 \"Redstone 4\""),
("17763", "1809 \"Redstone 5\""),
("18362", "1903 / 19H1"),
("18363", "1909 / 19H2"),
("19041", "2004 / 20H1"),
("19042", "20H2"),
("19043", "21H1"),
("19044", "21H2"),
("19045", "22H2"),
// Windows 11
("22000", "21H2"),
("22621", "22H2"),
("22631", "23H2"),
("26100", "24H2"),
("26200", "25H2"),
])
});
#[derive(Clone, Debug)]
pub struct WimFile {
pub path: String,
pub images: Vec<WimImage>,
pub is_backup: bool,
}
impl WimFile {
pub fn summary(&self) -> String {
let mut s = format!("{self}");
self.images.iter().for_each(|image| {
let image = format!("\n\t\t{image}");
s.push_str(&image);
});
s
}
}
impl fmt::Display for WimFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.path.split("\\").last().unwrap_or(&self.path))
}
}
impl PartialEq for WimFile {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for WimFile {}
impl Ord for WimFile {
fn cmp(&self, other: &Self) -> Ordering {
self.path.cmp(&other.path)
}
}
impl PartialOrd for WimFile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Default)]
pub struct WimImage {
pub build: String,
pub index: String,
pub name: String,
pub size: u64,
pub spbuild: String,
pub version: String,
}
impl WimImage {
pub fn new() -> Self {
Default::default()
}
pub fn reset(&mut self) {
self.build.clear();
self.index.clear();
self.name.clear();
self.spbuild.clear();
self.version.clear();
}
}
impl fmt::Display for WimImage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Windows 11 Home (24H2, 26100.xxxx)
let s = if self.version.is_empty() {
String::new()
} else {
format!("{}, ", self.version)
};
write!(
f,
"{} ({}{}.{}) [{}]",
self.name,
s,
self.build,
self.spbuild,
bytes_to_string(self.size)
)
}
}
#[derive(Debug, Default)]
pub struct WimSources {
pub local: Vec<WimFile>,
pub network: Vec<WimFile>,
pub thread_local: Option<JoinHandle<()>>,
pub thread_network: Option<JoinHandle<()>>,
}
impl WimSources {
pub fn new() -> Self {
Default::default()
}
pub fn add_local(&mut self, wim_file: WimFile) {
self.local.push(wim_file);
}
pub fn add_network(&mut self, wim_file: WimFile) {
self.network.push(wim_file);
}
pub fn get_file(&self, index: usize) -> WimFile {
let rel_index: usize;
let num_local = self.local.len();
let mut use_local = true;
if index < num_local {
rel_index = index;
} else {
rel_index = index - num_local;
use_local = false;
};
if use_local {
self.local.get(rel_index).unwrap().clone()
} else {
self.network.get(rel_index).unwrap().clone()
}
}
pub fn get_file_list(&self) -> Vec<WimFile> {
let mut list = self.local.clone();
list.append(&mut self.network.clone());
list
}
pub fn poll(&mut self) {
let thread = self.thread_local.take();
if let Some(local) = thread
&& !local.is_finished()
{
// Task still going, keep tracking
self.thread_local = Some(local);
}
let thread = self.thread_network.take();
if let Some(network) = thread
&& !network.is_finished()
{
// Task still going, keep tracking
self.thread_network = Some(network);
}
}
pub fn reset_all(&mut self) {
self.local.clear();
self.network.clear();
}
pub fn reset_local(&mut self) {
self.local.clear();
}
pub fn reset_network(&mut self) {
self.network.clear();
}
}
pub fn gen_unattend_xml(username: &str) -> String {
UNATTEND_XML.replace("NEWUSERNAME", username)
}
fn get_wim_xml(wim_file: &str) -> std::io::Result<File> {
let tmp_file = NamedTempFile::new()?;
let _ = Command::new(&*WIMINFO_EXE)
.args([
wim_file,
"--extract-xml",
tmp_file.path().as_os_str().to_str().unwrap(),
])
.output()
.expect("Failed to extract XML data");
let file = File::open(tmp_file.path())?;
Ok(file)
}
pub fn parse_wim_file(wim_file: &str, is_backup: bool) -> std::io::Result<WimFile> {
let mut wim_images: Vec<WimImage> = Vec::new();
if !Path::new(wim_file).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Failed to read WIM file",
));
};
let xml_file = get_wim_xml(wim_file).expect("Failed to open XML file");
let file = BufReader::new(xml_file);
let mut current_element = String::new();
let mut image = WimImage::new();
let parser = EventReader::new(file);
for e in parser {
match e {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
current_element = name.local_name.to_uppercase();
if current_element == "IMAGE" {
// Update index
if let Some(attr) = attributes.first()
&& attr.name.to_string().to_lowercase() == "index"
{
image.index = attr.value.clone();
}
}
}
Ok(XmlEvent::Characters(char_data)) => {
if current_element == "BUILD" {
let build = char_data.trim();
image.build = build.to_string();
image.version = WIN_BUILDS.get(build).map_or("", |v| v).to_string();
}
if current_element == "NAME" {
image.name = char_data.trim().to_string();
}
if current_element == "SPBUILD" {
image.spbuild = char_data.trim().to_string();
}
if current_element == "TOTALBYTES" {
let result = char_data.trim().parse::<u64>();
if let Ok(size) = result {
image.size = size;
}
}
}
Ok(XmlEvent::EndElement { name }) => {
if name.local_name.to_uppercase() == "IMAGE" {
if image.size == 0 {
break;
}
// Append image to list
if image.build.is_empty() {
image.build.push('?');
}
if image.spbuild.is_empty() {
image.spbuild.push('?');
}
if !image.name.is_empty() && !image.index.is_empty() {
wim_images.push(image.clone());
}
// Reset image
image.reset()
}
}
Err(_) => {
break;
}
_ => {}
}
}
let wim_file = WimFile {
path: wim_file.to_string(),
images: wim_images,
is_backup,
};
Ok(wim_file)
}