Compare commits

...

19 commits

20 changed files with 2064 additions and 28 deletions

63
Cargo.lock generated
View file

@ -689,7 +689,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -2661,15 +2661,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.19.1"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.3",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -2933,6 +2933,16 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]]
name = "tui-input"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19"
dependencies = [
"ratatui",
"unicode-width 0.2.0",
]
[[package]]
name = "typenum"
version = "1.18.0"
@ -3126,6 +3136,30 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "win-installer"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"color-eyre",
"core",
"crossterm",
"futures",
"ratatui",
"serde",
"tempfile",
"tokio",
"toml",
"tracing",
"tracing-error",
"tracing-subscriber",
"tui-input",
"vergen-gix",
"windows-sys 0.61.2",
"xml",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3176,6 +3210,12 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3194,6 +3234,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -3354,6 +3403,12 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xml"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838dd679b10a4180431ce7c2caa6e5585a7c8f63154c19ae99345126572e80cc"
[[package]]
name = "yaml-rust2"
version = "0.10.0"

View file

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

View file

@ -657,7 +657,11 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::PEMenu
| Mode::PreClone
| Mode::PostClone
| Mode::SelectTableType => {
| Mode::ScanWinSources
| Mode::SelectTableType
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetUserName => {
panic!("This shouldn't happen?")
}
}
@ -770,7 +774,11 @@ fn build_left_items(app: &App) -> Action {
| Mode::Confirm
| Mode::PreClone
| Mode::Clone
| Mode::PostClone => {
| Mode::PostClone
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetUserName => {
panic!("This shouldn't happen?")
}
};

View file

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

View file

@ -1,3 +1,18 @@
// 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;

View file

@ -188,5 +188,44 @@
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"ScanWinSources": {
"<Enter>": "Process",
"<b>": "FindWimBackups",
"<n>": "FindWimNetwork",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SelectWinSource": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<b>": "PrevScreen",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SelectWinImage": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<b>": "PrevScreen",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SetUserName": {
"<Esc>": "PrevScreen",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
},
"network_server": "SERVER",
"network_share": "SHARE",
"network_user": "USER",
"network_pass": "PASS"
}

View file

@ -55,6 +55,10 @@ pub enum Action {
OpenTerminal,
Restart,
Shutdown,
// App (Win-Installer)
FindWimBackups,
FindWimNetwork,
SetUserName(String),
// Screens
DismissPopup,
DisplayPopup(PopupType, String),

View file

@ -54,6 +54,14 @@ pub struct Config {
pub keybindings: KeyBindings,
#[serde(default)]
pub styles: Styles,
#[serde(default)]
pub network_server: String,
#[serde(default)]
pub network_share: String,
#[serde(default)]
pub network_user: String,
#[serde(default)]
pub network_pass: String,
}
pub static PROJECT_NAME: &str = "DEJA-VU";
@ -76,16 +84,14 @@ impl Config {
let config_dir = get_config_dir();
let mut builder = config::Config::builder()
.set_default("app_title", default_config.app_title.as_str())?
.set_default(
"clone_app_path",
String::from("C:\\Program Files\\Some Clone Tool\\app.exe"),
)?
.set_default(
"conemu_path",
String::from("C:\\Program Files\\ConEmu\\ConEmu64.exe"),
)?
.set_default("clone_app_path", default_config.app_title.as_str())?
.set_default("conemu_path", default_config.app_title.as_str())?
.set_default("config_dir", config_dir.to_str().unwrap())?
.set_default("data_dir", data_dir.to_str().unwrap())?;
.set_default("data_dir", data_dir.to_str().unwrap())?
.set_default("network_server", default_config.app_title.as_str())?
.set_default("network_share", default_config.app_title.as_str())?
.set_default("network_user", default_config.app_title.as_str())?
.set_default("network_pass", default_config.app_title.as_str())?;
let config_files = [
("config.json5", config::FileFormat::Json5),

View file

@ -42,6 +42,11 @@ pub enum Mode {
Clone,
SelectParts,
PostClone,
// Windows Installer
ScanWinSources,
SelectWinSource,
SelectWinImage,
SetUserName,
// WinPE
PEMenu,
}

View file

@ -129,7 +129,11 @@ impl App {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}
}
@ -154,7 +158,11 @@ impl App {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
};
if new_mode == self.cur_mode {
@ -461,9 +469,8 @@ impl App {
self.set_mode(new_mode)?;
self.action_tx
.send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?;
self.action_tx.send(build_left_items(self, self.cur_mode))?;
self.action_tx
.send(build_right_items(self, self.cur_mode))?;
self.action_tx.send(build_left_items(self))?;
self.action_tx.send(build_right_items(self))?;
match new_mode {
Mode::SelectTableType | Mode::Confirm => {
// Select source/dest disks
@ -634,16 +641,20 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}
}
fn build_left_items(app: &App, cur_mode: Mode) -> Action {
fn build_left_items(app: &App) -> Action {
let select_type: SelectionType;
let title: String;
let mut items = Vec::new();
let mut labels: Vec<String> = Vec::new();
match cur_mode {
match app.cur_mode {
Mode::Home => {
select_type = SelectionType::Loop;
title = String::from("Home");
@ -707,16 +718,20 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
};
Action::UpdateLeft(title, labels, items, select_type)
}
fn build_right_items(app: &App, cur_mode: Mode) -> Action {
fn build_right_items(app: &App) -> Action {
let mut items = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0;
match cur_mode {
match app.cur_mode {
Mode::InstallDrivers => {
items.push(vec![DVLine {
line_parts: vec![String::from("CPU")],

50
win_installer/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
# This file is part of Deja-Vu.
#
# Deja-Vu is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Deja-Vu is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
[package]
name = "win-installer"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024"
license = "GPL"
version = "0.1.0"
[dependencies]
core = { path = "../core" }
clap = { version = "4.4.5", features = [
"derive",
"cargo",
"wrap_help",
"unicode",
"string",
"unstable-styles",
] }
color-eyre = "0.6.3"
crossterm = { version = "0.28.1", features = ["event-stream"] }
futures = "0.3.30"
ratatui = "0.29.0"
serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] }
toml = "0.8.13"
tracing = "0.1.41"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tempfile = "3.23.0"
windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] }
xml = "1.1.0"
tui-input = "0.14.0"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

28
win_installer/build.rs Normal file
View file

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

844
win_installer/src/app.rs Normal file
View file

@ -0,0 +1,844 @@
// 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,
footer::Footer,
left::{Left, SelectionType},
popup,
right::Right,
title::Title,
},
config::Config,
line::{DVLine, get_disk_description_right},
state::Mode,
system::{cpu::get_cpu_name, disk::PartitionTableType, drivers},
tasks::{TaskType, Tasks},
tui::{Event, Tui},
};
use std::{
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::{
components::{set_username::InputUsername, wim_scan::WimScan},
state::{ScanType, 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
state: State,
cur_mode: Mode,
tasks: Tasks,
}
impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let config = Config::new()?;
let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
let state = State::new(config.clone(), disk_list_arc);
let wim_sources = Arc::clone(&state.wim_sources);
Ok(Self {
// TUI
action_rx,
action_tx,
components: vec![
Box::new(Title::new("Windows Install Tool")),
Box::new(Left::new()),
Box::new(Right::new()),
Box::new(WimScan::new(wim_sources)),
Box::new(InputUsername::new()),
Box::new(Footer::new()),
Box::new(popup::Popup::new()),
],
config,
frame_rate,
last_tick_key_events: Vec::new(),
should_quit: false,
should_suspend: false,
tick_rate,
// App
cur_mode: Mode::default(),
state,
tasks,
})
}
pub fn next_mode(&mut self) -> Mode {
match self.cur_mode {
Mode::Home | Mode::InstallDrivers => Mode::ScanDisks,
Mode::ScanDisks => Mode::SelectDisks,
Mode::SelectDisks => Mode::SelectTableType,
Mode::SelectTableType => Mode::ScanWinSources,
Mode::ScanWinSources => Mode::SelectWinSource,
Mode::SelectWinSource => Mode::SelectWinImage,
Mode::SelectWinImage => Mode::SetUserName,
Mode::SetUserName => Mode::Confirm,
Mode::Confirm => Mode::Process, // i.e. format, apply, etc
Mode::Process | Mode::Done => Mode::Done,
Mode::Failed => Mode::Failed,
// Invalid States
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::Clone
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::PreClone
| Mode::PostClone
| Mode::SelectParts
| Mode::SetBootMode => panic!("This shouldn't happen?"),
}
}
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::InstallDrivers => self.state.scan_drivers(),
// Mode::Process => {
// self.action_tx
// .send(Action::DisplayPopup(popup::Type::Info, String::from("...")))?;
// }
Mode::ScanDisks => {
self.state.reset_all();
if self.tasks.idle() {
self.tasks.add(TaskType::ScanDisks);
}
self.action_tx.send(Action::DisplayPopup(
popup::Type::Info,
String::from("Scanning Disks..."),
))?;
}
Mode::ScanWinSources => {
self.state.reset_all();
self.state.scan_wim_local(ScanType::WindowsInstallers);
}
Mode::SetUserName => {
if let Ok(wim_sources) = self.state.wim_sources.lock()
&& let Some(index) = self.state.wim_image_index
{
let image = wim_sources.get_file(index);
if !image.is_setup {
self.action_tx.send(Action::NextScreen)?;
}
}
}
Mode::Done => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?;
}
_ => {}
}
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();
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)
self.tasks.poll()?;
if let Ok(mut wim_sources) = self.state.wim_sources.lock() {
wim_sources.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::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?,
Action::InstallDriver => {
self.action_tx.send(Action::SetMode(Mode::InstallDrivers))?;
}
Action::FindWimBackups => {
self.state.reset_local();
self.state.scan_wim_local(ScanType::GeneralWimFiles);
}
Action::FindWimNetwork => {
self.state.reset_network();
self.state.scan_wim_network();
}
Action::NextScreen => {
let next_mode = self.next_mode();
self.action_tx.send(Action::DismissPopup)?;
self.action_tx.send(Action::SetMode(next_mode))?;
}
Action::PrevScreen => match self.cur_mode {
Mode::SelectTableType => {
self.action_tx.send(Action::SetMode(Mode::SelectDisks))?;
}
Mode::SelectWinSource => {
self.action_tx.send(Action::SetMode(Mode::ScanWinSources))?;
}
Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => {
self.action_tx
.send(Action::SetMode(Mode::SelectWinSource))?;
}
_ => {}
},
Action::Process => match self.cur_mode {
Mode::Confirm | Mode::ScanWinSources => {
self.action_tx.send(Action::NextScreen)?;
}
Mode::Done => {
self.action_tx.send(Action::Quit)?;
}
_ => {}
},
Action::ScanDisks => self.action_tx.send(Action::SetMode(Mode::ScanDisks))?,
Action::Select(one, _two) => match self.cur_mode {
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::SelectDisks => {
self.state.disk_index_dest = one;
}
Mode::SelectTableType => {
self.state.table_type = {
if let Some(index) = one {
match index {
0 => Some(PartitionTableType::Guid),
1 => Some(PartitionTableType::Legacy),
index => {
panic!("Failed to select PartitionTableType: {index}")
}
}
} else {
None
}
}
}
Mode::SelectWinSource => {
self.state.wim_file_index = one;
}
Mode::SelectWinImage => {
self.state.wim_image_index = one;
}
_ => {}
},
Action::SetMode(mode) => {
self.set_mode(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::SetUserName(ref name) => {
self.state.username = Some(name.clone());
self.action_tx.send(Action::NextScreen)?;
}
Action::TasksComplete => 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 render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
if let [header, _body, footer, center, left, right, username, popup] =
get_chunks(frame.area())[..]
{
let component_areas = vec![header, center, left, right, username, 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 build_footer_string(cur_mode: Mode) -> String {
match cur_mode {
Mode::Home | Mode::ScanDisks => String::from("(q) to quit"),
Mode::InstallDrivers => String::from("(Enter) to select / (q) to quit"),
Mode::SelectDisks => String::from(
"(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::SelectWinSource | Mode::SelectWinImage => {
String::from("(Enter) to select / (b) to go back / (q) to quit")
}
Mode::ScanWinSources => String::from(
"(Enter) to continue / (b) to scan for backups / (n) to scan network / (q) to quit",
),
Mode::SetUserName => String::from("(Enter) to continue / (Esc) to go back"),
Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"),
Mode::Done | Mode::Failed | Mode::Process => String::from("(Enter) or (q) to quit"),
// Invalid States
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::Clone
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::PreClone
| Mode::PostClone
| Mode::SelectParts
| Mode::SetBootMode => panic!("This shouldn't happen?"),
}
}
fn build_left_items(app: &App) -> Action {
let select_type: SelectionType;
let title: String;
let mut items = Vec::new();
let mut labels: Vec<String> = Vec::new();
match app.cur_mode {
Mode::Home => {
select_type = SelectionType::Loop;
title = String::from("Home");
}
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::Process => {
select_type = SelectionType::Loop;
title = String::from("Processing");
// TODO: FIXME
}
Mode::ScanWinSources => {
select_type = SelectionType::Loop;
title = String::from("Scanning");
// TODO: FIXME
}
Mode::SelectWinSource => {
select_type = SelectionType::One;
title = String::from("Select Windows Source");
if let Ok(wim_sources) = app.state.wim_sources.lock() {
wim_sources
.get_file_list()
.iter()
.for_each(|wim_file| items.push(wim_file.path.clone()));
}
}
Mode::SelectWinImage | Mode::SetUserName => {
select_type = SelectionType::One;
title = String::from("Select Windows Image");
if let Ok(wim_sources) = app.state.wim_sources.lock()
&& let Some(index) = app.state.wim_file_index
{
wim_sources
.get_file(index)
.images
.iter()
.for_each(|image| items.push(format!("{image}")));
}
}
Mode::SelectDisks => {
select_type = SelectionType::One;
title = String::from("Select Destination Disk");
let disk_list = app.state.disk_list.lock().unwrap();
disk_list
.iter()
.for_each(|disk| items.push(disk.description.to_string()));
}
Mode::SelectTableType => {
select_type = SelectionType::One;
title = String::from("Select Partition Table Type");
items.push(format!("{}", PartitionTableType::Guid));
items.push(format!("{}", PartitionTableType::Legacy));
}
Mode::Confirm => {
select_type = SelectionType::Loop;
title = String::from("Confirm Selections");
}
Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => {
select_type = SelectionType::Loop;
title = String::from("Processing");
}
Mode::Done | Mode::Failed => {
select_type = SelectionType::Loop;
title = String::from("Done");
}
// Invalid states
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::SelectParts
| Mode::SetBootMode => panic!("This shouldn't happen?"),
};
Action::UpdateLeft(title, labels, items, select_type)
}
fn build_right_items(app: &App) -> Action {
let mut items = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0;
match app.cur_mode {
Mode::InstallDrivers => {
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],
}]);
start_index = 2;
}
Mode::SelectDisks => {
// Labels
let dest_dv_line = DVLine {
line_parts: vec![
String::from("Dest"),
String::from(" (WARNING: ALL DATA WILL BE DELETED!)"),
],
line_colors: vec![Color::Cyan, Color::Red],
};
if let Some(table_type) = &app.state.table_type {
// Show table type
let type_str = match table_type {
PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR",
};
labels.push(vec![
dest_dv_line,
DVLine {
line_parts: vec![format!(" (Will be formatted {type_str})")],
line_colors: vec![Color::Yellow],
},
]);
} else {
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::SelectWinSource => {
// Disk Info
let type_str = match app.state.table_type.clone().unwrap() {
PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR",
};
let mut label_dv_lines = vec![
DVLine {
line_parts: vec![
String::from("Dest"),
String::from(" (WARNING: ALL DATA WILL BE DELETED!)"),
],
line_colors: vec![Color::Cyan, Color::Red],
},
DVLine {
line_parts: vec![format!(" (Will be formatted {type_str})")],
line_colors: vec![Color::Yellow],
},
DVLine::blank(),
];
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)
{
get_disk_description_right(disk, &None)
.into_iter()
.for_each(|dv_line| label_dv_lines.push(dv_line));
}
labels.push(label_dv_lines);
// WIM Info
if let Ok(wim_sources) = app.state.wim_sources.lock() {
wim_sources.get_file_list().iter().for_each(|source| {
let mut wim_dv_lines = vec![
DVLine {
line_parts: vec![String::from("WIM Info")],
line_colors: vec![Color::Cyan],
},
DVLine {
line_parts: vec![source.path.clone()],
line_colors: vec![Color::Reset],
},
DVLine::blank(),
DVLine {
line_parts: vec![String::from("Images")],
line_colors: vec![Color::Blue],
},
DVLine::blank(),
];
source.images.iter().for_each(|image| {
wim_dv_lines.push(DVLine {
line_parts: vec![format!("{image}")],
line_colors: vec![Color::Reset],
})
});
items.push(wim_dv_lines);
});
}
}
Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => {
info!("Building right items for: {:?}", &app.cur_mode);
let wim_file;
if let Ok(wim_sources) = app.state.wim_sources.lock()
&& let Some(index) = app.state.wim_file_index
{
wim_file = wim_sources.get_file(index);
} else {
panic!("Failed to get source WIM file");
}
// Disk Info
let type_str = match app.state.table_type.clone().unwrap() {
PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR",
};
let mut label_dv_lines = vec![
DVLine {
line_parts: vec![
String::from("Dest"),
String::from(" (WARNING: ALL DATA WILL BE DELETED!)"),
],
line_colors: vec![Color::Cyan, Color::Red],
},
DVLine {
line_parts: vec![format!(" (Will be formatted {type_str})")],
line_colors: vec![Color::Yellow],
},
DVLine::blank(),
];
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)
{
get_disk_description_right(disk, &None)
.into_iter()
.for_each(|dv_line| label_dv_lines.push(dv_line));
}
label_dv_lines.append(&mut vec![
DVLine::blank(),
DVLine {
line_parts: vec![String::from("WIM Info")],
line_colors: vec![Color::Cyan],
},
DVLine {
line_parts: vec![wim_file.path.clone()],
line_colors: vec![Color::Reset],
},
DVLine::blank(),
DVLine {
line_parts: vec![String::from("Image")],
line_colors: vec![Color::Blue],
},
]);
// WIM Info
match app.cur_mode {
Mode::SelectWinImage => {
wim_file.images.iter().for_each(|image| {
items.push(vec![DVLine {
line_parts: vec![format!("{image}")],
line_colors: vec![Color::Reset],
}])
});
}
Mode::Confirm => {
if let Some(index) = app.state.wim_image_index
&& let Some(image) = wim_file.images.get(index)
{
label_dv_lines.append(&mut vec![
DVLine {
line_parts: vec![format!("{image}")],
line_colors: vec![Color::Reset],
},
DVLine::blank(),
]);
}
if wim_file.is_setup
&& let Some(username) = &app.state.username
{
label_dv_lines.append(&mut vec![DVLine {
line_parts: vec![String::from("Username: "), username.clone()],
line_colors: vec![Color::Green, Color::Reset],
}]);
}
items.push(vec![DVLine::blank()]);
}
_ => {}
}
// Done
labels.push(label_dv_lines);
}
Mode::SelectTableType => {
// Labels
let dest_dv_line = DVLine {
line_parts: vec![
String::from("Dest"),
String::from(" (WARNING: ALL DATA WILL BE DELETED!)"),
],
line_colors: vec![Color::Cyan, Color::Red],
};
if let Some(table_type) = &app.state.table_type {
// Show table type
let type_str = match table_type {
PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR",
};
labels.push(vec![
dest_dv_line,
DVLine {
line_parts: vec![format!(" (Will be formatted {type_str})")],
line_colors: vec![Color::Yellow],
},
]);
} else {
labels.push(vec![dest_dv_line]);
}
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)
{
items.push(get_disk_description_right(disk, &None));
}
}
_ => {}
}
Action::UpdateRight(labels, start_index, items)
}
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(),
);
let center = centered_rect(90, 90, chunks[1]);
// Left/Right
chunks.extend(
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(center)
.to_vec(),
);
// Center
chunks.push(center);
// Set username
chunks.push(centered_rect(60, 20, r));
// Popup
chunks.push(centered_rect(60, 25, r));
// Done
chunks
}

View file

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

View file

@ -0,0 +1,135 @@
// This file is part of Deja-Vu.
//
// Deja-Vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-Vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
use color_eyre::Result;
use core::{action::Action, components::Component, config::Config, state::Mode, tui::Event};
use crossterm::event::KeyCode;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use tui_input::{Input, InputRequest};
#[derive(Default)]
pub struct InputUsername {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
input: Input,
mode: Mode,
}
impl InputUsername {
#[must_use]
pub fn new() -> Self {
Self {
input: Input::new(String::from("")),
..Default::default()
}
}
}
impl Component for InputUsername {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
if self.mode != Mode::SetUserName {
return Ok(None);
}
let action = match event {
Some(Event::Key(key_event)) => match key_event.code {
KeyCode::Backspace => {
self.input.handle(InputRequest::DeletePrevChar);
None
}
KeyCode::Char(c) => {
let ok_chars: Vec<char> = vec![' ', '-', '_'];
if c.is_ascii_alphanumeric() || ok_chars.contains(&c) {
self.input.handle(InputRequest::InsertChar(c));
}
None
}
KeyCode::Enter => {
let username = self.input.value();
Some(Action::SetUserName(String::from(username)))
}
KeyCode::Esc => Some(Action::SetMode(Mode::Home)),
_ => None,
},
Some(Event::Mouse(_)) => None,
_ => None,
};
Ok(action)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::SetMode(mode) => self.mode = mode.clone(),
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::SetUserName {
// Bail early
return Ok(());
}
// Set areas
let [_, center_area, _] = Layout::horizontal([
Constraint::Length(1),
Constraint::Min(1),
Constraint::Length(1),
])
.areas(area);
let [_, input_area, _] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Length(1),
])
.areas(center_area);
frame.render_widget(Clear, area);
let outer_block = Block::bordered().cyan().bold();
frame.render_widget(outer_block, area);
// Input Box
let width = input_area.width.max(3) - 3; // keep 2 for borders and 1 for cursor
let scroll = self.input.visual_scroll(width as usize);
let input = Paragraph::new(self.input.value())
.scroll((0, scroll as u16))
.white()
.block(Block::new().borders(Borders::ALL).title("Enter Username"));
frame.render_widget(input, input_area);
// Ratatui hides the cursor unless it's explicitly set. Position the cursor past the
// end of the input text and one line down from the border to the input line
let x = self.input.visual_cursor().max(scroll) - scroll + 1;
frame.set_cursor_position((input_area.x + x as u16, input_area.y + 1));
// Done
Ok(())
}
}

View file

@ -0,0 +1,154 @@
// This file is part of Deja-Vu.
//
// Deja-Vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-Vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
use core::{action::Action, components::Component, config::Config, state::Mode};
use std::{
iter::zip,
sync::{Arc, Mutex},
};
use color_eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, List, ListItem, Padding, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use crate::wim::WimSources;
#[derive(Default)]
pub struct WimScan {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
mode: Mode,
scan_network: bool,
wim_sources: Arc<Mutex<WimSources>>,
}
impl WimScan {
#[must_use]
pub fn new(wim_sources: Arc<Mutex<WimSources>>) -> Self {
let wim_sources = wim_sources.clone();
Self {
wim_sources,
..Default::default()
}
}
}
impl Component for WimScan {
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
let _ = key; // to appease clippy
Ok(None)
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::FindWimNetwork => self.scan_network = true,
Action::SetMode(new_mode) => {
self.mode = new_mode;
}
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::ScanWinSources {
return Ok(());
}
frame.render_widget(Clear, area);
// Prep
let [left, right] = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(area);
let [left_title, left_body] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.areas(left);
let [right_title, right_body] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.areas(right);
// Titles
let titles = vec![
Paragraph::new(Line::from("Local").centered())
.block(Block::default().borders(Borders::NONE)),
Paragraph::new(Line::from("Network").centered())
.block(Block::default().borders(Borders::NONE)),
];
for (title, area) in zip(titles, [left_title, right_title]) {
frame.render_widget(title, area);
}
// WIM Info
if let Ok(wim_sources) = self.wim_sources.lock() {
// Local
let mut left_list = Vec::new();
if wim_sources.thread_local.is_some() {
left_list.push(ListItem::new("Scanning..."));
} else {
left_list.extend(
wim_sources
.local
.iter()
.map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))),
);
}
let left_list = List::new(left_list).block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(1, 1, 1, 1)),
);
frame.render_widget(left_list, left_body);
// Network
let mut right_list = Vec::new();
if wim_sources.thread_network.is_some() {
right_list.push(ListItem::new("Scanning..."));
} else {
right_list.extend(wim_sources.network.iter().map(|wimfile| {
ListItem::new(format!(
"{}\n\n",
wimfile.path.split("\\").last().unwrap_or(&wimfile.path)
))
}));
}
let right_list = List::new(right_list).block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(1, 1, 1, 1)),
);
frame.render_widget(right_list, right_body);
}
// Done
Ok(())
}
}

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

@ -0,0 +1,36 @@
// This file is part of Deja-Vu.
//
// Deja-Vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-Vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
use clap::Parser;
use color_eyre::Result;
use crate::app::App;
mod app;
mod components;
mod net;
mod state;
mod wim;
#[tokio::main]
async fn main() -> Result<()> {
core::errors::init()?;
core::logging::init()?;
let args = core::cli::Cli::parse();
let mut app = App::new(args.tick_rate, args.frame_rate)?;
app.run().await?;
Ok(())
}

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

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

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

@ -0,0 +1,216 @@
// 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.disk_index_dest = None;
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_setup = 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_setup)
{
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, true)
// 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));
}
}

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

@ -0,0 +1,323 @@
// 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;
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_setup: 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 num_local = self.local.len();
let index = if index < num_local {
index
} else {
index - num_local
};
self.local.get(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();
}
}
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_setup: 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_setup,
};
Ok(wim_file)
}