Compare commits

..

No commits in common. "dev" and "move-core-logic-to-lib" have entirely different histories.

74 changed files with 1850 additions and 6931 deletions

669
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,19 @@
# This file is part of Deja-Vu. # This file is part of Deja-vu.
# #
# Deja-Vu is free software: you can redistribute it and/or modify it # 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 # under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Deja-Vu is distributed in the hope that it will be useful, but # Deja-vu is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of # WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details. # See the GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. # along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
[workspace] [workspace]
members = ["core", "boot_diags", "deja_vu", "pe_menu", "win_installer"] members = ["core", "deja_vu", "pe_menu"]
default-members = ["core", "boot_diags", "deja_vu", "pe_menu", "win_installer"] default-members = ["deja_vu", "pe_menu"]
resolver = "2" resolver = "2"
[profile.release]
lto = true

View file

@ -1,48 +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/>.
[package]
name = "boot-diags"
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"] }
check_elevation = "0.2.4"
regex = "1.11.1"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

View file

@ -1,28 +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 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()
}

View file

@ -1,966 +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 core::{
action::{Action, DiagResult},
components::{
Component,
footer::Footer,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
},
config::Config,
line::{DVLine, get_disk_description_right, get_part_description},
state::Mode,
system::{
boot::{self, SafeMode, configure_disk},
cpu::get_cpu_name,
drivers,
},
tasks::{Task, TaskResult, TaskType, Tasks},
tui::{Event, Tui},
};
use std::{
env,
iter::zip,
sync::{Arc, Mutex},
};
use color_eyre::Result;
use ratatui::{
crossterm::event::KeyEvent,
layout::{Constraint, Direction, Layout},
prelude::Rect,
style::Color,
};
use tokio::sync::mpsc;
use tracing::{debug, info};
use crate::{
diags::{
DiagGroup, Type as DiagType, get_diag_type, parse_bcd, parse_bitlocker, parse_chkdsk,
parse_dism, parse_registry_hives, parse_system_files,
},
scan,
state::State,
};
pub struct App {
// TUI
action_rx: mpsc::UnboundedReceiver<Action>,
action_tx: mpsc::UnboundedSender<Action>,
components: Vec<Box<dyn Component>>,
config: Config,
frame_rate: f64,
last_tick_key_events: Vec<KeyEvent>,
should_quit: bool,
should_suspend: bool,
tick_rate: f64,
// App
cur_mode: Mode,
diag_groups: Arc<Mutex<Vec<DiagGroup>>>,
list: StatefulList<Mode>,
boot_modes: Vec<SafeMode>,
setup_modes: Vec<SafeMode>,
selections: Vec<Option<usize>>,
state: State,
system32: String,
tasks: Tasks,
}
impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let diag_groups_arc = Arc::new(Mutex::new(Vec::new()));
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(vec![
Mode::BootScan,
Mode::BootSetup,
Mode::InjectDrivers,
Mode::SetBootMode,
]);
Ok(Self {
// TUI
action_rx,
action_tx,
components: vec![
Box::new(Title::new("Boot Diagnostics")),
Box::new(Left::new()),
Box::new(Right::new()),
Box::new(Footer::new()),
Box::new(popup::Popup::new()),
Box::new(crate::components::progress::Progress::new()),
Box::new(crate::components::logview::LogView::new(
diag_groups_arc.clone(),
)),
],
config: Config::new()?,
frame_rate,
last_tick_key_events: Vec::new(),
should_quit: false,
should_suspend: false,
tick_rate,
// App
state: State::new(disk_list_arc),
cur_mode: Mode::Home,
diag_groups: diag_groups_arc,
list,
boot_modes: vec![SafeMode::Enable, SafeMode::Disable],
setup_modes: vec![SafeMode::Disable, SafeMode::Enable],
system32: String::new(),
selections: vec![None, None],
tasks,
})
}
pub fn inject_driver(&mut self, index: usize) {
if let Some(driver) = self.state.driver_list.get(index)
&& let Some(disk_index) = self.state.disk_index_dest
{
let disk_list = self.state.disk_list.lock().unwrap();
if let Some(disk) = disk_list.get(disk_index)
&& let Some(os_index) = self.state.part_index_os
&& let Ok(task) = boot::inject_driver(
driver,
disk.get_part_letter(os_index).as_str(),
&self.system32,
)
{
self.tasks.add(task);
}
}
}
pub fn next_mode(&mut self) -> Mode {
match self.cur_mode {
Mode::Home => Mode::ScanDisks,
Mode::InstallDrivers => Mode::ScanDisks,
Mode::ScanDisks => Mode::SelectDisks,
Mode::SelectDisks => Mode::SelectParts,
Mode::SelectParts => Mode::DiagMenu,
Mode::BootDiags => Mode::DiagMenu,
Mode::BootScan => Mode::BootDiags,
Mode::BootSetup | Mode::InjectDrivers | Mode::SetBootMode => Mode::Process,
Mode::Process => Mode::DiagMenu,
Mode::Done => Mode::DiagMenu,
Mode::Failed => Mode::Failed,
// Default to current mode
_ => self.cur_mode,
}
}
pub fn set_boot_mode(&mut self, boot_mode: SafeMode) {
let new_mode = match boot_mode {
SafeMode::Disable => "Normal",
SafeMode::Enable => "Safe Mode (minimal)",
};
info!("Setting boot mode to: {new_mode}");
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 Some(boot_index) = self.state.part_index_boot
&& let Ok(task) = boot::set_mode(
disk.get_part_letter(boot_index).as_str(),
&boot_mode,
&self.system32,
&disk.part_type,
)
{
self.tasks.add(task);
};
}
pub fn set_mode(&mut self, new_mode: Mode) -> Result<()> {
info!("Setting mode to {new_mode:?}");
self.cur_mode = new_mode;
match new_mode {
Mode::DiagMenu => {
self.selections[0] = None;
self.selections[1] = None;
self.list.select_first_item();
}
Mode::BootScan => {
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
String::from("Gathering info..."),
))?;
scan::queue_boot_scan_tasks(
self.action_tx.clone(),
self.diag_groups.clone(),
&self.state,
self.system32.clone(),
&mut self.tasks,
)?;
}
Mode::InjectDrivers | Mode::InstallDrivers => self.state.scan_drivers(),
Mode::Process => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Info, String::from("...")))?;
}
Mode::ScanDisks => {
if self.tasks.idle() {
self.tasks.add(TaskType::ScanDisks);
}
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
String::from("Scanning Disks..."),
))?;
}
_ => {}
}
Ok(())
}
pub async fn run(&mut self) -> Result<()> {
let mut tui = Tui::new()?
// .mouse(true) // uncomment this line to enable mouse support
.tick_rate(self.tick_rate)
.frame_rate(self.frame_rate);
tui.enter()?;
for component in &mut self.components {
component.register_action_handler(self.action_tx.clone())?;
}
for component in &mut self.components {
component.register_config_handler(self.config.clone())?;
}
for component in &mut self.components {
component.init(tui.size()?)?;
}
let action_tx = self.action_tx.clone();
// Late init
self.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(".")
};
action_tx.send(Action::SetMode(Mode::ScanDisks))?;
loop {
self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?;
if self.should_suspend {
tui.suspend()?;
action_tx.send(Action::Resume)?;
action_tx.send(Action::ClearScreen)?;
// tui.mouse(true);
tui.enter()?;
} else if self.should_quit {
tui.stop()?;
break;
}
}
tui.exit()?;
Ok(())
}
async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> {
let Some(event) = tui.next_event().await else {
return Ok(());
};
let action_tx = self.action_tx.clone();
match event {
Event::Quit => action_tx.send(Action::Quit)?,
Event::Tick => action_tx.send(Action::Tick)?,
Event::Render => action_tx.send(Action::Render)?,
Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
Event::Key(key) => self.handle_key_event(key)?,
_ => {}
}
for component in &mut self.components {
if let Some(action) = component.handle_events(Some(event.clone()))? {
action_tx.send(action)?;
}
}
Ok(())
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
let action_tx = self.action_tx.clone();
let Some(keymap) = self.config.keybindings.get(&self.cur_mode) else {
return Ok(());
};
if let Some(action) = keymap.get(&vec![key]) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
} else {
// If the key was not handled as a single key action,
// then consider it for multi-key combinations.
self.last_tick_key_events.push(key);
// Check for multi-key combinations
if let Some(action) = keymap.get(&self.last_tick_key_events) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
}
}
Ok(())
}
fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> {
while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render {
debug!("{action:?}");
}
match action {
Action::Tick => {
self.last_tick_key_events.drain(..);
// Check background task(s)
if let Some(task) = self.tasks.poll()? {
self.handle_task(&task)?;
}
}
Action::Quit => self.should_quit = true,
Action::Suspend => self.should_suspend = true,
Action::Resume => self.should_suspend = false,
Action::ClearScreen => tui.terminal.clear()?,
Action::KeyUp => self.list.previous(),
Action::KeyDown => self.list.next(),
Action::Error(ref msg) => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?;
self.action_tx.send(Action::SetMode(Mode::Failed))?;
}
Action::BootScan => self.action_tx.send(Action::SetMode(Mode::BootScan))?,
Action::DiagMainMenu => self.action_tx.send(Action::SetMode(Mode::DiagMenu))?,
Action::InstallDriver => {
self.action_tx.send(Action::SetMode(Mode::InstallDrivers))?;
}
Action::NextScreen => {
let next_mode = self.next_mode();
self.cur_mode = next_mode;
self.action_tx.send(Action::DismissPopup)?;
self.action_tx.send(Action::SetMode(next_mode))?;
}
Action::Process => match self.cur_mode {
Mode::BootDiags => {
self.action_tx.send(Action::SetMode(Mode::LogView))?;
}
Mode::DiagMenu => {
// Use highlighted entry
if let Some(new_mode) = self.list.get_selected() {
self.action_tx.send(Action::SetMode(new_mode))?;
}
}
Mode::Done => {
self.action_tx.send(Action::NextScreen)?;
}
_ => {}
},
Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?,
Action::ScanDisks => self.action_tx.send(Action::SetMode(Mode::ScanDisks))?,
Action::Select(one, two) => match self.cur_mode {
Mode::InjectDrivers => {
if let Some(index) = one {
self.inject_driver(index);
}
}
Mode::InstallDrivers => {
if let Some(index) = one
&& let Some(driver) = self.state.driver_list.get(index).cloned()
{
drivers::load(&driver.inf_paths);
self.state.driver = Some(driver);
}
}
Mode::BootSetup => {
if let Some(index) = one
&& let Some(boot_mode) = self.setup_modes.get(index)
{
info!("create_boot_files?");
create_boot_files(self, boot_mode.clone());
}
}
Mode::SelectDisks => {
self.state.disk_index_dest = one;
}
Mode::SelectParts => {
self.state.part_index_boot = one;
self.state.part_index_os = two;
}
Mode::SetBootMode => {
if let Some(index) = one
&& let Some(boot_mode) = self.boot_modes.get(index)
{
self.set_boot_mode(boot_mode.to_owned());
}
}
_ => {}
},
Action::SetMode(new_mode) => {
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.action_tx.send(build_right_items(self))?;
self.action_tx.send(Action::Select(None, None))?;
}
Action::TaskGroupStart(ref title) => {
// TODO: Verify this isn't broken and/or unused
if self.cur_mode == Mode::BootScan {
self.action_tx.send(Action::DiagLineStart {
text: title.clone(),
})?;
if let Ok(mut diag_groups) = self.diag_groups.lock() {
diag_groups.push(DiagGroup::new(get_diag_type(title)));
}
}
}
Action::TasksComplete => {
if self.cur_mode == Mode::BootDiags {
self.action_tx.send(Action::DismissPopup)?;
} else {
self.action_tx.send(Action::NextScreen)?;
}
}
_ => {}
}
for component in &mut self.components {
if let Some(action) = component.update(action.clone())? {
self.action_tx.send(action)?;
};
}
}
Ok(())
}
fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> {
tui.resize(Rect::new(0, 0, w, h))?;
self.render(tui)?;
Ok(())
}
fn handle_task(&mut self, task: &Task) -> Result<()> {
info!("Handling Task: {task:?}");
if self.cur_mode == Mode::BootScan {
if let Ok(mut diag_groups) = self.diag_groups.lock()
&& let Some(current_group) = diag_groups.last_mut()
{
match current_group.diag_type {
DiagType::Bitlocker => {
if let Some(task_result) = &task.result {
parse_bitlocker(current_group, task_result.clone());
}
}
DiagType::BootConfigData => {
if let Some(task_result) = &task.result {
parse_bcd(current_group, task_result.clone());
}
}
DiagType::CheckDisk => {
if let Some(task_result) = &task.result {
parse_chkdsk(current_group, task_result.clone());
}
}
DiagType::ComponentStore => {
if let Some(task_result) = &task.result {
parse_dism(current_group, task_result.clone());
}
}
DiagType::SystemFiles => {
if let Some(task_result) = &task.result {
parse_system_files(current_group, task_result.clone());
}
}
DiagType::Registry => {
if let Some(task_result) = &task.result {
match &task.task_type {
TaskType::CommandWait(_, cmd_args) => {
parse_registry_hives(
current_group,
task_result.clone(),
cmd_args.clone(),
);
}
_ => {}
}
}
}
DiagType::Unknown => {
panic!("This shouldn't happen?");
}
}
self.action_tx.send(Action::DiagLineUpdate {
result: current_group.get_pass_fail_warn(),
text: current_group.result.clone(),
})?;
}
return Ok(());
}
match task.task_type {
TaskType::CommandNoWait(_, _) | TaskType::CommandWait(_, _) | TaskType::Diskpart(_) => {
// Check result
if let Some(result) = &task.result {
match result {
TaskResult::Error(msg) => {
self.action_tx
.send(Action::Error(format!("{task:?} Failed: {msg}")))?;
}
TaskResult::Output(stdout, stderr, success) => {
if !success {
let msg = if !stdout.is_empty() {
stdout.clone()
} else if !stderr.is_empty() {
stderr.clone()
} else {
String::from("Unknown Error")
};
self.action_tx
.send(Action::Error(format!("{task:?} Failed: {msg}")))?;
}
}
}
}
}
TaskType::GroupEnd { ref label } => {
self.action_tx.send(Action::DiagLineEnd {
text: label.clone(),
})?;
}
_ => {}
}
Ok(())
}
fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
if let [header, _body, footer, left, right, popup, progress, results] =
get_chunks(frame.area())[..]
{
let component_areas = vec![
header, left, right, footer, popup, // core
progress, results, // boot-diags
];
for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {err:?}")));
}
}
};
})?;
Ok(())
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
// Cut the given rectangle into three vertical pieces
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
// Then cut the middle vertical piece into three width-wise pieces
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1] // Return the middle chunk
}
fn get_chunks(r: Rect) -> Vec<Rect> {
let mut chunks: Vec<Rect> = Vec::with_capacity(6);
// Main sections
chunks.extend(
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(3),
])
.split(r)
.to_vec(),
);
// Left/Right
chunks.extend(
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(centered_rect(90, 90, chunks[1]))
.to_vec(),
);
// Popup
chunks.push(centered_rect(60, 25, r));
// Progress
chunks.push(centered_rect(60, 70, r));
// Results
chunks.push(centered_rect(60, 70, r));
// Done
chunks
}
fn build_footer_string(cur_mode: Mode) -> String {
match cur_mode {
Mode::BootDiags | Mode::BootSetup | Mode::InjectDrivers | Mode::SetBootMode => {
String::from("(Enter) to select / (m) for menu / (s) to start over / (q) to quit")
}
Mode::BootScan | Mode::Home | Mode::Process | Mode::ScanDisks => {
String::from("(q) to quit")
}
Mode::DiagMenu | Mode::SelectParts => {
String::from("(Enter) to select / (s) to start over / (q) to quit")
}
Mode::Done => String::from("(Enter) to continue / (q) to quit"),
Mode::InstallDrivers => String::from("(Enter) to select / (q) to quit"),
Mode::LogView => {
String::from("(Enter | Esc) to close log / (up | down) to scroll / (q) to quit")
}
Mode::SelectDisks => String::from(
"(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit",
),
Mode::Failed => String::from("(Enter) or (q) to quit"),
// Invalid states
Mode::Confirm
| Mode::Clone
| Mode::PEMenu
| Mode::PreClone
| Mode::PostClone
| Mode::ScanWinSources
| Mode::SelectTableType
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetUserName => {
panic!("This shouldn't happen?")
}
}
}
fn build_left_items(app: &App) -> Action {
let mut items = Vec::new();
let mut labels = vec![String::new(), String::new()];
let select_type: SelectionType;
let title: String;
match app.cur_mode {
Mode::Home => {
select_type = SelectionType::Loop;
title = String::from("Home");
}
Mode::DiagMenu => {
select_type = SelectionType::Loop;
title = String::from("Troubleshooting");
app.list.items.iter().for_each(|mode| {
let (name, _) = get_mode_strings(*mode);
items.push(name);
});
}
Mode::InstallDrivers => {
select_type = SelectionType::One;
title = String::from("Install Drivers");
app.state
.driver_list
.iter()
.for_each(|driver| items.push(driver.to_string()));
}
Mode::InjectDrivers => {
select_type = SelectionType::One;
title = String::from("Select Drivers");
app.state
.driver_list
.iter()
.for_each(|driver| items.push(driver.to_string()));
}
Mode::SelectDisks => {
select_type = SelectionType::One;
title = String::from("Select Disk");
let disk_list = app.state.disk_list.lock().unwrap();
disk_list
.iter()
.for_each(|disk| items.push(disk.description.to_string()));
}
Mode::BootScan | Mode::Process | Mode::ScanDisks => {
select_type = SelectionType::Loop;
title = String::from("Processing");
}
Mode::SelectParts => {
select_type = SelectionType::Two;
title = String::from("Select Boot and OS Partitions");
labels[0] = String::from("boot");
labels[1] = String::from("os");
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::BootDiags | Mode::LogView => {
select_type = SelectionType::Loop;
let (new_title, _) = get_mode_strings(app.cur_mode);
title = new_title;
if let Ok(diag_groups) = app.diag_groups.lock() {
let labels: Vec<String> = diag_groups
.iter()
.map(|group| {
let label = group.diag_type.to_string();
let status = if group.get_pass_fail_warn() == DiagResult::Pass {
"" // Leave blank if OK
} else {
" -- Issue(s) detected"
};
format!("{label}{status}")
})
.collect();
items.extend(labels);
}
}
Mode::BootSetup => {
select_type = SelectionType::One;
let (new_title, _) = get_mode_strings(app.cur_mode);
title = new_title;
app.boot_modes.iter().rev().for_each(|entry| match entry {
SafeMode::Disable => items.push(String::from("Normal Mode")),
SafeMode::Enable => items.push(String::from("Locked in Safe Mode")),
});
}
Mode::SetBootMode => {
select_type = SelectionType::One;
let (new_title, _) = get_mode_strings(app.cur_mode);
title = new_title;
app.boot_modes.iter().for_each(|entry| {
items.push(format!("{:?} Safe Mode", entry));
});
}
Mode::Done | Mode::Failed => {
select_type = SelectionType::Loop;
title = String::from("Done");
}
// Invalid states
Mode::SelectTableType
| Mode::PEMenu
| Mode::Confirm
| Mode::PreClone
| Mode::Clone
| Mode::PostClone
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetUserName => {
panic!("This shouldn't happen?")
}
};
Action::UpdateLeft(title, labels, items, select_type)
}
fn build_right_items(app: &App) -> Action {
let mut items: Vec<Vec<DVLine>> = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0;
let disk_header = get_disk_header(app);
match app.cur_mode {
Mode::BootDiags
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::InstallDrivers
| Mode::Process
| Mode::SetBootMode => {
items.push(vec![DVLine {
line_parts: vec![String::from("CPU")],
line_colors: vec![Color::Cyan],
}]);
items.push(vec![DVLine {
line_parts: vec![get_cpu_name()],
line_colors: vec![Color::Reset],
}]);
items.push(vec![DVLine {
line_parts: vec![String::new()],
line_colors: vec![Color::Reset],
}]);
start_index += 3;
if !disk_header.is_empty() {
items.push(disk_header);
start_index += 1;
}
}
_ => {}
}
match app.cur_mode {
Mode::DiagMenu => {
app.list.items.iter().for_each(|mode| {
let (name, description) = get_mode_strings(*mode);
items.push(vec![
DVLine {
line_parts: vec![name],
line_colors: vec![Color::Cyan],
},
DVLine {
line_parts: vec![String::new()],
line_colors: vec![Color::Reset],
},
DVLine {
line_parts: vec![description],
line_colors: vec![Color::Reset],
},
]);
});
}
Mode::BootDiags => {
if let Ok(diag_groups) = app.diag_groups.lock() {
let mut summary: Vec<DVLine> = Vec::new();
diag_groups
.iter()
.for_each(|group| summary.extend(group.get_logs_summary()));
items.push(summary);
}
}
Mode::BootSetup => {
items.push(vec![DVLine {
line_parts: vec![String::from("Normal Mode")],
line_colors: vec![Color::Reset],
}]);
items.push(vec![DVLine {
line_parts: vec![String::from("Safe Mode")],
line_colors: vec![Color::Reset],
}]);
}
Mode::SelectDisks => {
let dest_dv_line = DVLine {
line_parts: vec![String::from("Disk")],
line_colors: vec![Color::Cyan],
};
labels.push(vec![dest_dv_line]);
let disk_list = app.state.disk_list.lock().unwrap();
disk_list
.iter()
.for_each(|disk| items.push(get_disk_description_right(disk, &None)));
}
Mode::SelectParts => {
["Boot", "OS"].iter().for_each(|s| {
labels.push(vec![DVLine {
line_parts: vec![String::from(*s)],
line_colors: vec![Color::Cyan],
}])
});
if let Some(index) = app.state.disk_index_dest {
start_index += 1;
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));
// Partition Details
disk.parts
.iter()
.for_each(|part| items.push(get_part_description(part)));
}
}
}
Mode::SetBootMode => {
items.push(vec![DVLine {
line_parts: vec![String::from("Enable Safe Mode (minimal)")],
line_colors: vec![Color::Reset],
}]);
items.push(vec![DVLine {
line_parts: vec![String::from("Disable Safe Mode")],
line_colors: vec![Color::Reset],
}]);
}
_ => {}
}
Action::UpdateRight(labels, start_index, items)
}
fn create_boot_files(app: &mut App, safe_mode: SafeMode) {
let mut tasks: Vec<TaskType> = Vec::new();
if let Some(index) = app.state.disk_index_dest {
let disk_list = app.state.disk_list.lock().unwrap();
if let Some(disk) = disk_list.get(index) {
let letter_boot = disk.get_part_letter(app.state.part_index_boot.unwrap());
let letter_os = disk.get_part_letter(app.state.part_index_os.unwrap());
tasks = configure_disk(
&letter_boot,
&letter_os,
safe_mode,
&app.system32,
&disk.part_type,
);
}
}
tasks
.into_iter()
.for_each(|task_type| app.tasks.add(task_type));
}
fn get_disk_header(app: &App) -> Vec<DVLine> {
let mut header_lines: Vec<DVLine> = Vec::new();
if let Some(index) = app.state.disk_index_dest {
let disk_list = app.state.disk_list.lock().unwrap();
if let Some(disk) = disk_list.get(index) {
let mut parts: Vec<usize> = Vec::new();
if let Some(index) = app.state.part_index_boot {
parts.push(index);
}
if let Some(index) = app.state.part_index_os {
parts.push(index);
}
header_lines.append(&mut get_disk_description_right(disk, &Some(parts)));
}
}
header_lines
}
fn get_mode_strings(mode: Mode) -> (String, String) {
match mode {
Mode::BootScan | Mode::BootDiags | Mode::LogView => (
String::from("Boot Diagnostics"),
String::from("Check for common Windows boot issues"),
),
Mode::BootSetup => (
String::from("Boot Setup"),
String::from("Create or recreate boot files"),
),
Mode::InjectDrivers => (
String::from("Inject Drivers"),
String::from("Inject drivers into existing Windows environment"),
),
Mode::SetBootMode => (
String::from("Toggle Safe Mode"),
String::from("Enable or disable safe mode"),
),
_ => panic!("This shouldn't happen"),
}
}

View file

@ -1,17 +0,0 @@
// This file is part of Deja-Vu.
//
// Deja-Vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-Vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
pub mod logview;
pub mod progress;

View file

@ -1,168 +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 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

@ -1,172 +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 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

@ -1,526 +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 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,
CheckDisk,
ComponentStore,
Registry,
SystemFiles,
Unknown,
}
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 Log {
pub label: String,
pub summary: Vec<DVLine>,
pub raw: String,
}
#[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_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")
}
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
}
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
}
}
}
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

@ -1,50 +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 clap::Parser;
use color_eyre::Result;
use crate::app::App;
mod app;
mod components;
mod diags;
mod scan;
mod state;
#[tokio::main]
async fn main() -> Result<()> {
let mut msg = None;
if cfg!(windows) {
use check_elevation::is_elevated;
if !is_elevated().expect("Failed to get elevation status.") {
msg.replace("Administrator privedges required for Deja-Vu.");
}
};
match msg {
Some(text) => {
println!("{text}");
}
None => {
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(())
}

View file

@ -1,184 +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 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(())
}

View file

@ -1,42 +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::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

@ -1,28 +1,21 @@
{ {
"app_title": "Deja-Vu", "app_title": "Deja-vu: Clone Tool",
"clone_app_path": "C:/Program Files/Some Clone Tool/app.exe", "clone_app_path": "C:/Program Files/Some Clone Tool/app.exe",
"conemu_path": "C:/Program Files/ConEmu/ConEmu64.exe",
"keybindings": { "keybindings": {
"Home": {
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"ScanDisks": { "ScanDisks": {
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"InstallDrivers": { "InstallDrivers": {
"<Enter>": "Process", "<Enter>": "Process",
"<Up>": "KeyUp", "<Up>": "KeyUp",
"<Down>": "KeyDown", "<Down>": "KeyDown",
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"SelectDisks": { "SelectDisks": {
"<i>": "InstallDriver", "<i>": "InstallDriver",
@ -30,202 +23,70 @@
"<Enter>": "Process", "<Enter>": "Process",
"<Up>": "KeyUp", "<Up>": "KeyUp",
"<Down>": "KeyDown", "<Down>": "KeyDown",
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"SelectTableType": { "SelectTableType": {
"<b>": "PrevScreen", "<b>": "PrevScreen",
"<Enter>": "Process", "<Enter>": "Process",
"<Up>": "KeyUp", "<Up>": "KeyUp",
"<Down>": "KeyDown", "<Down>": "KeyDown",
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"Confirm": { "Confirm": {
"<b>": "PrevScreen", "<b>": "PrevScreen",
"<Enter>": "Process", "<Enter>": "Process",
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"PreClone": { "PreClone": {
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"Clone": { "Clone": {
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"SelectParts": { "SelectParts": {
"<Enter>": "Process", "<Enter>": "Process",
"<Up>": "KeyUp", "<Up>": "KeyUp",
"<Down>": "KeyDown", "<Down>": "KeyDown",
"<s>": "ScanDisks", "<s>": "ScanDisks", // Start over
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"PostClone": { "PostClone": {
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"Done": { "Done": {
"<Enter>": "Process", "<Enter>": "Quit",
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"Failed": { "Failed": {
"<Enter>": "Quit", "<Enter>": "Quit",
"<q>": "Quit", "<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit", // Yet another way to quit
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend" // Suspend the application
}, },
"DiagMenu": { }
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"BootDiags": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<m>": "DiagMainMenu",
"<r>": "BootScan",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"BootScan": {
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"BootSetup": {
"<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",
"<Ctrl-z>": "Suspend"
},
"InjectDrivers": {
"<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",
"<Ctrl-z>": "Suspend"
},
"SetBootMode": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<m>": "DiagMainMenu",
"<s>": "ScanDisks",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"PEMenu": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<q>": "Quit",
"<r>": "Restart",
"<p>": "Shutdown",
"<t>": "OpenTerminal",
"<Ctrl-d>": "Quit",
"<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"
} }

View file

@ -1,83 +0,0 @@
<?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

@ -1,22 +1,22 @@
# This file is part of Deja-Vu. # This file is part of Deja-vu.
# #
# Deja-Vu is free software: you can redistribute it and/or modify it # 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 # under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Deja-Vu is distributed in the hope that it will be useful, but # Deja-vu is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of # WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details. # See the GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. # along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
[package] [package]
name = "core" name = "core"
authors = ["2Shirt <2xShirt@gmail.com>"] authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024" edition = "2021"
license = "GPL" license = "GPL"
version = "0.2.0" version = "0.2.0"
@ -40,8 +40,8 @@ human-panic = "2.0.1"
json5 = "0.4.1" json5 = "0.4.1"
lazy_static = "1.5.0" lazy_static = "1.5.0"
libc = "0.2.158" libc = "0.2.158"
once_cell = "1.20.2"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
rand = "0.9.0"
ratatui = { version = "0.29.0", features = ["serde", "macros"] } ratatui = { version = "0.29.0", features = ["serde", "macros"] }
raw-cpuid = "11.2.0" raw-cpuid = "11.2.0"
regex = "1.11.1" regex = "1.11.1"

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use anyhow::Result; use anyhow::Result;
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder}; use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};

View file

@ -1,67 +1,39 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
use crate::{ use crate::{components::popup::Type, line::DVLine, state::Mode, system::disk::Disk};
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)] #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum Action { pub enum Action {
// App (Boot-Diags) // App
BootScan,
DiagLineStart { text: String },
DiagLineUpdate { result: DiagResult, text: String },
DiagLineEnd { text: String },
DiagMainMenu,
// App (Clone)
Highlight(usize), Highlight(usize),
InstallDriver, InstallDriver,
Process, Process,
ScanDisks, ScanDisks,
Select(Option<usize>, Option<usize>), // indicies for (source, dest) etc Select(Option<usize>, Option<usize>), // indicies for (source, dest) etc
SelectRight(Option<usize>, Option<usize>), // indicies for right info pane SelectRight(Option<usize>, Option<usize>), // indicies for right info pane
TaskGroupStart(String),
TasksComplete,
UpdateDiskList(Vec<Disk>), UpdateDiskList(Vec<Disk>),
UpdateFooter(String), UpdateFooter(String),
UpdateLeft(String, Vec<String>, Vec<String>, SelectionType), // (title, labels, items, select_type) UpdateLeft(String, Vec<String>, Vec<String>, bool), // (title, labels, items, select_one)
UpdateRight(Vec<Vec<DVLine>>, usize, Vec<Vec<DVLine>>), // (labels, start_index, items) - items before start are always shown 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 // Screens
DismissPopup, DismissPopup,
DisplayPopup(PopupType, String), DisplayPopup(Type, String),
NextScreen, NextScreen,
PrevScreen, PrevScreen,
SetMode(Mode), SetMode(Mode),
@ -71,12 +43,6 @@ pub enum Action {
Help, Help,
KeyDown, KeyDown,
KeyUp, KeyUp,
KeyLeft,
KeyRight,
KeyPageUp,
KeyPageDown,
KeyHome,
KeyEnd,
Quit, Quit,
Render, Render,
Resize(u16, u16), Resize(u16, u16),

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use clap::Parser; use clap::Parser;
@ -38,7 +38,6 @@ const VERSION_MESSAGE: &str = concat!(
")" ")"
); );
#[must_use]
pub fn version() -> String { pub fn version() -> String {
let author = clap::crate_authors!(); let author = clap::crate_authors!();

View file

@ -1,30 +1,30 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::missing_errors_doc)] //
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::{KeyEvent, MouseEvent}; use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::{ use ratatui::{
Frame,
layout::{Rect, Size}, layout::{Rect, Size},
Frame,
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::{action::Action, config::Config, tui::Event}; use crate::{action::Action, config::Config, tui::Event};
pub mod footer; pub mod footer;
pub mod fps;
pub mod left; pub mod left;
pub mod popup; pub mod popup;
pub mod right; pub mod right;

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use color_eyre::Result; use color_eyre::Result;
use ratatui::{ use ratatui::{
@ -31,7 +31,6 @@ pub struct Footer {
} }
impl Footer { impl Footer {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
text: String::from("(q) to quit"), text: String::from("(q) to quit"),
@ -52,7 +51,6 @@ impl Component for Footer {
} }
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
#[allow(clippy::single_match)]
match action { match action {
Action::UpdateFooter(text) => self.text = text, Action::UpdateFooter(text) => self.text = text,
_ => {} _ => {}

113
core/src/components/fps.rs Normal file
View file

@ -0,0 +1,113 @@
// 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

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
@ -19,19 +19,10 @@ use ratatui::{
prelude::*, prelude::*,
widgets::{Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph}, widgets::{Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph},
}; };
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::{Component, state::StatefulList}; use super::{state::StatefulList, Component};
use crate::{action::Action, config::Config, state::Mode}; use crate::{action::Action, config::Config};
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum SelectionType {
#[default]
Loop,
One,
Two,
}
#[derive(Default)] #[derive(Default)]
pub struct Left { pub struct Left {
@ -39,19 +30,17 @@ pub struct Left {
config: Config, config: Config,
labels: Vec<String>, labels: Vec<String>,
list: StatefulList<String>, list: StatefulList<String>,
mode: Mode, select_one: bool,
select_type: SelectionType,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>, selections_saved: Vec<Option<usize>>,
title_text: String, title_text: String,
} }
impl Left { impl Left {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
select_one: false,
labels: vec![String::from("one"), String::from("two")], labels: vec![String::from("one"), String::from("two")],
select_type: SelectionType::Loop,
selections: vec![None, None], selections: vec![None, None],
selections_saved: vec![None, None], selections_saved: vec![None, None],
title_text: String::from("Home"), title_text: String::from("Home"),
@ -83,29 +72,15 @@ impl Component for Left {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::Highlight(index) => self.set_highlight(index), Action::Highlight(index) => self.set_highlight(index),
Action::KeyUp => { Action::KeyUp => self.list.previous(),
if self.mode != Mode::LogView { Action::KeyDown => self.list.next(),
self.list.previous();
}
}
Action::KeyDown => {
if self.mode != Mode::LogView {
self.list.next();
}
}
Action::Process => { Action::Process => {
if self.mode == Mode::LogView { if let Some(command_tx) = self.command_tx.clone() {
// 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]) { match (self.selections[0], self.selections[1]) {
(None, None) => { (None, None) => {
// Making first selection // Making first selection
command_tx.send(Action::Select(self.list.selected(), None))?; command_tx.send(Action::Select(self.list.selected(), None))?;
if self.select_type == SelectionType::One { if self.select_one {
// Confirm selection // Confirm selection
command_tx.send(Action::NextScreen)?; command_tx.send(Action::NextScreen)?;
} }
@ -136,19 +111,18 @@ impl Component for Left {
self.selections_saved[0] = one; self.selections_saved[0] = one;
self.selections_saved[1] = two; self.selections_saved[1] = two;
} }
Action::SetMode(new_mode) => { Action::SetMode(_) => {
self.mode = new_mode;
self.selections[0] = None; self.selections[0] = None;
self.selections[1] = None; self.selections[1] = None;
} }
Action::UpdateLeft(title, labels, items, select_type) => { Action::UpdateLeft(title, labels, items, select_one) => {
self.title_text = title; self.title_text = title;
self.labels = labels self.labels = labels
.iter() .iter()
.map(|label| format!(" ~{}~", label.to_lowercase())) .map(|label| format!(" ~{}~", label.to_lowercase()))
.collect(); .collect();
self.list.set_items(items); self.list.set_items(items);
self.select_type = select_type; self.select_one = select_one;
} }
_ => {} _ => {}
} }
@ -162,9 +136,13 @@ impl Component for Left {
.areas(area); .areas(area);
// Title // Title
let title = Paragraph::new( let title_text = if self.selections[1].is_some() || self.select_one {
Line::from(Span::styled(self.title_text.as_str(), Style::default())).centered(), "Confirm Selections"
) } else {
self.title_text.as_str()
};
let title =
Paragraph::new(Line::from(Span::styled(title_text, Style::default())).centered())
.block(Block::default().borders(Borders::NONE)); .block(Block::default().borders(Borders::NONE));
frame.render_widget(title, title_area); frame.render_widget(title, title_area);
@ -191,7 +169,7 @@ impl Component for Left {
.map(|(index, item)| { .map(|(index, item)| {
let mut style = Style::default(); let mut style = Style::default();
let text = if self.selections[0].is_some_and(|first_index| first_index == index) { let text = if self.selections[0].is_some_and(|first_index| first_index == index) {
if let Some(label) = self.labels.first() { if let Some(label) = self.labels.get(0) {
style = style.yellow(); style = style.yellow();
label.as_str() label.as_str()
} else { } else {

View file

@ -1,20 +1,19 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use color_eyre::Result; use color_eyre::Result;
use rand::random;
use ratatui::{ use ratatui::{
prelude::*, prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Wrap}, widgets::{Block, Borders, Clear, Paragraph, Wrap},
@ -22,7 +21,6 @@ use ratatui::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tracing::info;
use super::Component; use super::Component;
use crate::{action::Action, config::Config}; use crate::{action::Action, config::Config};
@ -44,9 +42,11 @@ pub struct Popup {
} }
impl Popup { impl Popup {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self {
popup_text: String::from("Scanning Disks..."),
..Default::default()
}
} }
} }
@ -65,9 +65,8 @@ impl Component for Popup {
match action { match action {
Action::DismissPopup => self.popup_text.clear(), Action::DismissPopup => self.popup_text.clear(),
Action::DisplayPopup(new_type, new_text) => { Action::DisplayPopup(new_type, new_text) => {
info!("Show Popup ({new_type}): {new_text}");
self.popup_type = new_type; self.popup_type = new_type;
self.popup_text = format!("\n{new_text}"); self.popup_text = new_text;
} }
_ => {} _ => {}
} }
@ -99,21 +98,3 @@ impl Component for Popup {
Ok(()) Ok(())
} }
} }
#[must_use]
pub fn fortune() -> String {
String::from(match random::<u8>() / 4 {
0 => "FUN FACT\n\n\nComputers barely work.",
1 => "CRASH OVERRIDE\n\n\n\"Hack the planet!\"",
2 => "CATS\n\n\n\"All your base are belong to us!\"",
3 => "HMM\n\n\nThis has all happened before...\n\nThis will all happen again.",
4 => "CYPHER\n\n\n\"I dont even see the code. All I see is blonde, brunette, red-head.\"",
5 => "CONGRATULATIONS\n\n\nYour did it!",
6 => "DID YOU KNOW?\n\n\nmacOS includes a built-in screen reader!",
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

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
@ -21,8 +21,8 @@ use ratatui::{
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::{Component, state::StatefulList}; use super::{state::StatefulList, Component};
use crate::{action::Action, config::Config, line::DVLine, state::Mode}; use crate::{action::Action, config::Config, line::DVLine};
#[derive(Default)] #[derive(Default)]
pub struct Right { pub struct Right {
@ -31,14 +31,12 @@ pub struct Right {
list_header: Vec<DVLine>, list_header: Vec<DVLine>,
list_labels: Vec<Vec<DVLine>>, list_labels: Vec<Vec<DVLine>>,
list: StatefulList<Vec<DVLine>>, list: StatefulList<Vec<DVLine>>,
mode: Mode,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>, selections_saved: Vec<Option<usize>>,
title: String, title: String,
} }
impl Right { impl Right {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
selections: vec![None, None], selections: vec![None, None],
@ -94,16 +92,8 @@ impl Component for Right {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::Highlight(index) => self.set_highlight(index), Action::Highlight(index) => self.set_highlight(index),
Action::KeyUp => { Action::KeyUp => self.list.previous(),
if self.mode != Mode::LogView { Action::KeyDown => self.list.next(),
self.list.previous();
}
}
Action::KeyDown => {
if self.mode != Mode::LogView {
self.list.next();
}
}
Action::Select(one, two) => { Action::Select(one, two) => {
self.selections[0] = one; self.selections[0] = one;
self.selections[1] = two; self.selections[1] = two;
@ -118,8 +108,7 @@ impl Component for Right {
self.selections_saved[0] = one; self.selections_saved[0] = one;
self.selections_saved[1] = two; self.selections_saved[1] = two;
} }
Action::SetMode(new_mode) => { Action::SetMode(_) => {
self.mode = new_mode;
self.selections[0] = None; self.selections[0] = None;
self.selections[1] = None; self.selections[1] = None;
self.selections_saved[0] = None; self.selections_saved[0] = None;
@ -159,24 +148,23 @@ impl Component for Right {
} }
// First selection // First selection
if let Some(first_index) = self.get_first() if let Some(first_index) = self.get_first() {
&& let Some(first_desc) = self.list.get(first_index) if let Some(first_desc) = self.list.get(first_index) {
{ if let Some(label) = self.list_labels.get(0) {
if let Some(label) = self.list_labels.first() {
label label
.iter() .iter()
.for_each(|dv| body_text.push(dv.as_line().bold())); .for_each(|dv| body_text.push(dv.as_line().bold()));
body_text.push(Line::from(""));
} }
body_text.push(Line::from(""));
first_desc first_desc
.iter() .iter()
.for_each(|dv| body_text.push(dv.as_line())); .for_each(|dv| body_text.push(dv.as_line()));
} }
}
// Second selection // Second selection
if let Some(second_index) = self.get_second() if let Some(second_index) = self.get_second() {
&& let Some(second_desc) = self.list.get(second_index) if let Some(second_desc) = self.list.get(second_index) {
{
// Divider // Divider
body_text.push(Line::from("")); body_text.push(Line::from(""));
body_text.push(Line::from(str::repeat("", (body_area.width - 4) as usize))); body_text.push(Line::from(str::repeat("", (body_area.width - 4) as usize)));
@ -191,6 +179,7 @@ impl Component for Right {
.iter() .iter()
.for_each(|dv| body_text.push(dv.as_line())); .for_each(|dv| body_text.push(dv.as_line()));
} }
}
// Build Paragraph // Build Paragraph
let body = Paragraph::new(body_text) let body = Paragraph::new(body_text)

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use std::collections::HashMap; use std::collections::HashMap;
@ -31,17 +31,14 @@ impl<T: Clone> StatefulList<T> {
self.items.clear(); self.items.clear();
} }
#[must_use]
pub fn get(&self, index: usize) -> Option<&T> { pub fn get(&self, index: usize) -> Option<&T> {
self.items.get(index) self.items.get(index)
} }
#[must_use]
pub fn get_selected(&self) -> Option<T> { pub fn get_selected(&self) -> Option<T> {
self.state.selected().map(|i| self.items[i].clone()) self.state.selected().map(|i| self.items[i].clone())
} }
#[must_use]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.items.is_empty() self.items.is_empty()
} }
@ -54,12 +51,11 @@ impl<T: Clone> StatefulList<T> {
} }
} }
#[must_use]
pub fn selected(&self) -> Option<usize> { pub fn selected(&self) -> Option<usize> {
self.state.selected() self.state.selected()
} }
pub fn select_first_item(&mut self) { fn select_first_item(&mut self) {
if self.items.is_empty() { if self.items.is_empty() {
self.state.select(None); self.state.select(None);
} else { } else {

View file

@ -1,140 +1,67 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use color_eyre::Result; use color_eyre::Result;
use ratatui::{ use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
prelude::*, prelude::*,
style::{Style, Stylize},
text::Span,
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
use std::time::Instant; use tokio::sync::mpsc::UnboundedSender;
use super::Component; use super::Component;
use crate::action::Action; use crate::{action::Action, config::Config};
#[derive(Debug, Clone, PartialEq)] #[derive(Default)]
pub struct Title { pub struct Title {
mode: String, command_tx: Option<UnboundedSender<Action>>,
title: String, config: Config,
last_tick_update: Instant,
tick_count: u32,
ticks_per_second: f64,
last_frame_update: Instant,
frame_count: u32,
frames_per_second: f64,
} }
impl Title { impl Title {
#[must_use] pub fn new() -> Self {
pub fn new(title: &str) -> Self { Self::default()
Self {
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 { impl Component for Title {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
#[allow(clippy::match_single_binding)] self.command_tx = Some(tx);
match action { Ok(())
Action::SetMode(mode) => {
self.mode = format!("{mode:?}");
} }
Action::Render => self.render_tick()?,
Action::Tick => self.app_tick()?, fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
_ => {} _ => {}
} }
Ok(None) Ok(None)
} }
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { 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 // Title Block
let title_text = Span::styled( let title_text = Span::styled(
format!("Deja-Vu: {}", self.title), self.config.app_title.as_str(),
Style::default().fg(Color::LightCyan), Style::default().fg(Color::LightCyan),
); );
let title = Paragraph::new(Line::from(title_text).centered()) let title = Paragraph::new(Line::from(title_text).centered())
.block(Block::default().borders(Borders::ALL)); .block(Block::default().borders(Borders::ALL));
frame.render_widget(title, area); 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(()) Ok(())
} }
} }

View file

@ -1,20 +1,18 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // 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 #![allow(dead_code)] // Remove this once you start using the code
use std::{collections::HashMap, env, path::PathBuf}; use std::{collections::HashMap, env, path::PathBuf};
@ -25,7 +23,7 @@ use derive_deref::{Deref, DerefMut};
use directories::ProjectDirs; use directories::ProjectDirs;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use serde::{Deserialize, de::Deserializer}; use serde::{de::Deserializer, Deserialize};
use tracing::error; use tracing::error;
use crate::{action::Action, state::Mode}; use crate::{action::Action, state::Mode};
@ -46,33 +44,23 @@ pub struct Config {
pub app_title: String, pub app_title: String,
#[serde(default)] #[serde(default)]
pub clone_app_path: PathBuf, pub clone_app_path: PathBuf,
#[serde(default)]
pub conemu_path: PathBuf,
#[serde(default, flatten)] #[serde(default, flatten)]
pub config: AppConfig, pub config: AppConfig,
#[serde(default)] #[serde(default)]
pub keybindings: KeyBindings, pub keybindings: KeyBindings,
#[serde(default)] #[serde(default)]
pub styles: Styles, 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: &str = "DEJA-VU"; pub static PROJECT_NAME: &'static str = "DEJA-VU";
lazy_static! { lazy_static! {
//pub static ref PROJECT_NAME: String = env!("CARGO_PKG_NAME").to_uppercase().to_string(); //pub static ref PROJECT_NAME: String = env!("CARGO_PKG_NAME").to_uppercase().to_string();
pub static ref DATA_FOLDER: Option<PathBuf> = pub static ref DATA_FOLDER: Option<PathBuf> =
env::var(format!("{PROJECT_NAME}_DATA")) env::var(format!("{}_DATA", PROJECT_NAME))
.ok() .ok()
.map(PathBuf::from); .map(PathBuf::from);
pub static ref CONFIG_FOLDER: Option<PathBuf> = pub static ref CONFIG_FOLDER: Option<PathBuf> =
env::var(format!("{PROJECT_NAME}_CONFIG")) env::var(format!("{}_CONFIG", PROJECT_NAME))
.ok() .ok()
.map(PathBuf::from); .map(PathBuf::from);
} }
@ -84,14 +72,12 @@ impl Config {
let config_dir = get_config_dir(); let config_dir = get_config_dir();
let mut builder = config::Config::builder() let mut builder = config::Config::builder()
.set_default("app_title", default_config.app_title.as_str())? .set_default("app_title", default_config.app_title.as_str())?
.set_default("clone_app_path", default_config.app_title.as_str())? .set_default(
.set_default("conemu_path", default_config.app_title.as_str())? "clone_app_path",
String::from("C:\\Program Files\\Some Clone Tool\\app.exe"),
)?
.set_default("config_dir", config_dir.to_str().unwrap())? .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 = [ let config_files = [
("config.json5", config::FileFormat::Json5), ("config.json5", config::FileFormat::Json5),
@ -134,31 +120,31 @@ impl Config {
} }
} }
#[must_use]
pub fn get_data_dir() -> PathBuf { pub fn get_data_dir() -> PathBuf {
if let Some(s) = DATA_FOLDER.clone() { let directory = if let Some(s) = DATA_FOLDER.clone() {
s s
} else if let Some(proj_dirs) = project_directory() { } else if let Some(proj_dirs) = project_directory() {
proj_dirs.data_local_dir().to_path_buf() proj_dirs.data_local_dir().to_path_buf()
} else { } else {
PathBuf::from(".").join(".data") PathBuf::from(".").join(".data")
} };
directory
} }
#[must_use]
pub fn get_config_dir() -> PathBuf { pub fn get_config_dir() -> PathBuf {
if let Some(s) = CONFIG_FOLDER.clone() { let directory = if let Some(s) = CONFIG_FOLDER.clone() {
s s
} else if let Some(proj_dirs) = project_directory() { } else if let Some(proj_dirs) = project_directory() {
proj_dirs.config_local_dir().to_path_buf() proj_dirs.config_local_dir().to_path_buf()
} else { } else {
PathBuf::from(".").join(".config") PathBuf::from(".").join(".config")
} };
directory
} }
fn project_directory() -> Option<ProjectDirs> { fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("com", "Deja-Vu", "deja-vu") ProjectDirs::from("com", "Deja-vu", "deja-vu")
//ProjectDirs::from("com", "Deja-Vu", env!("CARGO_PKG_NAME")) //ProjectDirs::from("com", "Deja-vu", env!("CARGO_PKG_NAME"))
} }
#[derive(Clone, Debug, Default, Deref, DerefMut)] #[derive(Clone, Debug, Default, Deref, DerefMut)]
@ -266,7 +252,6 @@ fn parse_key_code_with_modifiers(
Ok(KeyEvent::new(c, modifiers)) Ok(KeyEvent::new(c, modifiers))
} }
#[must_use]
pub fn key_event_to_string(key_event: &KeyEvent) -> String { pub fn key_event_to_string(key_event: &KeyEvent) -> String {
let char; let char;
let key_code = match key_event.code { let key_code = match key_event.code {
@ -337,8 +322,8 @@ pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
let raw = if raw.contains("><") { let raw = if raw.contains("><") {
raw raw
} else { } else {
let mut raw = raw.strip_prefix('<').unwrap_or(raw); let raw = raw.strip_prefix('<').unwrap_or(raw);
raw = raw.strip_prefix('>').unwrap_or(raw); let raw = raw.strip_prefix('>').unwrap_or(raw);
raw raw
}; };
let sequences = raw let sequences = raw
@ -382,7 +367,6 @@ impl<'de> Deserialize<'de> for Styles {
} }
} }
#[must_use]
pub fn parse_style(line: &str) -> Style { pub fn parse_style(line: &str) -> Style {
let (foreground, background) = let (foreground, background) =
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len())); line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
@ -445,11 +429,8 @@ fn parse_color(s: &str) -> Option<Color> {
.unwrap_or_default(); .unwrap_or_default();
Some(Color::Indexed(c)) Some(Color::Indexed(c))
} else if s.contains("rgb") { } 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; 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; 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 blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
let c = 16 + red * 36 + green * 6 + blue; let c = 16 + red * 36 + green * 6 + blue;
Some(Color::Indexed(c)) Some(Color::Indexed(c))
@ -548,7 +529,7 @@ mod tests {
let c = Config::new()?; let c = Config::new()?;
assert_eq!( assert_eq!(
c.keybindings c.keybindings
.get(&Mode::Home) // i.e. Home .get(&Mode::ScanDisks) // i.e. Home
.unwrap() .unwrap()
.get(&parse_key_sequence("<q>").unwrap_or_default()) .get(&parse_key_sequence("<q>").unwrap_or_default())
.unwrap(), .unwrap(),

View file

@ -1,19 +1,18 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::missing_errors_doc)] //
use std::env; use std::env;
use color_eyre::Result; use color_eyre::Result;
@ -31,11 +30,11 @@ pub fn init() -> Result<()> {
.into_hooks(); .into_hooks();
eyre_hook.install()?; eyre_hook.install()?;
std::panic::set_hook(Box::new(move |panic_info| { std::panic::set_hook(Box::new(move |panic_info| {
if let Ok(mut t) = crate::tui::Tui::new() if let Ok(mut t) = crate::tui::Tui::new() {
&& let Err(r) = t.exit() if let Err(r) = t.exit() {
{
error!("Unable to exit Terminal: {:?}", r); error!("Unable to exit Terminal: {:?}", r);
} }
}
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
{ {

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
pub mod action; pub mod action;
pub mod cli; pub mod cli;
@ -23,5 +23,4 @@ pub mod logging;
pub mod state; pub mod state;
pub mod system; pub mod system;
pub mod tasks; pub mod tasks;
pub mod tests;
pub mod tui; pub mod tui;

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use ratatui::{ use ratatui::{
style::{Color, Style}, style::{Color, Style},
@ -30,15 +30,13 @@ pub struct DVLine {
impl DVLine { impl DVLine {
/// Convert to Line with colored span(s) /// Convert to Line with colored span(s)
#[must_use] pub fn as_line(&self) -> Line {
pub fn as_line(&self) -> Line<'_> {
let mut spans = Vec::new(); let mut spans = Vec::new();
zip(self.line_parts.clone(), self.line_colors.clone()) zip(self.line_parts.clone(), self.line_colors.clone())
.for_each(|(part, color)| spans.push(Span::styled(part, Style::default().fg(color)))); .for_each(|(part, color)| spans.push(Span::styled(part, Style::default().fg(color))));
Line::from(spans) Line::from(spans)
} }
#[must_use]
pub fn blank() -> Self { pub fn blank() -> Self {
Self { Self {
line_parts: vec![String::new()], line_parts: vec![String::new()],
@ -47,11 +45,7 @@ impl DVLine {
} }
} }
#[must_use] pub fn get_disk_description_right(disk: &Disk) -> Vec<DVLine> {
pub fn get_disk_description_right(
disk: &Disk,
boot_os_indicies: &Option<Vec<usize>>,
) -> Vec<DVLine> {
let mut description: Vec<DVLine> = vec![ let mut description: Vec<DVLine> = vec![
DVLine { DVLine {
line_parts: vec![format!( line_parts: vec![format!(
@ -73,39 +67,15 @@ pub fn get_disk_description_right(
line_colors: vec![Color::Blue], line_colors: vec![Color::Blue],
}, },
]; ];
disk.parts_description for line in &disk.parts_description {
.iter()
.enumerate()
.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.first();
if boot_index.is_some_and(|i| i == &index) {
line_parts.push(String::from(" <-- Boot Partition"));
line_colors.push(Color::Cyan);
}
let boot_index = indicies.get(1);
if boot_index.is_some_and(|i| i == &index) {
line_parts.push(String::from(" <-- OS Partition"));
line_colors.push(Color::Cyan);
}
}
description.push(DVLine { description.push(DVLine {
line_parts, line_parts: vec![line.clone()],
line_colors,
});
});
if disk.parts_description.is_empty() {
description.push(DVLine {
line_parts: vec![String::from("-None-")],
line_colors: vec![Color::Reset], line_colors: vec![Color::Reset],
}); });
} }
description description
} }
#[must_use]
pub fn get_part_description(part: &Partition) -> Vec<DVLine> { pub fn get_part_description(part: &Partition) -> Vec<DVLine> {
let description: Vec<DVLine> = vec![ let description: Vec<DVLine> = vec![
DVLine { DVLine {

View file

@ -1,28 +1,27 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::missing_errors_doc)] //
use color_eyre::Result; use color_eyre::Result;
use tracing_error::ErrorLayer; use tracing_error::ErrorLayer;
use tracing_subscriber::{EnvFilter, fmt, prelude::*}; use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use crate::config; use crate::config;
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref LOG_ENV: String = format!("{}_LOG_LEVEL", config::PROJECT_NAME); pub static ref LOG_ENV: String = format!("{}_LOG_LEVEL", config::PROJECT_NAME);
pub static ref LOG_FILE: String = format!("{}.log", config::PROJECT_NAME.to_lowercase()); pub static ref LOG_FILE: String = format!("{}.log", config::PROJECT_NAME);
//pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); //pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
} }

View file

@ -1,38 +1,31 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::system::{
disk::{Disk, PartitionTableType},
drivers,
};
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mode { pub enum Mode {
// Core
#[default] #[default]
Home,
Done,
Failed,
// Boot Diags
DiagMenu,
BootDiags,
BootScan,
BootSetup,
LogView,
Process,
InjectDrivers,
SetBootMode,
// Clone
ScanDisks, ScanDisks,
InstallDrivers, InstallDrivers,
SelectDisks, SelectDisks,
@ -42,11 +35,31 @@ pub enum Mode {
Clone, Clone,
SelectParts, SelectParts,
PostClone, PostClone,
// Windows Installer Done,
ScanWinSources, Failed,
SelectWinSource, }
SelectWinImage,
SetUserName, #[derive(Debug, Default)]
// WinPE pub struct CloneSettings {
PEMenu, 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

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
pub mod boot; pub mod boot;
pub mod cpu; pub mod cpu;

View file

@ -1,44 +1,33 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // 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 super::{disk::PartitionTableType, drivers::Driver};
use crate::tasks::TaskType; use crate::tasks::Task;
use color_eyre::Result; use color_eyre::Result;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum SafeMode {
#[default]
Disable,
Enable,
}
#[must_use]
pub fn configure_disk( pub fn configure_disk(
letter_boot: &str, letter_boot: &str,
letter_os: &str, letter_os: &str,
safe_mode: SafeMode,
system32: &str, system32: &str,
table_type: &PartitionTableType, table_type: PartitionTableType,
) -> Vec<TaskType> { ) -> Vec<Task> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
// Create // Create
tasks.push(TaskType::CommandWait( tasks.push(Task::Command(
PathBuf::from(format!("{system32}/bcdboot.exe")), PathBuf::from(format!("{system32}/bcdboot.exe")),
vec![ vec![
format!("{letter_os}:\\Windows"), format!("{letter_os}:\\Windows"),
@ -53,8 +42,8 @@ pub fn configure_disk(
)); ));
// Update boot sector (for legacy setups) // Update boot sector (for legacy setups)
if *table_type == PartitionTableType::Legacy { if table_type == PartitionTableType::Legacy {
tasks.push(TaskType::CommandWait( tasks.push(Task::Command(
PathBuf::from(format!("{system32}/bootsect.exe")), PathBuf::from(format!("{system32}/bootsect.exe")),
vec![ vec![
String::from("/nt60"), String::from("/nt60"),
@ -65,38 +54,7 @@ pub fn configure_disk(
)); ));
} }
// Lock in safe mode (if needed) // Lock in safe mode
if safe_mode == SafeMode::Enable {
tasks.push(
set_mode(letter_boot, &SafeMode::Enable, system32, table_type)
.expect("Failed to create set_mode task."),
);
}
// Done
tasks
}
pub fn inject_driver(driver: &Driver, letter_os: &str, system32: &str) -> Result<TaskType> {
//if let Some(driver_path_str) = driver.path.to_str() {
let driver_path = driver.path.to_str().unwrap();
Ok(TaskType::CommandWait(
PathBuf::from(format!("{system32}/dism.exe")),
vec![
format!("/image:{letter_os}:\\"),
String::from("/add-driver"),
format!("/driver:{driver_path}"),
String::from("/recurse"),
],
))
}
pub fn set_mode(
letter_boot: &str,
mode: &SafeMode,
system32: &str,
table_type: &PartitionTableType,
) -> Result<TaskType> {
let bcd_path = match table_type { let bcd_path = match table_type {
PartitionTableType::Guid => { PartitionTableType::Guid => {
format!("{letter_boot}:\\EFI\\Microsoft\\Boot\\BCD") format!("{letter_boot}:\\EFI\\Microsoft\\Boot\\BCD")
@ -105,24 +63,32 @@ pub fn set_mode(
format!("{letter_boot}:\\Boot\\BCD") format!("{letter_boot}:\\Boot\\BCD")
} }
}; };
tasks.push(Task::Command(
// Build CommandWait
let mut cmd_args = vec![String::from("/store"), bcd_path];
match mode {
SafeMode::Disable => {
cmd_args.push(String::from("/deletevalue"));
cmd_args.push(String::from("{default}"));
cmd_args.push(String::from("safeboot"));
}
SafeMode::Enable => {
cmd_args.push(String::from("/set"));
cmd_args.push(String::from("{default}"));
cmd_args.push(String::from("safeboot"));
cmd_args.push(String::from("minimal"));
}
}
Ok(TaskType::CommandWait(
PathBuf::from(format!("{system32}/bcdedit.exe")), PathBuf::from(format!("{system32}/bcdedit.exe")),
cmd_args, vec![
String::from("/store"),
bcd_path,
String::from("/set"),
String::from("{default}"),
String::from("safeboot"),
String::from("minimal"),
],
));
// Done
tasks
}
pub fn inject_driver(driver: &Driver, letter_os: &str, system32: &str) -> Result<Task> {
//if let Some(driver_path_str) = driver.path.to_str() {
let driver_path = driver.path.to_str().unwrap();
Ok(Task::Command(
PathBuf::from(format!("{system32}/dism.exe")),
vec![
format!("/image:{letter_os}:\\"),
String::from("/add-driver"),
format!("/driver:\"{}\"", driver_path,),
String::from("/recurse"),
],
)) ))
} }

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
#[must_use] #[must_use]
pub fn get_cpu_name() -> String { pub fn get_cpu_name() -> String {

View file

@ -1,31 +1,31 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt, fmt,
process::{Command, Stdio}, process::{Command, Stdio},
sync::OnceLock,
thread::sleep, thread::sleep,
time::Duration, time::Duration,
}; };
use tracing::info; use tracing::info;
use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use crate::system::diskpart::{self, REGEXES}; use crate::system::diskpart;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Disk { pub struct Disk {
@ -36,6 +36,7 @@ pub struct Disk {
pub part_type: PartitionTableType, pub part_type: PartitionTableType,
pub parts: Vec<Partition>, pub parts: Vec<Partition>,
pub parts_description: Vec<String>, pub parts_description: Vec<String>,
pub sector_size: usize,
pub serial: String, pub serial: String,
pub size: u64, // In bytes pub size: u64, // In bytes
} }
@ -66,7 +67,6 @@ impl Disk {
} }
} }
#[must_use]
pub fn get_part_letter(&self, part_index: usize) -> String { pub fn get_part_letter(&self, part_index: usize) -> String {
// Used to get Boot and OS letters // Used to get Boot and OS letters
if let Some(part) = self.parts.get(part_index) { if let Some(part) = self.parts.get(part_index) {
@ -76,53 +76,13 @@ impl Disk {
} }
} }
#[must_use]
pub fn get_parts(&self) -> Vec<Partition> { pub fn get_parts(&self) -> Vec<Partition> {
self.parts.clone() self.parts.clone()
} }
#[must_use]
pub fn num_parts(&self) -> usize { pub fn num_parts(&self) -> usize {
self.parts.len() 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 { impl fmt::Display for Disk {
@ -143,24 +103,6 @@ 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 { impl fmt::Display for Partition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s: String; let mut s: String;
@ -218,9 +160,9 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1, id: 1,
fs_type: String::from("NTFS"), fs_type: String::from("NTFS"),
label: String::from("System Reserved"), label: String::from("System Reserved"),
letter: String::from("C"),
part_type: String::from("7"), part_type: String::from("7"),
size: 104_857_600, size: 104_857_600,
..Default::default()
}, },
Partition { Partition {
id: 2, id: 2,
@ -232,17 +174,17 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 5, id: 5,
fs_type: String::from("NTFS"), fs_type: String::from("NTFS"),
label: String::from("Win7"), label: String::from("Win7"),
letter: String::from("D"),
part_type: String::from("7"), part_type: String::from("7"),
size: 267_701_452_800, size: 267_701_452_800,
..Default::default()
}, },
Partition { Partition {
id: 6, id: 6,
fs_type: String::from("NTFS"), fs_type: String::from("NTFS"),
label: String::from("Tools"), label: String::from("Tools"),
letter: String::from("E"),
part_type: String::from("7"), part_type: String::from("7"),
size: 524_288_000, size: 524_288_000,
..Default::default()
}, },
], ],
serial: "MDZ1243".to_string(), serial: "MDZ1243".to_string(),
@ -258,9 +200,9 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1, id: 1,
fs_type: String::from("NTFS"), fs_type: String::from("NTFS"),
label: String::from("Scratch"), label: String::from("Scratch"),
letter: String::from("G"),
part_type: String::from("7"), part_type: String::from("7"),
size: 249_998_951_424, size: 249_998_951_424,
..Default::default()
}], }],
serial: "000010000".to_string(), serial: "000010000".to_string(),
size: 250_000_000_000, size: 250_000_000_000,
@ -276,7 +218,7 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1, id: 1,
fs_type: String::from("FAT32"), fs_type: String::from("FAT32"),
label: String::from("ESP"), label: String::from("ESP"),
letter: String::from("J"), letter: String::from("Q"),
part_type: String::from("EFI"), part_type: String::from("EFI"),
size: 272_629_760, size: 272_629_760,
}, },
@ -290,7 +232,7 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 4, id: 4,
fs_type: String::from("NTFS"), fs_type: String::from("NTFS"),
label: String::from("Win10"), label: String::from("Win10"),
letter: String::from("K"), letter: String::from("V"),
part_type: String::from("MS Basic Data"), part_type: String::from("MS Basic Data"),
size: 824_340_119_552, size: 824_340_119_552,
}, },
@ -309,9 +251,9 @@ pub fn get_fake_disks() -> Vec<Disk> {
id: 1, id: 1,
fs_type: String::from("FAT32"), fs_type: String::from("FAT32"),
label: String::from("EFI Boot"), label: String::from("EFI Boot"),
letter: String::from("I"),
part_type: String::from("EFI"), part_type: String::from("EFI"),
size: 209_715_200, size: 209_715_200,
..Default::default()
}, },
Partition { Partition {
id: 2, id: 2,
@ -370,6 +312,20 @@ pub fn get_disk_serial_number(id: usize) -> String {
serial 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> { fn refresh_fake_disk_info() -> Vec<Partition> {
vec![ vec![
Partition { Partition {
@ -426,11 +382,10 @@ pub fn bytes_to_string(size: u64) -> String {
/// ///
/// Will panic if s is not simliar to 32B, 64MB, etc... /// Will panic if s is not simliar to 32B, 64MB, etc...
pub fn string_to_bytes(s: &str) -> u64 { pub fn string_to_bytes(s: &str) -> u64 {
static RE_BYTES: OnceLock<Regex> = OnceLock::new(); static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s+(\w+)B").unwrap());
let re_bytes = RE_BYTES.get_or_init(|| Regex::new(r"(\d+)\s+(\w+)B").unwrap());
let base: u64 = 1024; let base: u64 = 1024;
let mut size: u64 = 0; let mut size: u64 = 0;
for (_, [size_str, suffix]) in re_bytes.captures_iter(s).map(|c| c.extract()) { for (_, [size_str, suffix]) in RE.captures_iter(s).map(|c| c.extract()) {
let x: u64 = size_str.parse().unwrap(); let x: u64 = size_str.parse().unwrap();
size += x; size += x;
match suffix { match suffix {

View file

@ -1,125 +1,43 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::missing_panics_doc)] //
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::File, fs::File,
io::Write, io::Write,
process::{Command, Output, Stdio}, process::{Command, Output, Stdio},
sync::OnceLock,
}; };
use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use tempfile::tempdir; use tempfile::tempdir;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::system::disk::{ use crate::system::disk::{
Disk, Partition, PartitionTableType, get_disk_serial_number, string_to_bytes, get_disk_serial_number, string_to_bytes, Disk, Partition, PartitionTableType,
}; };
static DEFAULT_MAX_DISKS: usize = 8; static DEFAULT_MAX_DISKS: usize = 8;
#[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()
})
}
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 { pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&str>) -> Disk {
let detail_disk = REGEXES.detail_disk(); static RE_DETAILS: Lazy<Regex> = Lazy::new(|| {
let re_uuid = REGEXES.uuid(); 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()
});
let mut disk = Disk { let mut disk = Disk {
id: disk_id, id: disk_id,
size: disk_size, size: disk_size,
@ -137,11 +55,11 @@ pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&st
// Parse details // Parse details
for (_, [model, part_type, conn_type]) in for (_, [model, part_type, conn_type]) in
detail_disk.captures_iter(&details).map(|c| c.extract()) RE_DETAILS.captures_iter(&details).map(|c| c.extract())
{ {
disk.model = String::from(model); disk.model = String::from(model);
disk.conn_type = String::from(conn_type); 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; disk.part_type = PartitionTableType::Guid;
} else { } else {
disk.part_type = PartitionTableType::Legacy; disk.part_type = PartitionTableType::Legacy;
@ -153,12 +71,13 @@ pub fn get_disk_details(disk_id: usize, disk_size: u64, disk_details: Option<&st
disk disk
} }
pub fn get_partitions( pub fn get_partition_details(
disk_id: usize, disk_id: usize,
disk_details: Option<&str>, disk_details: Option<&str>,
part_details: Option<&str>, part_details: Option<&str>,
) -> Vec<Partition> { ) -> Vec<Partition> {
let list_partitions = REGEXES.list_partition(); static RE_LIS: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Partition\s+(\d+)\s+\w+\s+(\d+\s+\w+B)").unwrap());
let mut parts = Vec::new(); let mut parts = Vec::new();
// List partition // List partition
@ -169,10 +88,7 @@ pub fn get_partitions(
let script = format!("select disk {disk_id}\r\nlist partition"); let script = format!("select disk {disk_id}\r\nlist partition");
contents = run_script(&script); contents = run_script(&script);
}; };
for (_, [number, size]) in list_partitions for (_, [number, size]) in RE_LIS.captures_iter(&contents).map(|c| c.extract()) {
.captures_iter(&contents)
.map(|c| c.extract())
{
let part_num = number.parse().unwrap(); let part_num = number.parse().unwrap();
if part_num != 0 { if part_num != 0 {
// part_num == 0 is reserved for extended partition "containers" so we can exclude them // part_num == 0 is reserved for extended partition "containers" so we can exclude them
@ -203,7 +119,7 @@ pub fn get_partitions(
part_contents = String::from(details); part_contents = String::from(details);
} else { } else {
part_contents = run_script(script.join("\r\n").as_str()); part_contents = run_script(script.join("\r\n").as_str());
} };
parse_partition_details(&mut parts, &part_contents); parse_partition_details(&mut parts, &part_contents);
// Done // Done
@ -211,13 +127,9 @@ pub fn get_partitions(
} }
#[must_use] #[must_use]
pub fn build_dest_format_script( pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType) -> String {
disk_id: usize,
part_type: &PartitionTableType,
format_use_case: FormatUseCase,
) -> String {
let disk_id = format!("{disk_id}"); let disk_id = format!("{disk_id}");
let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"]; let mut script = vec!["select disk {disk_id}", "clean"];
match part_type { match part_type {
PartitionTableType::Guid => { PartitionTableType::Guid => {
script.push("convert gpt"); script.push("convert gpt");
@ -231,21 +143,18 @@ pub fn build_dest_format_script(
script.push("format fs=ntfs quick label=System"); 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) script.join("\r\n").replace("{disk_id}", &disk_id)
} }
#[must_use] #[must_use]
pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String { pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
let capacity = DEFAULT_MAX_DISKS * 3 + 1;
let script: String; let script: String;
// Get disk and partition details // Get disk and partition details
if let Some(disks) = disk_nums { if let Some(disks) = disk_nums {
// (Slower case) // (Slower case)
let mut script_parts = Vec::with_capacity(DEFAULT_MAX_DISKS * 3 + 1); let mut script_parts = Vec::with_capacity(capacity);
// Get list of disks // Get list of disks
script_parts.push(String::from("list disk")); script_parts.push(String::from("list disk"));
@ -261,7 +170,7 @@ pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
script = script_parts.join("\n"); script = script_parts.join("\n");
} else { } else {
// (Best case) // (Best case)
let mut script_parts = Vec::with_capacity(DEFAULT_MAX_DISKS + 1); let mut script_parts = Vec::with_capacity(capacity);
// Get list of disks // Get list of disks
script_parts.push("list disk"); script_parts.push("list disk");
@ -271,9 +180,9 @@ pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
script_parts.push("detail disk"); script_parts.push("detail disk");
script_parts.push("list partition"); script_parts.push("list partition");
// Limit to DEFAULT_MAX_DISKS (if there's more the manual "worst" case will be used) // Limit to 8 disks (if there's more the manual "worst" case will be used)
let mut i = 0; let mut i = 0;
while i < DEFAULT_MAX_DISKS { while i < 8 {
script_parts.push("select disk next"); script_parts.push("select disk next");
script_parts.push("detail disk"); script_parts.push("detail disk");
script_parts.push("list partition"); script_parts.push("list partition");
@ -288,8 +197,11 @@ pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
} }
pub fn get_disks() -> Vec<Disk> { pub fn get_disks() -> Vec<Disk> {
let detail_all_disks = REGEXES.detail_all_disks(); static RE_DIS_DET: Lazy<Regex> = Lazy::new(|| {
let list_disk = REGEXES.list_disk(); 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 mut contents: String; let mut contents: String;
let mut output; let mut output;
let mut script: String; let mut script: String;
@ -300,7 +212,7 @@ pub fn get_disks() -> Vec<Disk> {
contents = String::from_utf8_lossy(&output.stdout).to_string(); contents = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(return_code) = output.status.code() { if let Some(return_code) = output.status.code() {
let disk_nums = parse_disk_numbers(&contents); let disk_nums = parse_disk_numbers(&contents);
if return_code != 0 && !disk_nums.is_empty() && disk_nums.len() != DEFAULT_MAX_DISKS { if return_code != 0 && !disk_nums.is_empty() {
// The base assumptions were correct! skipping fallback method // The base assumptions were correct! skipping fallback method
// //
// Since the return_code was not zero, and at least one disk was detected, that // Since the return_code was not zero, and at least one disk was detected, that
@ -331,7 +243,7 @@ pub fn get_disks() -> Vec<Disk> {
// i.e. 0, 1, 3, 4 // i.e. 0, 1, 3, 4
// For instance, this can happen if a drive is disconnected after startup // 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); let mut disks_map: HashMap<&str, Disk> = HashMap::with_capacity(DEFAULT_MAX_DISKS);
for (_, [number, _status, size]) in list_disk for (_, [number, _status, size]) in RE_DIS_LIS
.captures_iter(dp_sections.remove(0)) // This is the "list disk" section .captures_iter(dp_sections.remove(0)) // This is the "list disk" section
.map(|c| c.extract()) .map(|c| c.extract())
{ {
@ -348,11 +260,11 @@ pub fn get_disks() -> Vec<Disk> {
// Add Disk details // Add Disk details
let mut disks_raw: Vec<Disk> = Vec::with_capacity(DEFAULT_MAX_DISKS); let mut disks_raw: Vec<Disk> = Vec::with_capacity(DEFAULT_MAX_DISKS);
for section in dp_sections { for section in dp_sections {
for (_, [id, details]) in detail_all_disks.captures_iter(section).map(|c| c.extract()) { for (_, [id, details]) in RE_DIS_DET.captures_iter(section).map(|c| c.extract()) {
if let Some(disk) = disks_map.remove(id) { if let Some(disk) = disks_map.remove(id) {
// We remove the disk from the HashMap because we're moving it to the Vec // 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)); let mut disk = get_disk_details(disk.id, disk.size, Some(details));
disk.parts = get_partitions(disk.id, Some(details), None); disk.parts = get_partition_details(disk.id, Some(details), None);
disk.generate_descriptions(); disk.generate_descriptions();
disks_raw.push(disk); disks_raw.push(disk);
} }
@ -368,26 +280,57 @@ pub fn parse_disk_numbers(contents: &str) -> Vec<&str> {
// //
//Red Hat VirtIO SCSI Disk Device //Red Hat VirtIO SCSI Disk Device
//Disk ID: {E9CE8DFA-46B2-43C1-99BB-850C661CEE6B} //Disk ID: {E9CE8DFA-46B2-43C1-99BB-850C661CEE6B}
let disk_numbers = REGEXES.disk_numbers(); static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\s+Disk\s+(\d+).*\n.*\n.*\nDisk ID:").unwrap());
let mut disk_nums = Vec::new(); let mut disk_nums = Vec::new();
for (_, [number]) in disk_numbers.captures_iter(contents).map(|c| c.extract()) { for (_, [number]) in RE.captures_iter(contents).map(|c| c.extract()) {
disk_nums.push(number); disk_nums.push(number);
} }
disk_nums disk_nums
} }
pub fn parse_partition_details(parts: &mut [Partition], contents: &str) { pub fn parse_partition_details(parts: &mut [Partition], contents: &str) {
let detail_partition = REGEXES.detail_partition(); 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()
});
for (part_index, (_, [_part_id, part_type, _, _vol_header, vol_line])) in detail_partition for (part_index, (_, [_part_id, part_type, _, _vol_header, vol_line])) in RE_PAR
.captures_iter(contents) .captures_iter(contents)
.map(|c| c.extract()) .map(|c| c.extract())
.enumerate() .enumerate()
{ {
if let Some(part) = parts.get_mut(part_index) { if let Some(part) = parts.get_mut(part_index) {
part.set_details(part_type, vol_line); // 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());
} }
} }
}
}
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 /// # Panics
@ -401,11 +344,12 @@ pub fn run_script_raw(script: &str) -> Output {
script_file script_file
.write_all(script.as_bytes()) .write_all(script.as_bytes())
.expect("Failed to write script to disk"); .expect("Failed to write script to disk");
Command::new("diskpart") let output = Command::new("diskpart")
.args(["/s", format!("{}", script_path.display()).as_str()]) .args(["/s", format!("{}", script_path.display()).as_str()])
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.output() .output()
.expect("Failed to execute Diskpart script") .expect("Failed to execute Diskpart script");
output
} }
#[must_use] #[must_use]
@ -416,11 +360,12 @@ pub fn run_script(script: &str) -> String {
pub fn split_diskpart_disk_output(contents: &str) -> Vec<&str> { 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 // NOTE: A simple split isn't helpful since we want to include the matching lines
let split_all_disks = REGEXES.split_all_disks(); static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"Disk \d+ is now the selected disk").unwrap());
let mut sections = Vec::new(); let mut sections = Vec::new();
let mut starts: Vec<usize> = vec![0]; let mut starts: Vec<usize> = vec![0];
let mut ends: Vec<usize> = Vec::new(); let mut ends: Vec<usize> = Vec::new();
let _: Vec<_> = split_all_disks let _: Vec<_> = RE
.find_iter(contents) .find_iter(contents)
.map(|m| { .map(|m| {
ends.push(m.start() - 1); ends.push(m.start() - 1);

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use std::{env, fmt, fs::read_dir, path::PathBuf, process::Command}; use std::{env, fmt, fs::read_dir, path::PathBuf, process::Command};
@ -73,9 +73,8 @@ pub fn scan() -> Vec<Driver> {
let driver_path = exe_path.with_file_name("drivers"); let driver_path = exe_path.with_file_name("drivers");
if let Ok(dir_entry) = read_dir(driver_path) { if let Ok(dir_entry) = read_dir(driver_path) {
for entry in dir_entry.flatten() { for entry in dir_entry.flatten() {
if entry.path().is_dir() if entry.path().is_dir() {
&& let Ok(name) = entry.file_name().into_string() if let Ok(name) = entry.file_name().into_string() {
{
drivers.push(Driver { drivers.push(Driver {
name, name,
path: entry.path(), path: entry.path(),
@ -85,6 +84,7 @@ pub fn scan() -> Vec<Driver> {
} }
} }
} }
}
drivers.sort(); drivers.sort();
drivers.reverse(); drivers.reverse();
for driver in &mut drivers { for driver in &mut drivers {
@ -95,12 +95,12 @@ pub fn scan() -> Vec<Driver> {
.into_iter() .into_iter()
.filter_map(Result::ok) .filter_map(Result::ok)
{ {
if let Some(ext) = entry.path().extension() if let Some(ext) = entry.path().extension() {
&& ext == "inf" if ext == "inf" {
{
driver.inf_paths.push(entry.into_path()); driver.inf_paths.push(entry.into_path());
} }
} }
} }
}
drivers drivers
} }

View file

@ -1,32 +1,29 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::missing_errors_doc)] //
#![allow(clippy::missing_panics_doc)]
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
fmt,
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::{self, JoinHandle, sleep}, thread::{self, sleep, JoinHandle},
time::Duration, time::Duration,
}; };
use color_eyre::Result; use color_eyre::Result;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::info; use tracing::info;
@ -37,76 +34,23 @@ use crate::{
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum TaskResult { pub enum Task {
Error(String), Command(PathBuf, Vec<String>), // (command, args)
Output(String, String, bool), // stdout, stderr, success
}
#[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) Diskpart(String), // (script_as_string)
ScanDisks, ScanDisks,
Sleep, Sleep,
TestPaths(Vec<PathBuf>),
UpdateDestDisk(usize), // (disk_index) UpdateDestDisk(usize), // (disk_index)
UpdateDiskList, UpdateDiskList,
GroupStart { label: String },
GroupEnd { label: String },
}
impl fmt::Display for TaskType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TaskType::CommandNoWait(cmd_path, _) | TaskType::CommandWait(cmd_path, _) => {
write!(
f,
"Command(\"{}\")",
cmd_path.file_name().unwrap().to_string_lossy()
)
}
TaskType::Diskpart(_) => {
write!(f, "Diskpart")
}
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),
}
}
}
#[derive(Debug)]
pub struct Task {
pub handle: Option<JoinHandle<()>>,
pub result: Option<TaskResult>,
pub task_type: TaskType,
}
impl Task {
#[must_use]
pub fn new(task_type: TaskType) -> Task {
Task {
handle: None,
result: None,
task_type,
}
}
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Tasks { pub struct Tasks {
action_tx: mpsc::UnboundedSender<Action>, action_tx: mpsc::UnboundedSender<Action>,
disk_list: Arc<Mutex<Vec<disk::Disk>>>, disk_list: Arc<Mutex<Vec<disk::Disk>>>,
cur_handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
cur_task: Option<Task>,
task_list: VecDeque<Task>, task_list: VecDeque<Task>,
task_rx: mpsc::UnboundedReceiver<TaskResult>, task_rx: mpsc::UnboundedReceiver<Action>, // Used to forward Actions from Tasks to App
task_tx: mpsc::UnboundedSender<TaskResult>, task_tx: mpsc::UnboundedSender<Action>, // Used to forward Actions from Tasks to App
} }
impl Tasks { impl Tasks {
@ -118,239 +62,159 @@ impl Tasks {
Tasks { Tasks {
action_tx, action_tx,
disk_list: disk_list_arc, disk_list: disk_list_arc,
cur_handle: None, handle: None,
cur_task: None,
task_list: VecDeque::new(), task_list: VecDeque::new(),
task_rx, task_rx,
task_tx, task_tx,
} }
} }
pub fn add(&mut self, task_type: TaskType) { pub fn add(&mut self, task: Task) {
info!("Adding task: {:?}", &task_type); info!("Adding task: {:?}", &task);
self.task_list.push_back(Task::new(task_type)); self.task_list.push_back(task);
} }
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 { pub fn idle(&self) -> bool {
self.cur_handle.is_none() self.handle.is_none()
} }
pub fn poll(&mut self) -> Result<Option<Task>> { pub fn poll(&mut self) -> Result<()> {
let mut return_task: Option<Task> = None; // Forward any actions to main app
// Handle task channel item(s) if let Ok(action) = self.task_rx.try_recv() {
if let Ok(result) = self.task_rx.try_recv() let result = self.action_tx.send(action.clone());
&& let Some(mut task) = self.cur_task.take() assert!(result.is_ok(), "Failed to send Action: {action:?}");
{
task.result.replace(result);
self.cur_task.replace(task);
} }
// Check status of current task (if one is running). // Check status of current task (if one is running).
// NOTE: Action::TasksComplete is sent once all tasks are complete // NOTE: Action::NextScreen is sent once all tasks are complete
if let Some(task_handle) = self.cur_handle.take() { if let Some(handle) = self.handle.take() {
if task_handle.is_finished() { if handle.is_finished() {
// Need to return task with handle
if let Some(mut cur_task) = self.cur_task.take() {
cur_task.handle = Some(task_handle);
return_task = Some(cur_task);
}
if self.task_list.is_empty() { if self.task_list.is_empty() {
// No tasks remain // No tasks remain
self.action_tx.send(Action::TasksComplete)?; self.task_tx.send(Action::NextScreen)?;
} else { } else {
// Start next task // Start next task
self.start()?; self.start()?;
} }
} else { } else {
// TaskType not complete, return handle // Task not complete, return handle
self.cur_handle.replace(task_handle); self.handle = Some(handle);
} }
} else if !self.task_list.is_empty() { } else if !self.task_list.is_empty() {
// No current task but one is available // No current task but one is available
self.start()?; self.start()?;
} }
Ok(return_task) Ok(())
} }
pub fn start(&mut self) -> Result<()> { pub fn start(&mut self) -> Result<()> {
self.cur_task = self.task_list.pop_front(); if let Some(task) = self.task_list.pop_front() {
if let Some(task) = self.cur_task.take() { let task_str = format!("{task:?}");
let task_tx = self.task_tx.clone(); let task_tx = self.task_tx.clone();
match task.task_type { match task {
TaskType::CommandNoWait(ref cmd_path, ref cmd_args) => { Task::Command(ref cmd_path, ref cmd_args) => {
self.cur_handle = None; let cmd_path = cmd_path.clone();
run_task_command(cmd_path.clone(), cmd_args.clone(), task_tx); let cmd_args = cmd_args.clone();
if cfg!(windows) {
self.handle = Some(thread::spawn(move || {
let result = Command::new(cmd_path)
.args(cmd_args)
.stdout(Stdio::piped())
.output();
if let Some(action) = match result {
Ok(output) => {
if output.status.success() {
None
} else {
// Command returned an error status
let mut msg = String::new();
if let Ok(stdout) = String::from_utf8(output.stdout) {
msg = String::from(stdout.trim());
} }
TaskType::CommandWait(ref cmd_path, ref cmd_args) => { if msg.is_empty() {
self.cur_handle = Some(run_task_command( msg = String::from("Generic error");
cmd_path.clone(),
cmd_args.clone(),
task_tx,
));
} }
TaskType::Diskpart(ref script) => { Some(Action::Error(format!("Command failed: {msg}",)))
self.cur_handle = Some(run_task_diskpart(script, task_tx));
} }
TaskType::ScanDisks => { }
Err(err) => {
Some(Action::Error(format!("Failed to run command: {err:?}")))
}
} {
let msg = format!("{:?}", &action);
let result = task_tx.send(action);
assert!(result.is_ok(), "Failed to send Action: {msg}");
}
}));
} else {
// Simulate task if not running under Windows
self.handle = Some(thread::spawn(|| sleep(Duration::from_millis(250))));
}
}
Task::Diskpart(ref script) => {
if cfg!(windows) {
let script = String::from(script);
self.handle = Some(thread::spawn(move || {
let output = diskpart::run_script_raw(script.as_str());
if !output.status.success()
&& task_tx
.send(Action::Error(String::from(
"Diskpart script returned an error",
)))
.is_err()
{
panic!("Failed to send Action: {task_str:?}");
}
}));
} else {
// Simulate task if not running under Windows
self.handle = Some(thread::spawn(|| sleep(Duration::from_millis(250))));
}
}
Task::ScanDisks => {
let disk_list_arc = self.disk_list.clone(); let disk_list_arc = self.disk_list.clone();
// Queue UpdateDiskList for various components // Queue UpdateDiskList for various components
self.add(TaskType::UpdateDiskList); self.add(Task::UpdateDiskList);
self.cur_handle = Some(thread::spawn(move || { self.handle = Some(thread::spawn(move || {
let mut disks = disk_list_arc.lock().unwrap(); let mut disks = disk_list_arc.lock().unwrap();
*disks = disk::get_disks(); *disks = disk::get_disks();
})); }));
} }
TaskType::Sleep => { Task::Sleep => {
self.cur_handle = Some(thread::spawn(|| sleep(Duration::from_millis(250)))); self.handle = Some(thread::spawn(|| sleep(Duration::from_millis(250))));
} }
TaskType::TestPaths(ref list) => { Task::UpdateDestDisk(index) => {
self.cur_handle = Some(test_paths(list.clone(), task_tx.clone()));
}
TaskType::UpdateDestDisk(index) => {
self.action_tx.send(Action::DisplayPopup( self.action_tx.send(Action::DisplayPopup(
popup::Type::Info, popup::Type::Info,
String::from("Refreshing disk info"), String::from("Refreshing disk info"),
))?; ))?;
// Queue UpdateDiskList for various components // Queue UpdateDiskList for various components
self.add(TaskType::Sleep); self.add(Task::Sleep);
self.add(TaskType::UpdateDiskList); self.add(Task::UpdateDiskList);
// Update destination disk ~in-place // Update destination disk ~in-place
let disk_list_arc = self.disk_list.clone(); let disk_list_arc = self.disk_list.clone();
self.cur_handle = Some(thread::spawn(move || { self.handle = Some(thread::spawn(move || {
let mut disks = disk_list_arc.lock().unwrap(); let mut disks = disk_list_arc.lock().unwrap();
disks[index].refresh_disk_info(None); let old_disk = &mut disks[index];
disks[index] = disk::refresh_disk_info(old_disk);
})); }));
} }
TaskType::UpdateDiskList => { Task::UpdateDiskList => {
let disks = self.disk_list.lock().unwrap(); let disks = self.disk_list.lock().unwrap();
let disks_copy = disks.clone(); let disks_copy = disks.clone();
let action_tx = self.action_tx.clone(); let action_tx = self.action_tx.clone();
self.cur_handle = Some(thread::spawn(move || { self.handle = Some(thread::spawn(move || {
if let Err(err) = action_tx.send(Action::UpdateDiskList(disks_copy)) { if let Err(err) = action_tx.send(Action::UpdateDiskList(disks_copy)) {
panic!("Failed to send Action: {err:?}"); panic!("Failed to send Action: {err:?}");
} }
})); }));
} }
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);
} }
Ok(()) Ok(())
} }
} }
fn parse_bytes_as_str(bytes: Vec<u8>) -> String {
match String::from_utf8(bytes) {
Ok(s) => s.trim().to_string(),
Err(_) => String::from("Failed to parse bytes as UTF-8 text"),
}
}
fn run_task_command(
cmd_path: PathBuf,
cmd_args: Vec<String>,
task_tx: mpsc::UnboundedSender<TaskResult>,
) -> JoinHandle<()> {
if cfg!(windows) {
thread::spawn(move || {
let result = Command::new(cmd_path)
.args(cmd_args)
.stdout(Stdio::piped())
.output();
match result {
Err(e) => {
task_tx
.send(TaskResult::Error(format!("{:?}", &e)))
.expect("Failed to propegate error?");
}
Ok(output) => {
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)
.unwrap_or_else(|_| panic!("{}", err_str));
}
}
})
} else {
// Simulate task if not running under Windows
thread::spawn(|| sleep(Duration::from_millis(500)))
}
}
fn run_task_diskpart(script: &str, task_tx: mpsc::UnboundedSender<TaskResult>) -> JoinHandle<()> {
if cfg!(windows) {
let script = script.to_owned();
thread::spawn(move || {
let output = diskpart::run_script_raw(&script);
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)
.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

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
pub mod sample_output; pub mod sample_output;
@ -37,7 +37,7 @@ mod diskpart {
} }
#[test] #[test]
fn get_partitions() { fn get_partition_details() {
// Left // Left
let partition_1 = system::disk::Partition { let partition_1 = system::disk::Partition {
id: 1, id: 1,
@ -60,6 +60,7 @@ mod diskpart {
letter: String::from("C"), letter: String::from("C"),
part_type: String::from("ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"), part_type: String::from("ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"),
size: 50465865728, size: 50465865728,
..Default::default()
}; };
// Right // Right
@ -76,7 +77,7 @@ mod diskpart {
id: 4, id: 4,
..Default::default() ..Default::default()
}); });
disk.parts = system::diskpart::get_partitions( disk.parts = system::diskpart::get_partition_details(
2, 2,
Some(sample_output::DETAIL_DISK_GPT), Some(sample_output::DETAIL_DISK_GPT),
Some(sample_output::SELECT_PART_DETAIL_PARTS), Some(sample_output::SELECT_PART_DETAIL_PARTS),
@ -91,7 +92,7 @@ mod diskpart {
#[test] #[test]
fn parse_disk_numbers() { fn parse_disk_numbers() {
let 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); assert_eq!(vec!["0", "2"], disk_numbers);
} }
@ -110,7 +111,7 @@ mod diskpart {
system::diskpart::parse_partition_details( system::diskpart::parse_partition_details(
&mut disk.parts, &mut disk.parts,
sample_output::SELECT_PART_DETAIL_ONE_PART, &sample_output::SELECT_PART_DETAIL_ONE_PART,
); );
assert_eq!(partition_1, disk.parts[0]); assert_eq!(partition_1, disk.parts[0]);
} }

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
#[allow(dead_code)] #[allow(dead_code)]
pub static DETAIL_DISK_GPT: &str = "Disk 2 is now the selected disk. pub static DETAIL_DISK_GPT: &str = "Disk 2 is now the selected disk.

View file

@ -1,22 +1,22 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // 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 #![allow(dead_code)] // Remove this once you start using the code
use std::{ use std::{
io::{Stdout, stdout}, io::{stdout, Stdout},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
time::Duration, time::Duration,
}; };
@ -85,25 +85,21 @@ impl Tui {
}) })
} }
#[must_use]
pub fn tick_rate(mut self, tick_rate: f64) -> Self { pub fn tick_rate(mut self, tick_rate: f64) -> Self {
self.tick_rate = tick_rate; self.tick_rate = tick_rate;
self self
} }
#[must_use]
pub fn frame_rate(mut self, frame_rate: f64) -> Self { pub fn frame_rate(mut self, frame_rate: f64) -> Self {
self.frame_rate = frame_rate; self.frame_rate = frame_rate;
self self
} }
#[must_use]
pub fn mouse(mut self, mouse: bool) -> Self { pub fn mouse(mut self, mouse: bool) -> Self {
self.mouse = mouse; self.mouse = mouse;
self self
} }
#[must_use]
pub fn paste(mut self, paste: bool) -> Self { pub fn paste(mut self, paste: bool) -> Self {
self.paste = paste; self.paste = paste;
self self
@ -139,7 +135,7 @@ impl Tui {
.expect("failed to send init event"); .expect("failed to send init event");
loop { loop {
let event = tokio::select! { let event = tokio::select! {
() = cancellation_token.cancelled() => { _ = cancellation_token.cancelled() => {
break; break;
} }
_ = tick_interval.tick() => Event::Tick, _ = tick_interval.tick() => Event::Tick,
@ -152,7 +148,7 @@ impl Tui {
CrosstermEvent::FocusLost => Event::FocusLost, CrosstermEvent::FocusLost => Event::FocusLost,
CrosstermEvent::FocusGained => Event::FocusGained, CrosstermEvent::FocusGained => Event::FocusGained,
CrosstermEvent::Paste(s) => Event::Paste(s), CrosstermEvent::Paste(s) => Event::Paste(s),
CrosstermEvent::Key(_) => continue, // ignore other events _ => continue, // ignore other events
} }
Some(Err(_)) => Event::Error, Some(Err(_)) => Event::Error,
None => break, // the event stream has stopped and will not produce any more events None => break, // the event stream has stopped and will not produce any more events

View file

@ -1,25 +1,27 @@
# This file is part of Deja-Vu. # This file is part of Deja-vu.
# #
# Deja-Vu is free software: you can redistribute it and/or modify it # 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 # under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Deja-Vu is distributed in the hope that it will be useful, but # Deja-vu is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of # WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details. # See the GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. # along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
[package] [package]
name = "deja-vu" name = "deja-vu"
authors = ["2Shirt <2xShirt@gmail.com>"] authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024" edition = "2021"
license = "GPL" license = "GPL"
version = "0.2.0" version = "0.2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
core = { path = "../core" } core = { path = "../core" }
color-eyre = "0.6.3" color-eyre = "0.6.3"
@ -39,7 +41,6 @@ tokio-util = "0.7.11"
tracing = "0.1.41" tracing = "0.1.41"
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
check_elevation = "0.2.4"
[build-dependencies] [build-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"

View file

@ -1,17 +1,17 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use anyhow::Result; use anyhow::Result;
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder}; use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};

View file

@ -1,46 +1,38 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
//
use core::{ use core::{
action::Action, action::Action,
components::{ components::{
Component, footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, state::StatefulList,
footer::Footer, title::Title, Component,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
}, },
config::Config, config::Config,
line::{DVLine, get_disk_description_right, get_part_description}, line::{get_disk_description_right, get_part_description, DVLine},
state::Mode, state::{CloneSettings, Mode},
system::{ system::{
boot, boot, cpu::get_cpu_name, disk::PartitionTableType, diskpart::build_dest_format_script,
cpu::get_cpu_name,
disk::PartitionTableType,
diskpart::{FormatUseCase, build_dest_format_script},
drivers, drivers,
}, },
tasks::{Task, TaskResult, TaskType, Tasks}, tasks::{Task, Tasks},
tui::{Event, Tui}, tui::{Event, Tui},
}; };
use std::{ use std::{
env, env,
iter::zip, iter::zip,
path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -54,8 +46,6 @@ use ratatui::{
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::state::State;
pub struct App { pub struct App {
// TUI // TUI
action_rx: mpsc::UnboundedReceiver<Action>, action_rx: mpsc::UnboundedReceiver<Action>,
@ -68,11 +58,11 @@ pub struct App {
should_suspend: bool, should_suspend: bool,
tick_rate: f64, tick_rate: f64,
// App // App
clone: CloneSettings,
cur_mode: Mode, cur_mode: Mode,
list: StatefulList<usize>, list: StatefulList<usize>,
prev_mode: Mode, prev_mode: Mode,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
state: State,
tasks: Tasks, tasks: Tasks,
} }
@ -80,13 +70,15 @@ impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> { pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel(); let (action_tx, action_rx) = mpsc::unbounded_channel();
let disk_list_arc = Arc::new(Mutex::new(Vec::new())); let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); let mut tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
tasks.add(Task::ScanDisks);
Ok(Self { Ok(Self {
// TUI // TUI
action_rx, action_rx,
action_tx, action_tx,
components: vec![ components: vec![
Box::new(Title::new("Clone Tool")), Box::new(Title::new()),
Box::new(FpsCounter::new()),
Box::new(Left::new()), Box::new(Left::new()),
Box::new(Right::new()), Box::new(Right::new()),
Box::new(Footer::new()), Box::new(Footer::new()),
@ -99,50 +91,36 @@ impl App {
should_suspend: false, should_suspend: false,
tick_rate, tick_rate,
// App // App
state: State::new(disk_list_arc), clone: CloneSettings::new(disk_list_arc),
cur_mode: Mode::default(), cur_mode: Mode::ScanDisks,
list: StatefulList::default(), list: StatefulList::default(),
prev_mode: Mode::default(), prev_mode: Mode::ScanDisks,
selections: vec![None, None], selections: vec![None, None],
tasks, tasks,
}) })
} }
pub fn prev_mode(&mut self) -> Option<Mode> { pub fn prev_mode(&mut self) -> Option<Mode> {
match self.cur_mode { let new_mode = match self.cur_mode {
Mode::Home => Some(Mode::Home),
Mode::Failed => Some(Mode::Failed), Mode::Failed => Some(Mode::Failed),
Mode::Done => Some(Mode::Done), Mode::Done => Some(Mode::Done),
Mode::SelectParts => Some(Mode::SelectParts), Mode::SelectParts => Some(Mode::SelectParts),
Mode::Confirm => Some(Mode::SelectTableType), Mode::Confirm => Some(Mode::SelectTableType),
Mode::SelectTableType => Some(Mode::SelectDisks), Mode::SelectTableType => Some(Mode::SelectDisks),
Mode::SelectDisks => Some(Mode::ScanDisks), Mode::SelectDisks => Some(Mode::ScanDisks),
// Disallowed moves //
Mode::InstallDrivers Mode::InstallDrivers
| Mode::ScanDisks | Mode::ScanDisks
| Mode::PreClone | Mode::PreClone
| Mode::Clone | Mode::Clone
| Mode::PostClone => None, | Mode::PostClone => None,
// Invalid states };
Mode::BootDiags new_mode
| Mode::BootScan
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| 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> { pub fn next_mode(&mut self) -> Option<Mode> {
let new_mode = match self.cur_mode { let new_mode = match self.cur_mode {
Mode::Home | Mode::InstallDrivers => Mode::ScanDisks, Mode::InstallDrivers => Mode::ScanDisks,
Mode::ScanDisks => Mode::SelectDisks, Mode::ScanDisks => Mode::SelectDisks,
Mode::SelectDisks => Mode::SelectTableType, Mode::SelectDisks => Mode::SelectTableType,
Mode::SelectTableType => Mode::Confirm, Mode::SelectTableType => Mode::Confirm,
@ -152,20 +130,6 @@ impl App {
Mode::SelectParts => Mode::PostClone, Mode::SelectParts => Mode::PostClone,
Mode::PostClone | Mode::Done => Mode::Done, Mode::PostClone | Mode::Done => Mode::Done,
Mode::Failed => Mode::Failed, Mode::Failed => Mode::Failed,
// Invalid states
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}; };
if new_mode == self.cur_mode { if new_mode == self.cur_mode {
@ -180,11 +144,11 @@ impl App {
info!("Setting mode to {new_mode:?}"); info!("Setting mode to {new_mode:?}");
self.cur_mode = new_mode; self.cur_mode = new_mode;
match new_mode { match new_mode {
Mode::InstallDrivers => self.state.scan_drivers(), Mode::InstallDrivers => self.clone.scan_drivers(),
Mode::ScanDisks => { Mode::ScanDisks => {
self.prev_mode = self.cur_mode; self.prev_mode = self.cur_mode;
if self.tasks.idle() { if self.tasks.idle() {
self.tasks.add(TaskType::ScanDisks); self.tasks.add(Task::ScanDisks);
} }
self.action_tx.send(Action::DisplayPopup( self.action_tx.send(Action::DisplayPopup(
popup::Type::Info, popup::Type::Info,
@ -197,24 +161,14 @@ impl App {
String::from("Formatting destination disk"), String::from("Formatting destination disk"),
))?; ))?;
// Get System32 path
let system32 = get_system32_path(&self.action_tx);
// (Re)Enable volume mounting
self.tasks.add(TaskType::CommandWait(
PathBuf::from(format!("{system32}/mountvol.exe")),
vec![String::from("/e")],
));
// Build Diskpart script to format destination disk // Build Diskpart script to format destination disk
let disk_list = self.state.disk_list.lock().unwrap(); let disk_list = self.clone.disk_list.lock().unwrap();
if let Some(disk_index) = self.state.disk_index_dest if let Some(disk_index) = self.clone.disk_index_dest {
&& let Some(disk) = disk_list.get(disk_index) if let Some(disk) = disk_list.get(disk_index) {
{ let table_type = self.clone.table_type.clone().unwrap();
let table_type = self.state.table_type.clone().unwrap(); let diskpart_script = build_dest_format_script(disk.id, &table_type);
let diskpart_script = self.tasks.add(Task::Diskpart(diskpart_script));
build_dest_format_script(disk.id, &table_type, FormatUseCase::Clone); }
self.tasks.add(TaskType::Diskpart(diskpart_script));
} }
} }
Mode::Clone => { Mode::Clone => {
@ -222,12 +176,12 @@ impl App {
popup::Type::Info, popup::Type::Info,
String::from("Running Clone Tool"), String::from("Running Clone Tool"),
))?; ))?;
self.tasks.add(TaskType::CommandWait( self.tasks.add(Task::Command(
self.config.clone_app_path.clone(), self.config.clone_app_path.clone(),
Vec::new(), Vec::new(),
)); ));
if let Some(dest_index) = self.state.disk_index_dest { if let Some(dest_index) = self.clone.disk_index_dest {
self.tasks.add(TaskType::UpdateDestDisk(dest_index)); self.tasks.add(Task::UpdateDestDisk(dest_index));
} }
} }
Mode::PostClone => { Mode::PostClone => {
@ -237,16 +191,26 @@ impl App {
))?; ))?;
// Get System32 path // Get System32 path
let system32 = get_system32_path(&self.action_tx); 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(".")
};
// Add actions // Add actions
let disk_list = self.state.disk_list.lock().unwrap(); let disk_list = self.clone.disk_list.lock().unwrap();
if let Some(disk_index) = self.state.disk_index_dest if let Some(disk_index) = self.clone.disk_index_dest {
&& let Some(disk) = disk_list.get(disk_index) if let Some(disk) = disk_list.get(disk_index) {
{ let table_type = self.clone.table_type.clone().unwrap();
let table_type = self.state.table_type.clone().unwrap(); let letter_boot = disk.get_part_letter(self.clone.part_index_boot.unwrap());
let letter_boot = disk.get_part_letter(self.state.part_index_boot.unwrap()); let letter_os = disk.get_part_letter(self.clone.part_index_os.unwrap());
let letter_os = disk.get_part_letter(self.state.part_index_os.unwrap());
// Safety check // Safety check
if letter_boot.is_empty() || letter_os.is_empty() { if letter_boot.is_empty() || letter_os.is_empty() {
@ -257,18 +221,14 @@ impl App {
} }
// Create boot files // Create boot files
for task in boot::configure_disk( for task in
&letter_boot, boot::configure_disk(&letter_boot, &letter_os, &system32, table_type)
&letter_os, {
boot::SafeMode::Enable,
&system32,
&table_type,
) {
self.tasks.add(task); self.tasks.add(task);
} }
// Inject driver(s) (if selected) // Inject driver(s) (if selected)
if let Some(driver) = &self.state.driver { if let Some(driver) = &self.clone.driver {
if let Ok(task) = boot::inject_driver(driver, &letter_os, &system32) { if let Ok(task) = boot::inject_driver(driver, &letter_os, &system32) {
self.tasks.add(task); self.tasks.add(task);
} else { } else {
@ -280,9 +240,12 @@ impl App {
} }
} }
} }
}
Mode::Done => { Mode::Done => {
self.action_tx self.action_tx.send(Action::DisplayPopup(
.send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?; popup::Type::Success,
String::from("COMPLETE\n\n\nThank you for using this tool!"),
))?;
} }
_ => {} _ => {}
} }
@ -307,7 +270,6 @@ impl App {
} }
let action_tx = self.action_tx.clone(); let action_tx = self.action_tx.clone();
action_tx.send(Action::SetMode(Mode::ScanDisks))?;
loop { loop {
self.handle_events(&mut tui).await?; self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?; self.handle_actions(&mut tui)?;
@ -377,9 +339,12 @@ impl App {
match action { match action {
Action::Tick => { Action::Tick => {
self.last_tick_key_events.drain(..); self.last_tick_key_events.drain(..);
// Check background task(s) match self.cur_mode {
if let Some(task) = self.tasks.poll()? { Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => {
self.handle_task(&task)?; // Check background task
self.tasks.poll()?; // Once all are complete Action::NextScreen is sent
}
_ => {}
} }
} }
Action::Quit => self.should_quit = true, Action::Quit => self.should_quit = true,
@ -400,9 +365,6 @@ impl App {
Mode::Confirm => { Mode::Confirm => {
self.action_tx.send(Action::NextScreen)?; self.action_tx.send(Action::NextScreen)?;
} }
Mode::Done => {
self.action_tx.send(Action::Quit)?;
}
_ => {} _ => {}
}, },
Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
@ -427,29 +389,29 @@ impl App {
Action::Select(one, two) => { Action::Select(one, two) => {
match self.cur_mode { match self.cur_mode {
Mode::InstallDrivers => { Mode::InstallDrivers => {
if let Some(index) = one if let Some(index) = one {
&& let Some(driver) = self.state.driver_list.get(index).cloned() if let Some(driver) = self.clone.driver_list.get(index).cloned() {
{
drivers::load(&driver.inf_paths); drivers::load(&driver.inf_paths);
self.state.driver = Some(driver); self.clone.driver = Some(driver);
}
} }
} }
Mode::SelectDisks => { Mode::SelectDisks => {
self.state.disk_index_source = one; self.clone.disk_index_source = one;
self.state.disk_index_dest = two; self.clone.disk_index_dest = two;
} }
Mode::SelectParts => { Mode::SelectParts => {
self.state.part_index_boot = one; self.clone.part_index_boot = one;
self.state.part_index_os = two; self.clone.part_index_os = two;
} }
Mode::SelectTableType => { Mode::SelectTableType => {
self.state.table_type = { self.clone.table_type = {
if let Some(index) = one { if let Some(index) = one {
match index { match index {
0 => Some(PartitionTableType::Guid), 0 => Some(PartitionTableType::Guid),
1 => Some(PartitionTableType::Legacy), 1 => Some(PartitionTableType::Legacy),
index => { index => {
panic!("Failed to select PartitionTableType: {index}") panic!("Failed to select PartitionTableType: {}", index)
} }
} }
} else { } else {
@ -463,24 +425,24 @@ impl App {
self.selections[1] = two; self.selections[1] = two;
} }
Action::SetMode(new_mode) => { Action::SetMode(new_mode) => {
// Clear TableType selection
match new_mode {
Mode::SelectDisks | Mode::SelectTableType => {
self.state.table_type = None;
}
_ => {}
}
self.set_mode(new_mode)?; self.set_mode(new_mode)?;
self.action_tx self.action_tx
.send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?; .send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?;
self.action_tx.send(build_left_items(self))?; let (title, labels, items, select_one) = build_left_items(self, self.cur_mode);
self.action_tx.send(build_right_items(self))?; self.action_tx
.send(Action::UpdateLeft(title, labels, items, select_one))?;
let (labels, start, items) = build_right_items(self, self.cur_mode);
self.action_tx
.send(Action::UpdateRight(labels, start, items))?;
match new_mode { match new_mode {
// Mode::InstallDrivers | Mode::SelectDisks => {
// self.action_tx.send(Action::Select(None, None))?
// }
Mode::SelectTableType | Mode::Confirm => { Mode::SelectTableType | Mode::Confirm => {
// Select source/dest disks // Select source/dest disks
self.action_tx.send(Action::SelectRight( self.action_tx.send(Action::SelectRight(
self.state.disk_index_source, self.clone.disk_index_source,
self.state.disk_index_dest, self.clone.disk_index_dest,
))?; ))?;
} }
Mode::SelectParts => { Mode::SelectParts => {
@ -488,7 +450,7 @@ impl App {
self.action_tx.send(Action::Select(Some(0), None))?; self.action_tx.send(Action::Select(Some(0), None))?;
// Highlight 2nd or 3rd partition as OS partition // Highlight 2nd or 3rd partition as OS partition
let index = if let Some(table_type) = &self.state.table_type { let index = if let Some(table_type) = &self.clone.table_type {
match table_type { match table_type {
PartitionTableType::Guid => 2, PartitionTableType::Guid => 2,
PartitionTableType::Legacy => 1, PartitionTableType::Legacy => 1,
@ -501,7 +463,6 @@ impl App {
_ => {} _ => {}
}; };
} }
Action::TasksComplete => self.action_tx.send(Action::NextScreen)?,
_ => {} _ => {}
} }
for component in &mut self.components { for component in &mut self.components {
@ -519,43 +480,14 @@ impl App {
Ok(()) Ok(())
} }
fn handle_task(&mut self, task: &Task) -> Result<()> {
match task.task_type {
TaskType::CommandWait(_, _) | TaskType::Diskpart(_) => {
// Check result
if let Some(result) = &task.result {
match result {
TaskResult::Error(msg) => {
self.action_tx
.send(Action::Error(format!("{} Failed: {msg}", task.task_type)))?;
}
TaskResult::Output(stdout, stderr, success) => {
if !success {
let msg = if !stdout.is_empty() {
stdout.clone()
} else if !stderr.is_empty() {
stderr.clone()
} else {
String::from("Unknown Error")
};
self.action_tx.send(Action::Error(format!(
"{} Failed: {msg}",
task.task_type
)))?;
}
}
}
}
}
_ => {}
}
Ok(())
}
fn render(&mut self, tui: &mut Tui) -> Result<()> { fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| { tui.draw(|frame| {
if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] { if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] {
let component_areas = vec![header, left, right, footer, popup]; let component_areas = vec![
header, // Title Bar
header, // FPS Counter
left, right, footer, popup,
];
for (component, area) in zip(self.components.iter_mut(), component_areas) { for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) { if let Err(err) = component.draw(frame, area) {
let _ = self let _ = self
@ -625,117 +557,76 @@ fn get_chunks(r: Rect) -> Vec<Rect> {
fn build_footer_string(cur_mode: Mode) -> String { fn build_footer_string(cur_mode: Mode) -> String {
match cur_mode { match cur_mode {
Mode::Home | Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => {
String::from("(q) to quit") String::from("(q) to quit")
} }
Mode::SelectParts => String::from("(Enter) to select / (s) to start over / (q) to quit"), Mode::SelectParts => String::from("(Enter) to select / (s) to start over / (q) to quit"),
Mode::InstallDrivers => String::from("(Enter) to select / (q) to quit"),
Mode::SelectDisks => String::from( Mode::SelectDisks => String::from(
"(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit", "(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit",
), ),
Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"), Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"),
Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"), Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"),
Mode::Done | Mode::Failed => String::from("(Enter) or (q) to quit"), Mode::Done | Mode::Failed | Mode::InstallDrivers => String::from("(Enter) or (q) to quit"),
// Invalid states
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| 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) -> (String, Vec<String>, Vec<String>, bool) {
fn build_left_items(app: &App) -> Action {
let select_type: SelectionType;
let title: String; let title: String;
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels: Vec<String> = Vec::new(); let mut labels: Vec<String> = Vec::new();
match app.cur_mode { let mut select_one = false;
Mode::Home => { match cur_mode {
select_type = SelectionType::Loop;
title = String::from("Home");
}
Mode::InstallDrivers => { Mode::InstallDrivers => {
select_type = SelectionType::One;
title = String::from("Install Drivers"); title = String::from("Install Drivers");
app.state select_one = true;
app.clone
.driver_list .driver_list
.iter() .iter()
.for_each(|driver| items.push(driver.to_string())); .for_each(|driver| items.push(driver.to_string()));
} }
Mode::SelectDisks => { Mode::SelectDisks => {
select_type = SelectionType::Two;
title = String::from("Select Source and Destination Disks"); title = String::from("Select Source and Destination Disks");
labels.push(String::from("source")); labels.push(String::from("source"));
labels.push(String::from("dest")); labels.push(String::from("dest"));
let disk_list = app.state.disk_list.lock().unwrap(); let disk_list = app.clone.disk_list.lock().unwrap();
disk_list disk_list
.iter() .iter()
.for_each(|disk| items.push(disk.description.to_string())); .for_each(|disk| items.push(disk.description.to_string()));
} }
Mode::SelectTableType => { Mode::SelectTableType => {
select_type = SelectionType::One;
title = String::from("Select Partition Table Type"); title = String::from("Select Partition Table Type");
select_one = true;
items.push(format!("{}", PartitionTableType::Guid)); items.push(format!("{}", PartitionTableType::Guid));
items.push(format!("{}", PartitionTableType::Legacy)); items.push(format!("{}", PartitionTableType::Legacy));
} }
Mode::Confirm => { Mode::Confirm => {
select_type = SelectionType::Loop;
title = String::from("Confirm Selections"); title = String::from("Confirm Selections");
} }
Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { Mode::PreClone | Mode::Clone | Mode::PostClone | Mode::ScanDisks => {
select_type = SelectionType::Loop;
title = String::from("Processing"); title = String::from("Processing");
} }
Mode::SelectParts => { Mode::SelectParts => {
select_type = SelectionType::Two;
title = String::from("Select Boot and OS Partitions"); title = String::from("Select Boot and OS Partitions");
labels.push(String::from("boot")); labels.push(String::from("boot"));
labels.push(String::from("os")); labels.push(String::from("os"));
let disk_list = app.state.disk_list.lock().unwrap(); let disk_list = app.clone.disk_list.lock().unwrap();
if let Some(index) = app.state.disk_index_dest if let Some(index) = app.clone.disk_index_dest {
&& let Some(disk) = disk_list.get(index) if let Some(disk) = disk_list.get(index) {
{
disk.get_parts().iter().for_each(|part| { disk.get_parts().iter().for_each(|part| {
items.push(part.to_string()); items.push(part.to_string());
}); });
} }
} }
Mode::Done | Mode::Failed => {
select_type = SelectionType::Loop;
title = String::from("Done");
} }
// Invalid states Mode::Done | Mode::Failed => title = String::from("Done"),
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}; };
Action::UpdateLeft(title, labels, items, select_type) (title, labels, items, select_one)
} }
fn build_right_items(app: &App) -> Action { fn build_right_items(app: &App, cur_mode: Mode) -> (Vec<Vec<DVLine>>, usize, Vec<Vec<DVLine>>) {
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new(); let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0; let mut start_index = 0;
match app.cur_mode { match cur_mode {
Mode::InstallDrivers => { Mode::InstallDrivers => {
items.push(vec![DVLine { items.push(vec![DVLine {
line_parts: vec![String::from("CPU")], line_parts: vec![String::from("CPU")],
@ -760,7 +651,7 @@ fn build_right_items(app: &App) -> Action {
], ],
line_colors: vec![Color::Cyan, Color::Red], line_colors: vec![Color::Cyan, Color::Red],
}; };
if let Some(table_type) = &app.state.table_type { if let Some(table_type) = &app.clone.table_type {
// Show table type // Show table type
let type_str = match table_type { let type_str = match table_type {
PartitionTableType::Guid => "GPT", PartitionTableType::Guid => "GPT",
@ -776,49 +667,33 @@ fn build_right_items(app: &App) -> Action {
} else { } else {
labels.push(vec![dest_dv_line]); labels.push(vec![dest_dv_line]);
} }
let disk_list = app.state.disk_list.lock().unwrap(); let disk_list = app.clone.disk_list.lock().unwrap();
disk_list disk_list
.iter() .iter()
.for_each(|disk| items.push(get_disk_description_right(disk, &None))); .for_each(|disk| items.push(get_disk_description_right(&disk)));
} }
Mode::SelectParts => { Mode::SelectParts => {
for s in &["Boot", "OS"] { vec!["Boot", "OS"].iter().for_each(|s| {
labels.push(vec![DVLine { labels.push(vec![DVLine {
line_parts: vec![String::from(*s)], line_parts: vec![String::from(*s)],
line_colors: vec![Color::Cyan], line_colors: vec![Color::Cyan],
}]); }])
} });
if let Some(index) = app.state.disk_index_dest { if let Some(index) = app.clone.disk_index_dest {
start_index = 1; start_index = 1;
let disk_list = app.state.disk_list.lock().unwrap(); let disk_list = app.clone.disk_list.lock().unwrap();
if let Some(disk) = disk_list.get(index) { if let Some(disk) = disk_list.get(index) {
// Disk Details // Disk Details
items.push(get_disk_description_right(disk, &None)); items.push(get_disk_description_right(&disk));
// Partition Details // Partition Details
disk.parts disk.parts
.iter() .iter()
.for_each(|part| items.push(get_part_description(part))); .for_each(|part| items.push(get_part_description(&part)));
} }
} }
} }
_ => {} _ => {}
} }
Action::UpdateRight(labels, start_index, items) (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

@ -1,44 +1,33 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use core;
use crate::app::App; use crate::app::App;
mod app; mod app;
mod state;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let mut msg = None;
if cfg!(windows) {
use check_elevation::is_elevated;
if !is_elevated().expect("Failed to get elevation status.") {
msg.replace("Administrator privedges required for Deja-Vu.");
}
};
if let Some(text) = msg {
println!("{text}");
} else {
core::errors::init()?; core::errors::init()?;
core::logging::init()?; core::logging::init()?;
let args = core::cli::Cli::parse(); let args = core::cli::Cli::parse();
let mut app = App::new(args.tick_rate, args.frame_rate)?; let mut app = App::new(args.tick_rate, args.frame_rate)?;
app.run().await?; app.run().await?;
}
Ok(()) Ok(())
} }

View file

@ -1,47 +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::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();
}
}

View file

@ -1,5 +1,4 @@
name = 'Deja-Vu' name = 'Deja-Vu'
command = 'X:\tools\deja-vu.exe' command = 'X:\tools\deja-vu.exe'
description = "Windows clone assistant tool"
use_conemu = true use_conemu = true
separator = false separator = false

View file

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

View file

@ -1,5 +1,4 @@
name = 'NTPWEdit' name = 'NTPWEdit'
command = 'X:\Program Files\NTPWEdit\ntpwedit.exe' command = 'X:\Program Files\NTPWEdit\ntpwedit.exe'
description = 'Mostly used to unlock the built-in admin account'
use_conemu = false use_conemu = false
separator = false separator = false

View file

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

View file

@ -1,5 +1,4 @@
name = 'Task Manager' name = 'Task Manager'
command = 'X:\Windows\System32\taskmgr.exe' command = 'X:\Windows\System32\taskmgr.exe'
description = 'Manage those tasks'
use_conemu = false use_conemu = false
separator = false separator = false

View file

@ -1,5 +0,0 @@
name = 'Boot-Diagnostics'
command = 'X:\tools\boot-diags.exe'
description = "Boot issue assessment tool"
use_conemu = true
separator = false

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

@ -0,0 +1,17 @@
# This file is part of Deja-vu.
#
# Deja-vu is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Deja-vu is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
con_emu = 'X:\Program Files\ConEmu\ConEmu64.exe'
tools = []

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

BIN
logo.xcf

Binary file not shown.

View file

@ -1,46 +1,29 @@
# This file is part of Deja-Vu. # This file is part of Deja-vu.
# #
# Deja-Vu is free software: you can redistribute it and/or modify it # 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 # under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Deja-Vu is distributed in the hope that it will be useful, but # Deja-vu is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of # WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details. # See the GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. # along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
[package] [package]
name = "pe-menu" name = "pe-menu"
authors = ["2Shirt <2xShirt@gmail.com>"] authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024" edition = "2021"
license = "GPL" license = "GPL"
version = "0.2.0" version = "0.2.0"
[dependencies] [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"] } crossterm = { version = "0.28.1", features = ["event-stream"] }
futures = "0.3.30" futures = "0.3.30"
ratatui = "0.29.0" ratatui = "0.29.0"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] } tokio = { version = "1.43.0", features = ["full"] }
toml = "0.8.13" toml = "0.8.13"
tracing = "0.1.41"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

View file

@ -1,28 +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 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()
}

View file

@ -1,485 +1,366 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use core::{ use ratatui::widgets::ListState;
action::Action,
components::{
Component,
footer::Footer,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
},
config::Config,
line::DVLine,
state::Mode,
tasks::{TaskType, Tasks},
tui::{Event, Tui},
};
use std::{
env, fs,
iter::zip,
path::PathBuf,
sync::{Arc, Mutex},
};
use color_eyre::Result;
use ratatui::{
crossterm::event::KeyEvent,
layout::{Constraint, Direction, Layout},
prelude::Rect,
style::Color,
};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::mpsc; use std::{
use tracing::{debug, info}; env, error, fs, io,
path::PathBuf,
process::{Command, Output},
thread::{self, JoinHandle},
};
#[derive(Clone, Debug, Default, Deserialize)] /// Application result type.
#[allow(clippy::module_name_repetitions)]
pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
/// Application exit reasons
#[derive(Debug, Default)]
pub enum QuitReason {
#[default]
Exit,
Poweroff,
Restart,
}
/// Config
#[derive(Debug, Deserialize)]
pub struct Config {
con_emu: String,
tools: Vec<Tool>,
}
impl Config {
/// # Panics
///
/// Will panic for many reasons
#[must_use]
pub fn load() -> Option<Config> {
// Main config
let exe_path = env::current_exe().expect("Failed to find main executable");
let contents = fs::read_to_string(exe_path.with_file_name("pe-menu.toml"))
.expect("Failed to load config file");
let mut new_config: Config =
toml::from_str(&contents).expect("Failed to parse config file");
// Tools
let tool_config_path = exe_path.parent().unwrap().join("menu_entries");
let mut entries: Vec<PathBuf> = std::fs::read_dir(tool_config_path)
.expect("Failed to find any tool configs")
.map(|res| res.map(|e| e.path()))
.filter_map(Result::ok)
.collect();
entries.sort();
for entry in entries {
let contents = fs::read_to_string(&entry).expect("Failed to read tool config file");
let tool: Tool = toml::from_str(&contents).expect("Failed to parse tool config file");
new_config.tools.push(tool);
}
// Done
Some(new_config)
}
}
/// `PopUp`
#[derive(Debug, Clone, PartialEq)]
pub struct PopUp {
pub title: String,
pub body: String,
}
impl PopUp {
#[must_use]
pub fn new(title: &str, body: &str) -> PopUp {
PopUp {
title: String::from(title),
body: String::from(body),
}
}
}
/// `Tool`
#[derive(Debug, Deserialize)]
pub struct Tool { pub struct Tool {
name: String, name: String,
command: String, command: String,
description: String,
args: Option<Vec<String>>, args: Option<Vec<String>>,
use_conemu: bool, use_conemu: bool,
separator: bool, separator: bool,
} }
/// `MenuEntry`
#[derive(Default, Debug, Clone, PartialEq)]
pub struct MenuEntry {
pub name: String,
pub command: String,
pub args: Vec<String>,
pub use_conemu: bool,
pub separator: bool,
}
impl MenuEntry {
#[must_use]
pub fn new(
name: &str,
command: &str,
args: Option<Vec<String>>,
use_conemu: bool,
separator: bool,
) -> MenuEntry {
let mut my_args = Vec::new();
if let Some(a) = args {
my_args.clone_from(&a);
}
MenuEntry {
name: String::from(name),
command: String::from(command),
args: my_args,
use_conemu,
separator,
}
}
}
/// `StatefulList`
#[derive(Default, Debug, Clone, PartialEq)]
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
pub last_selected: Option<usize>,
}
impl<T: Clone> StatefulList<T> {
#[must_use]
pub fn new() -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items: Vec::new(),
last_selected: None,
}
}
#[must_use]
pub fn get_selected(&self) -> Option<&T> {
if let Some(i) = self.state.selected() {
self.items.get(i)
} else {
None
}
}
pub fn pop_selected(&mut self) -> Option<T> {
if let Some(i) = self.state.selected() {
Some(self.items[i].clone())
} else {
None
}
}
fn select_first_item(&mut self) {
if self.items.is_empty() {
self.state.select(None);
} else {
self.state.select(Some(0));
}
self.last_selected = None;
}
pub fn set_items(&mut self, items: Vec<T>) {
// Clear list and rebuild with provided items
self.items.clear();
for item in items {
self.items.push(item);
}
// Reset state and select first item (if available)
self.state = ListState::default();
self.select_first_item();
}
pub fn next(&mut self) {
if self.items.is_empty() {
return;
}
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
if self.items.is_empty() {
return;
}
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
}
/// Application.
#[derive(Debug)]
pub struct App { pub struct App {
// TUI pub config: Config,
action_rx: mpsc::UnboundedReceiver<Action>, pub main_menu: StatefulList<MenuEntry>,
action_tx: mpsc::UnboundedSender<Action>, pub popup: Option<PopUp>,
components: Vec<Box<dyn Component>>, pub quit_reason: QuitReason,
config: Config, pub running: bool,
frame_rate: f64, pub thread_pool: Vec<JoinHandle<Result<Output, io::Error>>>,
last_tick_key_events: Vec<KeyEvent>, }
should_quit: bool,
should_suspend: bool, impl Default for App {
tick_rate: f64, fn default() -> Self {
// App let config = Config::load();
list: StatefulList<Tool>, Self {
mode: Mode, config: config.unwrap(),
tasks: Tasks, running: true,
quit_reason: QuitReason::Exit,
main_menu: StatefulList::new(),
popup: None,
thread_pool: Vec::new(),
}
}
} }
impl App { impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> { /// Constructs a new instance of [`App`].
let (action_tx, action_rx) = mpsc::unbounded_channel(); #[must_use]
let disk_list_arc = Arc::new(Mutex::new(Vec::new())); pub fn new() -> Self {
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); let mut app = Self::default();
let mut list = StatefulList::default();
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(Left::new()),
Box::new(Right::new()),
Box::new(Footer::new()),
Box::new(popup::Popup::new()),
],
config: Config::new()?,
frame_rate,
last_tick_key_events: Vec::new(),
should_quit: false,
should_suspend: false,
tick_rate,
// App
list,
mode: Mode::default(),
tasks,
})
}
pub async fn run(&mut self) -> Result<()> { // Add MenuEntries
let mut tui = Tui::new()? for tool in &app.config.tools {
// .mouse(true) // uncomment this line to enable mouse support app.main_menu.items.push(MenuEntry::new(
.tick_rate(self.tick_rate) &tool.name,
.frame_rate(self.frame_rate); &tool.command,
tui.enter()?; tool.args.clone(),
tool.use_conemu,
for component in &mut self.components { tool.separator,
component.register_action_handler(self.action_tx.clone())?;
}
for component in &mut self.components {
component.register_config_handler(self.config.clone())?;
}
for component in &mut self.components {
component.init(tui.size()?)?;
}
let action_tx = self.action_tx.clone();
action_tx.send(Action::SetMode(Mode::PEMenu))?;
loop {
self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?;
if self.should_suspend {
tui.suspend()?;
action_tx.send(Action::Resume)?;
action_tx.send(Action::ClearScreen)?;
// tui.mouse(true);
tui.enter()?;
} else if self.should_quit {
tui.stop()?;
break;
}
}
tui.exit()?;
Ok(())
}
async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> {
let Some(event) = tui.next_event().await else {
return Ok(());
};
let action_tx = self.action_tx.clone();
match event {
Event::Quit => action_tx.send(Action::Quit)?,
Event::Tick => action_tx.send(Action::Tick)?,
Event::Render => action_tx.send(Action::Render)?,
Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
Event::Key(key) => self.handle_key_event(key)?,
_ => {}
}
for component in &mut self.components {
if let Some(action) = component.handle_events(Some(event.clone()))? {
action_tx.send(action)?;
}
}
Ok(())
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
let action_tx = self.action_tx.clone();
let Some(keymap) = self.config.keybindings.get(&self.mode) else {
return Ok(());
};
if let Some(action) = keymap.get(&vec![key]) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
} else {
// If the key was not handled as a single key action,
// then consider it for multi-key combinations.
self.last_tick_key_events.push(key);
// Check for multi-key combinations
if let Some(action) = keymap.get(&self.last_tick_key_events) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
}
}
Ok(())
}
fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> {
while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render {
debug!("{action:?}");
}
match action {
Action::Tick => {
self.last_tick_key_events.drain(..);
// Check background task(s)
self.tasks.poll()?;
}
Action::Quit => self.should_quit = true,
Action::Suspend => self.should_suspend = true,
Action::Resume => self.should_suspend = false,
Action::ClearScreen => tui.terminal.clear()?,
Action::KeyUp => {
self.list.previous();
if let Some(tool) = self.list.get_selected()
&& tool.separator
{
// Skip over separator
self.list.previous();
if let Some(index) = self.list.selected() {
self.action_tx.send(Action::Highlight(index))?;
}
}
}
Action::KeyDown => {
self.list.next();
if let Some(tool) = self.list.get_selected()
&& tool.separator
{
// Skip over separator
self.list.next();
if let Some(index) = self.list.selected() {
self.action_tx.send(Action::Highlight(index))?;
}
}
}
Action::Error(ref msg) => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?;
self.action_tx.send(Action::SetMode(Mode::Failed))?;
}
Action::Process => {
// Run selected tool
if let Some(tool) = self.list.get_selected() {
info!("Run tool: {:?}", &tool);
self.tasks.add(build_tool_command(self, &tool));
}
}
Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?,
Action::SetMode(mode) => {
self.mode = mode;
self.action_tx.send(Action::UpdateFooter(String::from(
"(Enter) to select / (t) for terminal / (p) to power off / (r) to restart",
)))?;
self.action_tx.send(build_left_items(self))?;
self.action_tx.send(build_right_items(self))?;
self.action_tx.send(Action::Select(None, None))?;
}
Action::OpenTerminal => {
self.tasks.add(TaskType::CommandNoWait(
PathBuf::from("cmd.exe"),
vec![String::from("-new_console:n")],
)); ));
} }
Action::Restart => { app.main_menu.select_first_item();
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
String::from("Restarting..."),
))?;
self.tasks.add(TaskType::CommandWait(
PathBuf::from("X:/Windows/System32/sync64.exe"),
vec![String::from("-r")],
));
self.tasks.add(TaskType::CommandWait(
PathBuf::from("X:/Windows/System32/wpeutil.exe"),
vec![String::from("reboot")],
));
}
Action::Shutdown => {
// NOTE: Using 'Powering off' to match the key pressed
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
String::from("Powering off..."),
))?;
self.tasks.add(TaskType::CommandWait(
PathBuf::from("X:/Windows/System32/sync64.exe"),
vec![String::from("-r")],
));
self.tasks.add(TaskType::CommandWait(
PathBuf::from("X:/Windows/System32/wpeutil.exe"),
vec![String::from("shutdown")],
));
}
_ => {}
}
for component in &mut self.components {
if let Some(action) = component.update(action.clone())? {
self.action_tx.send(action)?;
};
}
}
Ok(())
}
fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> {
tui.resize(Rect::new(0, 0, w, h))?;
self.render(tui)?;
Ok(())
}
fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] {
let component_areas = vec![header, left, right, footer, popup];
for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {err:?}")));
}
}
};
})?;
Ok(())
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
// Cut the given rectangle into three vertical pieces
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
// Then cut the middle vertical piece into three width-wise pieces
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1] // Return the middle chunk
}
fn get_chunks(r: Rect) -> Vec<Rect> {
let mut chunks: Vec<Rect> = Vec::with_capacity(6);
// Main sections
chunks.extend(
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(3),
])
.split(r)
.to_vec(),
);
// Left/Right
chunks.extend(
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(centered_rect(90, 90, chunks[1]))
.to_vec(),
);
// Popup
chunks.push(centered_rect(60, 25, r));
// Done // Done
chunks app
} }
pub fn build_tool_command(app: &App, tool: &Tool) -> TaskType { /// Handles the tick event of the terminal.
let cmd_path: PathBuf; pub fn tick(&self) {}
let mut cmd_args: Vec<String> = Vec::new();
let start_index: usize; /// Actually exit application
if tool.use_conemu { ///
cmd_path = app.config.conemu_path.clone(); /// # Errors
cmd_args.push(String::from("-run")); /// # Panics
cmd_args.push(tool.command.clone()); ///
cmd_args.push(String::from("-new_console:n")); /// Will panic if wpeutil fails to reboot or shutdown
start_index = 1; pub fn exit(&self) -> Result<(), &'static str> {
let mut argument: Option<String> = None;
match self.quit_reason {
QuitReason::Exit => {}
QuitReason::Poweroff => argument = Some(String::from("shutdown")),
QuitReason::Restart => argument = Some(String::from("reboot")),
}
if let Some(a) = argument {
Command::new("wpeutil")
.arg(a)
.output()
.expect("Failed to run exit command");
}
Ok(())
}
/// Set running to false to quit the application.
pub fn quit(&mut self, reason: QuitReason) {
self.running = false;
self.quit_reason = reason;
}
/// # Panics
///
/// Will panic if command fails to run
pub fn open_terminal(&mut self) {
Command::new("cmd.exe")
.arg("-new_console:n")
.output()
.expect("Failed to run command");
}
/// # Panics
///
/// Will panic if menu entry isn't found
pub fn run_tool(&mut self) {
// Command
let tool: &MenuEntry;
if let Some(index) = self.main_menu.state.selected() {
tool = &self.main_menu.items[index];
} else { } else {
cmd_path = PathBuf::from(tool.command.clone()); self.popup = Some(PopUp::new(
start_index = 0; "Failed to find menu entry",
"Check for an updated version of Deja-Vu",
));
return;
} }
if let Some(args) = &tool.args let command = if tool.use_conemu {
&& args.len() > start_index self.config.con_emu.clone()
{ } else {
args[start_index..].iter().for_each(|a| { tool.command.clone()
cmd_args.push(a.clone()); };
});
}
TaskType::CommandNoWait(cmd_path, cmd_args)
}
fn build_left_items(app: &App) -> Action { // Separators
let title = String::from("Tools");
let labels = vec![String::new(), String::new()];
let items = app
.list
.items
.iter()
.map(|tool| {
if tool.separator { if tool.separator {
String::from("──────────────") return;
} else {
tool.name.clone()
} }
})
// ─
.collect();
Action::UpdateLeft(title, labels, items, SelectionType::Loop)
}
fn build_right_items(app: &App) -> Action { // Args
let labels: Vec<Vec<DVLine>> = Vec::new(); let mut args = tool.args.clone();
let items = app if tool.use_conemu {
.list args.insert(0, tool.command.clone());
.items args.push(String::from("-new_console:n"));
.iter() }
.map(|entry| {
vec![
DVLine {
line_parts: vec![entry.name.clone()],
line_colors: vec![Color::Cyan],
},
DVLine {
line_parts: vec![String::new()],
line_colors: vec![Color::Reset],
},
DVLine {
line_parts: vec![entry.description.clone()],
line_colors: vec![Color::Reset],
},
]
})
.collect();
let start_index = 0;
Action::UpdateRight(labels, start_index, items)
}
pub fn load_tools(action_tx: mpsc::UnboundedSender<Action>) -> Vec<Tool> { // Check path
let mut entries: Vec<PathBuf>; let command_path = PathBuf::from(&command);
let mut tools: Vec<Tool> = Vec::new(); if let Ok(true) = command_path.try_exists() {
let exe_path = env::current_exe().expect("Failed to find main executable"); // File path exists
let tool_config_path = exe_path.parent().unwrap().join("menu_entries");
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();
} else { } else {
action_tx // File path doesn't exist or is a broken symlink/etc
.send(Action::Error(String::from( // The latter case would be Ok(false) rather than Err(_)
"Failed to find any tool configs", self.popup = Some(PopUp::new("Tool Missing", &format!("Tool path: {command}")));
))) return;
.unwrap();
entries = Vec::new();
} }
entries.iter().for_each(|entry| {
if let Ok(toml_str) = fs::read_to_string(entry) { // Run
if let Ok(tool) = toml::from_str(&toml_str) { // TODO: This really needs refactored to use channels so we can properly check if the
tools.push(tool); // command fails.
} else { let new_thread = thread::spawn(move || Command::new(command_path).args(args).output());
action_tx self.thread_pool.push(new_thread);
.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
} }

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

@ -0,0 +1,116 @@
// This file is part of Deja-vu.
//
// Deja-vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
//
use std::time::Duration;
use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
use futures::{FutureExt, StreamExt};
use tokio::sync::mpsc;
use crate::app::AppResult;
/// Terminal events.
#[derive(Clone, Copy, Debug)]
pub enum Event {
/// Terminal tick.
Tick,
/// Key press.
Key(KeyEvent),
/// Mouse click/scroll.
Mouse(MouseEvent),
/// Terminal resize.
Resize(u16, u16),
}
/// Terminal event handler.
#[allow(dead_code)]
#[derive(Debug)]
pub struct Handler {
/// Event sender channel.
sender: mpsc::UnboundedSender<Event>,
/// Event receiver channel.
receiver: mpsc::UnboundedReceiver<Event>,
/// Event handler thread.
handler: tokio::task::JoinHandle<()>,
}
impl Handler {
/// Constructs a new instance of [`Handler`].
///
/// # Panics
///
/// Will panic if `sender_clone ` doesn't unwrap
#[must_use]
pub fn new(tick_rate: u64) -> Self {
let tick_rate = Duration::from_millis(tick_rate);
let (sender, receiver) = mpsc::unbounded_channel();
let sender_clone = sender.clone();
let handler = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut tick = tokio::time::interval(tick_rate);
loop {
let tick_delay = tick.tick();
let crossterm_event = reader.next().fuse();
tokio::select! {
() = sender_clone.closed() => {
break;
}
_ = tick_delay => {
sender_clone.send(Event::Tick).unwrap();
}
Some(Ok(evt)) = crossterm_event => {
match evt {
CrosstermEvent::Key(key) => {
if key.kind == crossterm::event::KeyEventKind::Press {
sender_clone.send(Event::Key(key)).unwrap();
}
},
CrosstermEvent::Mouse(mouse) => {
sender_clone.send(Event::Mouse(mouse)).unwrap();
},
CrosstermEvent::Resize(x, y) => {
sender_clone.send(Event::Resize(x, y)).unwrap();
},
CrosstermEvent::FocusGained | CrosstermEvent::FocusLost | CrosstermEvent::Paste(_) => {},
}
}
};
}
});
Self {
sender,
receiver,
handler,
}
}
/// Receive the next event from the handler thread.
///
/// This function will always block the current thread if
/// there is no data available and it's possible for more data to be sent.
///
/// # Errors
///
/// Will return error if a event is not found
pub async fn next(&mut self) -> AppResult<Event> {
self.receiver
.recv()
.await
.ok_or(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"This is an IO error",
)))
}
}

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

@ -0,0 +1,61 @@
// This file is part of Deja-vu.
//
// Deja-vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
//
use crate::app::{App, QuitReason};
use crossterm::event::{KeyCode, KeyEvent};
/// Handles the key events and updates the state of [`App`].
pub fn handle_key_events(key_event: KeyEvent, app: &mut App) {
match key_event.code {
KeyCode::F(5) => app.quit(QuitReason::Exit),
KeyCode::Char('p' | 'P') => app.quit(QuitReason::Poweroff),
KeyCode::Char('r' | 'R') => app.quit(QuitReason::Restart),
KeyCode::Char('t' | 'T') => app.open_terminal(),
KeyCode::Up => {
if app.popup.is_none() {
app.main_menu.previous();
if let Some(e) = app.main_menu.get_selected() {
if e.separator {
// Skip over separators
app.main_menu.previous();
}
}
}
}
KeyCode::Down => {
if app.popup.is_none() {
app.main_menu.next();
if let Some(e) = app.main_menu.get_selected() {
if e.separator {
// Skip over separators
app.main_menu.next();
}
}
}
}
KeyCode::Enter => {
if app.popup.is_some() {
// Clear popup and return to main menu
app.popup = None;
} else {
app.run_tool();
}
}
KeyCode::Esc | KeyCode::Char('q' | 'Q') => {
app.popup = None;
}
_ => {}
}
}

View file

@ -1,17 +1,29 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
pub mod set_username; /// Application.
pub mod wim_scan; pub mod app;
/// Terminal events handler.
pub mod event;
/// Widget renderer.
pub mod ui;
/// Terminal user interface.
pub mod tui;
/// Event handler.
pub mod handler;

View file

@ -1,32 +1,52 @@
// This file is part of Deja-Vu. // This file is part of Deja-vu.
// //
// Deja-Vu is free software: you can redistribute it and/or modify it // 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 // under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// Deja-Vu is distributed in the hope that it will be useful, but // Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of // WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details. // See the GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
// //
use clap::Parser; use pe_menu::app::{App, AppResult};
use color_eyre::Result; use pe_menu::event::{Event, Handler};
use pe_menu::handler::handle_key_events;
use crate::app::App; use pe_menu::tui::Tui;
use ratatui::backend::CrosstermBackend;
mod app; use ratatui::Terminal;
use std::io;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> AppResult<()> {
core::errors::init()?; // Create an application.
core::logging::init()?; let mut app = App::new();
let args = core::cli::Cli::parse(); // Initialize the terminal user interface.
let mut app = App::new(args.tick_rate, args.frame_rate)?; let backend = CrosstermBackend::new(io::stderr());
app.run().await?; let terminal = Terminal::new(backend)?;
let events = Handler::new(250);
let mut tui = Tui::new(terminal, events);
tui.init()?;
// Start the main loop.
while app.running {
// Render the user interface.
tui.draw(&mut app)?;
// Handle events.
match tui.events.next().await? {
Event::Tick => app.tick(),
Event::Key(key_event) => handle_key_events(key_event, &mut app),
Event::Mouse(_) | Event::Resize(_, _) => {}
}
}
// Exit the user interface.
tui.exit()?;
app.exit()?;
Ok(()) Ok(())
} }

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

@ -0,0 +1,111 @@
// This file is part of Deja-vu.
//
// Deja-vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
//
use crate::app::{App, AppResult};
use crate::event::Handler;
use crate::ui;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::Backend;
use ratatui::Terminal;
use std::io;
use std::panic;
/// Representation of a terminal user interface.
///
/// It is responsible for setting up the terminal,
/// initializing the interface and handling the draw events.
#[derive(Debug)]
pub struct Tui<B: Backend> {
/// Interface to the Terminal.
terminal: Terminal<B>,
/// Terminal event handler.
pub events: Handler,
}
impl<B: Backend> Tui<B> {
/// Constructs a new instance of [`Tui`].
pub fn new(terminal: Terminal<B>, events: Handler) -> Self {
Self { terminal, events }
}
/// Initializes the terminal interface.
///
/// It enables the raw mode and sets terminal properties.
///
/// # Errors
///
/// Will return error if `enable_raw_mode` fails
///
/// # Panics
///
/// Will panic if `reset` fails
pub fn init(&mut self) -> AppResult<()> {
terminal::enable_raw_mode()?;
crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?;
// Define a custom panic hook to reset the terminal properties.
// This way, you won't have your terminal messed up if an unexpected error happens.
let panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic| {
Self::reset().expect("failed to reset the terminal");
panic_hook(panic);
}));
self.terminal.hide_cursor()?;
self.terminal.clear()?;
Ok(())
}
/// [`Draw`] the terminal interface by [`rendering`] the widgets.
///
/// [`Draw`]: ratatui::Terminal::draw
/// [`rendering`]: crate::ui::render
///
/// # Errors
///
/// Will return error if `draw` fails
pub fn draw(&mut self, app: &mut App) -> AppResult<()> {
self.terminal.draw(|frame| ui::render(app, frame))?;
Ok(())
}
/// Resets the terminal interface.
///
/// This function is also used for the panic hook to revert
/// the terminal properties if unexpected errors occur.
///
/// # Errors
///
/// Will return error if `disable_raw_mode` fails
fn reset() -> AppResult<()> {
terminal::disable_raw_mode()?;
crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?;
Ok(())
}
/// Exits the terminal interface.
///
/// It disables the raw mode and reverts back the terminal properties.
///
/// # Errors
///
/// Will return error if either `reset` or `show_cursor` fails
pub fn exit(&mut self) -> AppResult<()> {
Self::reset()?;
self.terminal.show_cursor()?;
Ok(())
}
}

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

@ -0,0 +1,127 @@
// This file is part of Deja-vu.
//
// Deja-vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-vu. If not, see <https://www.gnu.org/licenses/>.
//
use crate::app::App;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Clear, HighlightSpacing, List, ListItem, Padding, Paragraph, Wrap},
Frame,
};
/// Renders the user interface widgets.
pub fn render(app: &mut App, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(3),
])
.split(frame.area());
// Title Block
let title_text = Span::styled("Deja-vu: PE Menu", Style::default().fg(Color::LightCyan));
let title = Paragraph::new(Line::from(title_text).centered())
.block(Block::default().borders(Borders::ALL));
frame.render_widget(title, chunks[0]);
// Main Block
let main_chunk = centered_rect(65, 90, chunks[1]);
render_main_pane(frame, app, main_chunk);
// Bottom Block
let footer_text = Span::styled(
"(Enter) to select / (p) to poweroff / (r) to restart / (t) for terminal",
Style::default().fg(Color::DarkGray),
);
let footer = Paragraph::new(Line::from(footer_text).centered())
.block(Block::default().borders(Borders::ALL));
frame.render_widget(footer, chunks[2]);
// Popup blocks
if app.popup.is_some() {
render_popup_pane(frame, app);
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
// Cut the given rectangle into three vertical pieces
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
// Then cut the middle vertical piece into three width-wise pieces
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1] // Return the middle chunk
}
fn render_main_pane(frame: &mut Frame, app: &mut App, chunk: Rect) {
let mut list_items = Vec::<ListItem>::new();
for entry in &app.main_menu.items {
let text = if entry.separator {
if entry.name.is_empty() {
String::from("....................")
} else {
entry.name.clone()
}
} else {
entry.name.clone()
};
list_items.push(ListItem::new(format!(" {text}\n\n\n")));
}
let list = List::new(list_items)
.block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(20, 20, 5, 5)),
)
.highlight_spacing(HighlightSpacing::Always)
.highlight_style(Style::new().green().bold())
.highlight_symbol(" --> ")
.repeat_highlight_symbol(false);
frame.render_stateful_widget(list, chunk, &mut app.main_menu.state);
}
fn render_popup_pane(frame: &mut Frame, app: &mut App) {
let popup_block = Block::default()
.borders(Borders::ALL)
.style(Style::default().red().bold());
if let Some(popup) = &app.popup {
let scan_paragraph = Paragraph::new(vec![
Line::from(Span::raw(&popup.title)),
Line::default(),
Line::from(Span::raw(&popup.body)),
])
.block(popup_block)
.centered()
.wrap(Wrap { trim: false });
let area = centered_rect(60, 25, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(scan_paragraph, area);
}
}

View file

@ -1,50 +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/>.
[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"] }

View file

@ -1,28 +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 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()
}

File diff suppressed because it is too large Load diff

View file

@ -1,135 +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 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

@ -1,154 +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 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(())
}
}

View file

@ -1,36 +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 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(())
}

View file

@ -1,69 +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/>.
//
//#![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)
}
}

View file

@ -1,215 +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::{
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));
}
}

View file

@ -1,336 +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::{
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)
}