From 94faae27ace8aa2e2f9952cc88f6ce4f106339a1 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 8 Nov 2025 16:52:09 -0800 Subject: [PATCH 01/27] Add initial base for win-installer --- Cargo.lock | 32 +++- Cargo.toml | 4 +- win_installer/Cargo.toml | 48 ++++++ win_installer/build.rs | 28 ++++ win_installer/src/app.rs | 315 +++++++++++++++++++++++++++++++++++++ win_installer/src/main.rs | 34 ++++ win_installer/src/state.rs | 46 ++++++ win_installer/src/wim.rs | 218 +++++++++++++++++++++++++ 8 files changed, 721 insertions(+), 4 deletions(-) create mode 100644 win_installer/Cargo.toml create mode 100644 win_installer/build.rs create mode 100644 win_installer/src/app.rs create mode 100644 win_installer/src/main.rs create mode 100644 win_installer/src/state.rs create mode 100644 win_installer/src/wim.rs diff --git a/Cargo.lock b/Cargo.lock index e156095..ab28626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2661,9 +2661,9 @@ 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", @@ -3126,6 +3126,28 @@ 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", + "vergen-gix", + "xml", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3354,6 +3376,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" diff --git a/Cargo.toml b/Cargo.toml index 7dc2384..0a6700c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,6 @@ # along with Deja-Vu. If not, see . [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" diff --git a/win_installer/Cargo.toml b/win_installer/Cargo.toml new file mode 100644 index 0000000..d7bcc76 --- /dev/null +++ b/win_installer/Cargo.toml @@ -0,0 +1,48 @@ +# This file is part of Deja-Vu. +# +# Deja-Vu is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Deja-Vu is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Deja-Vu. If not, see . + +[package] +name = "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" +xml = "1.1.0" + +[build-dependencies] +anyhow = "1.0.86" +vergen-gix = { version = "1.0.0", features = ["build", "cargo"] } diff --git a/win_installer/build.rs b/win_installer/build.rs new file mode 100644 index 0000000..8988d39 --- /dev/null +++ b/win_installer/build.rs @@ -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 . +// +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() +} diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs new file mode 100644 index 0000000..da38f04 --- /dev/null +++ b/win_installer/src/app.rs @@ -0,0 +1,315 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// +use core::{ + action::Action, + components::{Component, footer::Footer, left::Left, popup, right::Right, title::Title}, + config::Config, + state::Mode, + tasks::{TaskType, Tasks}, + tui::{Event, Tui}, +}; +use std::{ + iter::zip, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use color_eyre::Result; +use ratatui::{ + crossterm::event::KeyEvent, + layout::{Constraint, Direction, Layout}, + prelude::Rect, +}; +use tokio::sync::mpsc; +use tracing::{debug, info}; + +use crate::state::State; + +pub struct App<'a> { + // TUI + action_rx: mpsc::UnboundedReceiver, + action_tx: mpsc::UnboundedSender, + components: Vec>, + config: Config, + frame_rate: f64, + last_tick_key_events: Vec, + should_quit: bool, + should_suspend: bool, + tick_rate: f64, + // App + state: State<'a>, + mode: Mode, + tasks: Tasks, +} + +impl App<'_> { + pub fn new(tick_rate: f64, frame_rate: f64) -> Result { + let (action_tx, action_rx) = mpsc::unbounded_channel(); + let disk_list_arc = Arc::new(Mutex::new(Vec::new())); + let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); + 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(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 + mode: Mode::default(), + state: State::new(disk_list_arc), + tasks, + }) + } + + 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::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))?; + } + _ => {} + } + 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 { + let mut chunks: Vec = 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 + chunks +} diff --git a/win_installer/src/main.rs b/win_installer/src/main.rs new file mode 100644 index 0000000..88a0f38 --- /dev/null +++ b/win_installer/src/main.rs @@ -0,0 +1,34 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// +use clap::Parser; +use color_eyre::Result; + +use crate::app::App; + +mod app; +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(()) +} diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs new file mode 100644 index 0000000..48a0086 --- /dev/null +++ b/win_installer/src/state.rs @@ -0,0 +1,46 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// + +use std::sync::{Arc, Mutex}; + +use core::system::{disk::Disk, drivers}; + +use crate::wim::WimSources; + +#[derive(Debug, Default)] +pub struct State<'a> { + pub disk_index_dest: Option, + pub disk_list: Arc>>, + pub driver: Option, + pub driver_list: Vec, + pub part_index_boot: Option, + pub wim_file_index: Option, + pub wim_image_index: Option, + pub wim_sources: Arc>>, +} + +impl State<'_> { + pub fn new(disk_list: Arc>>) -> Self { + State { + disk_list, + ..Default::default() + } + } + + pub fn scan_drivers(&mut self) { + self.driver_list = drivers::scan(); + } +} diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs new file mode 100644 index 0000000..96cea57 --- /dev/null +++ b/win_installer/src/wim.rs @@ -0,0 +1,218 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// +use std::{ + collections::HashMap, fmt, fs::File, io::BufReader, path::Path, process::Command, + sync::LazyLock, +}; + +use tempfile::NamedTempFile; +use xml::reader::{EventReader, XmlEvent}; + +static WIN_BUILDS: LazyLock> = 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(Debug)] +struct WimFile<'a> { + path: &'a Path, + images: Vec, +} + +#[derive(Clone, Debug, Default)] +struct WimImage { + build: String, + index: String, + name: String, + spbuild: String, + 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) + } +} + +#[derive(Debug, Default)] +pub struct WimSources<'a> { + files: Vec>, +} + +impl WimSources<'_> { + pub fn new() -> Self { + Default::default() + } +} + +fn get_wim_xml(wim_file: &str) -> std::io::Result { + let tmp_file = NamedTempFile::new()?; + let _ = Command::new("wiminfo") + .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) +} + +fn parse_wim_file(wim_file: &str) -> std::io::Result> { + let mut wim_images: Vec = Vec::new(); + let wim_path = Path::new(wim_file); + if !wim_path.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); + let mut depth = 0; + for e in parser { + match e { + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + println!("{:spaces$}+{name}", "", spaces = depth * 2); + depth += 1; + 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(); + println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2); + } + if current_element == "NAME" { + image.name = char_data.trim().to_string(); + println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2); + } + if current_element == "SPBUILD" { + image.spbuild = char_data.trim().to_string(); + println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2); + } + } + Ok(XmlEvent::EndElement { name }) => { + depth -= 1; + println!("{:spaces$}-{name}", "", spaces = depth * 2); + + if name.local_name.to_uppercase() == "IMAGE" { + // Append image to list + if !image.build.is_empty() && !image.name.is_empty() && !image.index.is_empty() + { + wim_images.push(image.clone()); + } + + // Reset image + image.reset() + } + } + Err(e) => { + eprintln!("Error: {e}"); + break; + } + _ => {} + } + } + let wim_file = WimFile { + path: wim_path, + images: wim_images, + }; + + Ok(wim_file) +} + +// fn main() -> std::io::Result<()> { +// let mut sources = WimSources::new(); +// if let Ok(wim_file) = parse_wim_file("./23H2.wim") +// && !wim_file.images.is_empty() +// { +// sources.files.push(wim_file); +// } +// if let Ok(wim_file) = parse_wim_file("./24H2.wim") { +// sources.files.push(wim_file); +// } +// dbg!(&sources); +// sources.files.iter().for_each(|f| { +// println!("-- {} --", f.path.to_string_lossy()); +// f.images.iter().for_each(|i| { +// println!("* {i}"); +// }); +// println!(); +// }); +// +// Ok(()) +// } From 69c3feb838e90b141579687825991fc785a98ed4 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 8 Nov 2025 17:48:00 -0800 Subject: [PATCH 02/27] Update WIM structs --- win_installer/src/app.rs | 5 ++++- win_installer/src/state.rs | 10 ++++++++++ win_installer/src/wim.rs | 18 ++++++++++++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index da38f04..d9f6e6b 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -102,7 +102,7 @@ impl App<'_> { } let action_tx = self.action_tx.clone(); - action_tx.send(Action::SetMode(Mode::PEMenu))?; + action_tx.send(Action::SetMode(Mode::ScanDisks))?; loop { self.handle_events(&mut tui).await?; self.handle_actions(&mut tui)?; @@ -219,6 +219,9 @@ impl App<'_> { Action::Render => self.render(tui)?, Action::SetMode(mode) => { self.mode = mode; + if self.mode == Mode::ScanDisks { + self.state.reset(); + } self.action_tx.send(Action::UpdateFooter(String::from( "(Enter) to select / (t) for terminal / (p) to power off / (r) to restart", )))?; diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 48a0086..a52efd6 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -40,6 +40,16 @@ impl State<'_> { } } + pub fn reset(&mut self) { + self.disk_index_dest = None; + self.part_index_boot = None; + self.wim_file_index = None; + self.wim_image_index = None; + if let Ok(mut sources) = self.wim_sources.lock() { + sources.reset(); + } + } + pub fn scan_drivers(&mut self) { self.driver_list = drivers::scan(); } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 96cea57..3d464c0 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -48,13 +48,13 @@ static WIN_BUILDS: LazyLock> = LazyLock::new(|| { }); #[derive(Debug)] -struct WimFile<'a> { +pub struct WimFile<'a> { path: &'a Path, images: Vec, } #[derive(Clone, Debug, Default)] -struct WimImage { +pub struct WimImage { build: String, index: String, name: String, @@ -90,13 +90,19 @@ impl fmt::Display for WimImage { #[derive(Debug, Default)] pub struct WimSources<'a> { - files: Vec>, + pub local: Vec>, + pub network: Vec>, } impl WimSources<'_> { pub fn new() -> Self { Default::default() } + + pub fn reset(&mut self) { + self.local.clear(); + self.network.clear(); + } } fn get_wim_xml(wim_file: &str) -> std::io::Result { @@ -200,13 +206,13 @@ fn parse_wim_file(wim_file: &str) -> std::io::Result> { // if let Ok(wim_file) = parse_wim_file("./23H2.wim") // && !wim_file.images.is_empty() // { -// sources.files.push(wim_file); +// sources.local.push(wim_file); // } // if let Ok(wim_file) = parse_wim_file("./24H2.wim") { -// sources.files.push(wim_file); +// sources.local.push(wim_file); // } // dbg!(&sources); -// sources.files.iter().for_each(|f| { +// sources.local.iter().for_each(|f| { // println!("-- {} --", f.path.to_string_lossy()); // f.images.iter().for_each(|i| { // println!("* {i}"); From cf87ac32af095a8be4cece0e28b2038338107009 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 8 Nov 2025 17:48:17 -0800 Subject: [PATCH 03/27] Update Modes for win-installer --- boot_diags/src/app.rs | 10 ++++++++-- core/src/state.rs | 4 ++++ deja_vu/src/app.rs | 20 ++++++++++++++++---- win_installer/src/app.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/boot_diags/src/app.rs b/boot_diags/src/app.rs index dc68ffb..d3e9e89 100644 --- a/boot_diags/src/app.rs +++ b/boot_diags/src/app.rs @@ -657,7 +657,10 @@ fn build_footer_string(cur_mode: Mode) -> String { | Mode::PEMenu | Mode::PreClone | Mode::PostClone - | Mode::SelectTableType => { + | Mode::ScanWinImages + | Mode::SelectTableType + | Mode::SelectWinImage + | Mode::SetUserName => { panic!("This shouldn't happen?") } } @@ -770,7 +773,10 @@ fn build_left_items(app: &App) -> Action { | Mode::Confirm | Mode::PreClone | Mode::Clone - | Mode::PostClone => { + | Mode::PostClone + | Mode::ScanWinImages + | Mode::SelectWinImage + | Mode::SetUserName => { panic!("This shouldn't happen?") } }; diff --git a/core/src/state.rs b/core/src/state.rs index d05eaf5..c0bcadd 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -42,6 +42,10 @@ pub enum Mode { Clone, SelectParts, PostClone, + // Windows Installer + ScanWinImages, + SelectWinImage, + SetUserName, // WinPE PEMenu, } diff --git a/deja_vu/src/app.rs b/deja_vu/src/app.rs index 34afb20..2b2d516 100644 --- a/deja_vu/src/app.rs +++ b/deja_vu/src/app.rs @@ -129,7 +129,10 @@ impl App { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::SetBootMode => panic!("This shouldn't happen?"), + | Mode::ScanWinImages + | Mode::SelectWinImage + | Mode::SetBootMode + | Mode::SetUserName => panic!("This shouldn't happen?"), } } @@ -154,7 +157,10 @@ impl App { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::SetBootMode => panic!("This shouldn't happen?"), + | Mode::ScanWinImages + | Mode::SelectWinImage + | Mode::SetBootMode + | Mode::SetUserName => panic!("This shouldn't happen?"), }; if new_mode == self.cur_mode { @@ -634,7 +640,10 @@ fn build_footer_string(cur_mode: Mode) -> String { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::SetBootMode => panic!("This shouldn't happen?"), + | Mode::ScanWinImages + | Mode::SelectWinImage + | Mode::SetBootMode + | Mode::SetUserName => panic!("This shouldn't happen?"), } } @@ -707,7 +716,10 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::SetBootMode => panic!("This shouldn't happen?"), + | Mode::ScanWinImages + | Mode::SelectWinImage + | Mode::SetBootMode + | Mode::SetUserName => panic!("This shouldn't happen?"), }; Action::UpdateLeft(title, labels, items, select_type) } diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index d9f6e6b..aafa217 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -84,6 +84,34 @@ impl App<'_> { }) } + pub fn next_mode(&mut self) -> Mode { + match self.mode { + Mode::Home | Mode::InstallDrivers => Mode::ScanDisks, + Mode::ScanDisks => Mode::SelectDisks, + Mode::SelectDisks => Mode::SelectTableType, + Mode::SelectTableType => Mode::ScanWinImages, + Mode::ScanWinImages => 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 async fn run(&mut self) -> Result<()> { let mut tui = Tui::new()? // .mouse(true) // uncomment this line to enable mouse support From f51a4e85c48e184e9cb41759ad1d571c73021b13 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 8 Nov 2025 18:43:50 -0800 Subject: [PATCH 04/27] Add more framework for workflow --- config/config.json5 | 15 +++++++++++++++ core/src/action.rs | 2 ++ win_installer/src/app.rs | 17 +++++++++++++++-- win_installer/src/state.rs | 10 ++++++++-- win_installer/src/wim.rs | 6 +++++- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/config/config.json5 b/config/config.json5 index 81c4ee1..1ad0168 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -188,5 +188,20 @@ "": "Quit", "": "Suspend" }, + "ScanWinImages": { + "": "Process", + "": "FindWimNetwork", + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" + }, + "SetUserName": { + "": "Process", + "": "Process", + "": "Quit", + "": "Quit", + "": "Suspend" + }, }, } diff --git a/core/src/action.rs b/core/src/action.rs index 34bf83a..7675fd5 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -55,6 +55,8 @@ pub enum Action { OpenTerminal, Restart, Shutdown, + // App (Win-Installer) + FindWimNetwork, // Screens DismissPopup, DisplayPopup(PopupType, String), diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index aafa217..587ba2a 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -245,10 +245,23 @@ impl App<'_> { // } Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, + Action::FindWimNetwork => { + self.state.reset_network(); + // TODO: Actually scan network! + } Action::SetMode(mode) => { self.mode = mode; - if self.mode == Mode::ScanDisks { - self.state.reset(); + match mode { + Mode::Done => { + self.action_tx.send(Action::DisplayPopup( + popup::Type::Success, + popup::fortune(), + ))?; + } + Mode::ScanDisks => { + self.state.reset_all(); + } + _ => {} } self.action_tx.send(Action::UpdateFooter(String::from( "(Enter) to select / (t) for terminal / (p) to power off / (r) to restart", diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index a52efd6..6156c61 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -40,13 +40,19 @@ impl State<'_> { } } - pub fn reset(&mut self) { + pub fn reset_all(&mut self) { self.disk_index_dest = None; self.part_index_boot = None; self.wim_file_index = None; self.wim_image_index = None; if let Ok(mut sources) = self.wim_sources.lock() { - sources.reset(); + sources.reset_all(); + } + } + + pub fn reset_network(&mut self) { + if let Ok(mut sources) = self.wim_sources.lock() { + sources.reset_network(); } } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 3d464c0..6a8326f 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -99,10 +99,14 @@ impl WimSources<'_> { Default::default() } - pub fn reset(&mut self) { + pub fn reset_all(&mut self) { self.local.clear(); self.network.clear(); } + + pub fn reset_network(&mut self) { + self.network.clear(); + } } fn get_wim_xml(wim_file: &str) -> std::io::Result { From e0932c7b483371fb708cad0e7ee561a5154db385 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 8 Nov 2025 21:46:31 -0800 Subject: [PATCH 05/27] Add first few screens --- boot_diags/src/components.rs | 15 ++ boot_diags/src/scan.rs | 15 ++ deja_vu/src/app.rs | 13 +- win_installer/src/app.rs | 297 ++++++++++++++++++++--- win_installer/src/components.rs | 16 ++ win_installer/src/components/wim_scan.rs | 186 ++++++++++++++ win_installer/src/main.rs | 1 + win_installer/src/state.rs | 14 +- win_installer/src/wim.rs | 37 ++- 9 files changed, 523 insertions(+), 71 deletions(-) create mode 100644 win_installer/src/components.rs create mode 100644 win_installer/src/components/wim_scan.rs diff --git a/boot_diags/src/components.rs b/boot_diags/src/components.rs index 6454104..928e99a 100644 --- a/boot_diags/src/components.rs +++ b/boot_diags/src/components.rs @@ -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 . +// pub mod logview; pub mod progress; diff --git a/boot_diags/src/scan.rs b/boot_diags/src/scan.rs index 7d9354b..1e80185 100644 --- a/boot_diags/src/scan.rs +++ b/boot_diags/src/scan.rs @@ -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 . +// use color_eyre::Result; use core::system::disk::PartitionTableType; use core::tasks::Tasks; diff --git a/deja_vu/src/app.rs b/deja_vu/src/app.rs index 2b2d516..75194ad 100644 --- a/deja_vu/src/app.rs +++ b/deja_vu/src/app.rs @@ -467,9 +467,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 @@ -647,12 +646,12 @@ fn build_footer_string(cur_mode: Mode) -> String { } } -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 = Vec::new(); - match cur_mode { + match app.cur_mode { Mode::Home => { select_type = SelectionType::Loop; title = String::from("Home"); @@ -724,11 +723,11 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action { 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::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")], diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 587ba2a..3c77bc7 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -15,15 +15,23 @@ // use core::{ action::Action, - components::{Component, footer::Footer, left::Left, popup, right::Right, title::Title}, + 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, - path::PathBuf, sync::{Arc, Mutex}, }; @@ -32,13 +40,14 @@ use ratatui::{ crossterm::event::KeyEvent, layout::{Constraint, Direction, Layout}, prelude::Rect, + style::Color, }; use tokio::sync::mpsc; use tracing::{debug, info}; -use crate::state::State; +use crate::{components::wim_scan::WimScan, state::State, wim::parse_wim_file}; -pub struct App<'a> { +pub struct App { // TUI action_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, @@ -50,16 +59,18 @@ pub struct App<'a> { should_suspend: bool, tick_rate: f64, // App - state: State<'a>, - mode: Mode, + state: State, + cur_mode: Mode, tasks: Tasks, } -impl App<'_> { +impl App { pub fn new(tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); 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(disk_list_arc); + let wim_sources = Arc::clone(&state.wim_sources); Ok(Self { // TUI action_rx, @@ -68,6 +79,7 @@ impl App<'_> { Box::new(Title::new("Windows Install Tool")), Box::new(Left::new()), Box::new(Right::new()), + Box::new(WimScan::new(wim_sources)), Box::new(Footer::new()), Box::new(popup::Popup::new()), ], @@ -78,14 +90,14 @@ impl App<'_> { should_suspend: false, tick_rate, // App - mode: Mode::default(), - state: State::new(disk_list_arc), + cur_mode: Mode::default(), + state, tasks, }) } pub fn next_mode(&mut self) -> Mode { - match self.mode { + match self.cur_mode { Mode::Home | Mode::InstallDrivers => Mode::ScanDisks, Mode::ScanDisks => Mode::SelectDisks, Mode::SelectDisks => Mode::SelectTableType, @@ -112,6 +124,41 @@ impl App<'_> { } } + 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::ScanWinImages => { + // TODO: DELETEME + let mut wim_sources = self.state.wim_sources.lock().unwrap(); + wim_sources + .local + .push(parse_wim_file("/home/twoshirt/Projects/deja-vu/23H2.wim")?); + } + 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 @@ -172,7 +219,7 @@ impl App<'_> { 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 { + let Some(keymap) = self.config.keybindings.get(&self.cur_mode) else { return Ok(()); }; if let Some(action) = keymap.get(&vec![key]) { @@ -236,40 +283,78 @@ impl App<'_> { .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::FindWimNetwork => { self.state.reset_network(); + let mut wim_sources = self.state.wim_sources.lock().unwrap(); + wim_sources + .network + .push(parse_wim_file("/home/twoshirt/Projects/deja-vu/23H2.wim")?); + wim_sources + .network + .push(parse_wim_file("/home/twoshirt/Projects/deja-vu/24H2.wim")?); // TODO: Actually scan network! } - Action::SetMode(mode) => { - self.mode = mode; - match mode { - Mode::Done => { - self.action_tx.send(Action::DisplayPopup( - popup::Type::Success, - popup::fortune(), - ))?; - } - Mode::ScanDisks => { - self.state.reset_all(); - } - _ => {} + Action::NextScreen => { + let next_mode = self.next_mode(); + self.action_tx.send(Action::DismissPopup)?; + self.action_tx.send(Action::SetMode(next_mode))?; + } + Action::Process => match self.cur_mode { + Mode::Confirm => { + self.action_tx.send(Action::NextScreen)?; } + Mode::Done => { + self.action_tx.send(Action::Quit)?; + } + _ => {} + }, + 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::SelectWinImage => { + // TODO: FIXME + // PLAN: Abuse Action::Select to send (file_index, image_index) to set all at once + // self.state.wim_file_index = TODO; + // self.state.wim_image_index = TODO; + } + _ => {} + }, + Action::SetMode(mode) => { + self.set_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(build_left_items(self))?; + self.action_tx.send(build_right_items(self))?; self.action_tx.send(Action::Select(None, None))?; } + Action::TasksComplete => self.action_tx.send(Action::NextScreen)?, _ => {} } for component in &mut self.components { @@ -289,8 +374,10 @@ impl App<'_> { fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { - if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] { - let component_areas = vec![header, left, right, footer, popup]; + if let [header, _body, footer, center, left, right, popup] = + get_chunks(frame.area())[..] + { + let component_areas = vec![header, center, 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 @@ -304,6 +391,140 @@ impl App<'_> { } } +fn build_left_items(app: &App) -> Action { + let select_type: SelectionType; + let title: String; + let mut items = Vec::new(); + let mut labels: Vec = 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::ScanWinImages => { + select_type = SelectionType::Loop; + title = String::from("Scanning"); + // TODO: FIXME + } + Mode::SelectWinImage => { + select_type = SelectionType::One; + title = String::from("Select Windows Image"); + // TODO: FIXME + } + 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::SetUserName => { + select_type = SelectionType::Loop; + title = String::from("Customize"); + // TODO: FIXME + } + 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::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::ScanWinImages + | Mode::SelectWinImage + | Mode::SetUserName + | Mode::SelectDisks + | Mode::SelectTableType + | Mode::Confirm => { + // 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))); + } + _ => {} + } + 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() @@ -341,16 +562,20 @@ fn get_chunks(r: Rect) -> Vec { .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(centered_rect(90, 90, chunks[1])) + .split(center) .to_vec(), ); + // Center + chunks.push(center); + // Popup chunks.push(centered_rect(60, 25, r)); diff --git a/win_installer/src/components.rs b/win_installer/src/components.rs new file mode 100644 index 0000000..51833fd --- /dev/null +++ b/win_installer/src/components.rs @@ -0,0 +1,16 @@ +// 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 . +// +pub mod wim_scan; diff --git a/win_installer/src/components/wim_scan.rs b/win_installer/src/components/wim_scan.rs new file mode 100644 index 0000000..039177e --- /dev/null +++ b/win_installer/src/components/wim_scan.rs @@ -0,0 +1,186 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// +use 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, HighlightSpacing, List, ListItem, Padding, Paragraph}, +}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::wim::WimSources; + +#[derive(Default)] +pub struct WimScan { + command_tx: Option>, + config: Config, + mode: Mode, + scan_network: bool, + wim_sources: Arc>, +} + +impl WimScan { + #[must_use] + pub fn new(wim_sources: Arc>) -> 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> { + let _ = key; // to appease clippy + Ok(None) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> 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> { + 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::ScanWinImages { + 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); + let [local_area, network_area] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(left_body); + + // Titles + let titles = vec![ + Paragraph::new(Line::from("Scanning").centered()) + .block(Block::default().borders(Borders::NONE)), + Paragraph::new(Line::from("Info").centered()) + .block(Block::default().borders(Borders::NONE)), + ]; + for (title, area) in zip(titles, [left_title, right_title]) { + frame.render_widget(title, area); + } + + // Local Scan + let local_area = if self.scan_network { + local_area + } else { + left_body + }; + let local_words = "Lorem ipsum dolor sit amet".split_whitespace().to_owned(); + let local_list = List::new(local_words) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().green().bold()) + .highlight_symbol(" --> ") + .repeat_highlight_symbol(false); + frame.render_widget(local_list, local_area); + + // Network Scan + if self.scan_network { + let network_words = "Adipiscing elit quisque faucibus ex sapien" + .split_whitespace() + .to_owned(); + let network_list = List::new(network_words) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().green().bold()) + .highlight_symbol(" --> ") + .repeat_highlight_symbol(false); + frame.render_widget(network_list, network_area); + } + + // WIM Info + let wim_sources = self.wim_sources.lock().unwrap(); + let mut right_list = Vec::new(); + if !wim_sources.network.is_empty() { + right_list.push(ListItem::new("-- Network --\n\n")); + right_list.extend( + wim_sources + .network + .iter() + .map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))), + ); + right_list.push(ListItem::new("")); + } + right_list.push(ListItem::new("-- Local --\n\n")); + right_list.extend( + wim_sources + .local + .iter() + .map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))), + ); + let right_list = List::new(right_list) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().green().bold()) + .highlight_symbol(" --> ") + .repeat_highlight_symbol(false); + frame.render_widget(right_list, right_body); + + // Done + Ok(()) + } +} diff --git a/win_installer/src/main.rs b/win_installer/src/main.rs index 88a0f38..ab05a63 100644 --- a/win_installer/src/main.rs +++ b/win_installer/src/main.rs @@ -19,6 +19,7 @@ use color_eyre::Result; use crate::app::App; mod app; +mod components; mod state; mod wim; diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 6156c61..885485b 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -16,23 +16,26 @@ use std::sync::{Arc, Mutex}; -use core::system::{disk::Disk, drivers}; +use core::system::{ + disk::{Disk, PartitionTableType}, + drivers, +}; use crate::wim::WimSources; #[derive(Debug, Default)] -pub struct State<'a> { +pub struct State { pub disk_index_dest: Option, pub disk_list: Arc>>, pub driver: Option, pub driver_list: Vec, - pub part_index_boot: Option, + pub table_type: Option, pub wim_file_index: Option, pub wim_image_index: Option, - pub wim_sources: Arc>>, + pub wim_sources: Arc>, } -impl State<'_> { +impl State { pub fn new(disk_list: Arc>>) -> Self { State { disk_list, @@ -42,7 +45,6 @@ impl State<'_> { pub fn reset_all(&mut self) { self.disk_index_dest = None; - self.part_index_boot = None; self.wim_file_index = None; self.wim_image_index = None; if let Ok(mut sources) = self.wim_sources.lock() { diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 6a8326f..3e407be 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -48,18 +48,18 @@ static WIN_BUILDS: LazyLock> = LazyLock::new(|| { }); #[derive(Debug)] -pub struct WimFile<'a> { - path: &'a Path, - images: Vec, +pub struct WimFile { + pub path: String, + pub images: Vec, } #[derive(Clone, Debug, Default)] pub struct WimImage { - build: String, - index: String, - name: String, - spbuild: String, - version: String, + pub build: String, + pub index: String, + pub name: String, + pub spbuild: String, + pub version: String, } impl WimImage { @@ -89,12 +89,12 @@ impl fmt::Display for WimImage { } #[derive(Debug, Default)] -pub struct WimSources<'a> { - pub local: Vec>, - pub network: Vec>, +pub struct WimSources { + pub local: Vec, + pub network: Vec, } -impl WimSources<'_> { +impl WimSources { pub fn new() -> Self { Default::default() } @@ -124,10 +124,9 @@ fn get_wim_xml(wim_file: &str) -> std::io::Result { Ok(file) } -fn parse_wim_file(wim_file: &str) -> std::io::Result> { +pub fn parse_wim_file(wim_file: &str) -> std::io::Result { let mut wim_images: Vec = Vec::new(); - let wim_path = Path::new(wim_file); - if !wim_path.exists() { + if !Path::new(wim_file).exists() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "Failed to read WIM file", @@ -146,7 +145,6 @@ fn parse_wim_file(wim_file: &str) -> std::io::Result> { Ok(XmlEvent::StartElement { name, attributes, .. }) => { - println!("{:spaces$}+{name}", "", spaces = depth * 2); depth += 1; current_element = name.local_name.to_uppercase(); @@ -164,20 +162,16 @@ fn parse_wim_file(wim_file: &str) -> std::io::Result> { let build = char_data.trim(); image.build = build.to_string(); image.version = WIN_BUILDS.get(build).map_or("", |v| v).to_string(); - println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2); } if current_element == "NAME" { image.name = char_data.trim().to_string(); - println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2); } if current_element == "SPBUILD" { image.spbuild = char_data.trim().to_string(); - println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2); } } Ok(XmlEvent::EndElement { name }) => { depth -= 1; - println!("{:spaces$}-{name}", "", spaces = depth * 2); if name.local_name.to_uppercase() == "IMAGE" { // Append image to list @@ -191,14 +185,13 @@ fn parse_wim_file(wim_file: &str) -> std::io::Result> { } } Err(e) => { - eprintln!("Error: {e}"); break; } _ => {} } } let wim_file = WimFile { - path: wim_path, + path: wim_file.to_string(), images: wim_images, }; From 3ce36c5a0f2f81cccdff07acf152968ecfedfbb0 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 8 Nov 2025 22:32:41 -0800 Subject: [PATCH 06/27] Misc ??? --- config/config.json5 | 11 ++++- win_installer/src/app.rs | 92 +++++++++++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/config/config.json5 b/config/config.json5 index 1ad0168..2eecb94 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -196,9 +196,18 @@ "": "Quit", "": "Suspend" }, + "SelectWinImage": { + "": "Process", + "": "KeyUp", + "": "KeyDown", + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" + }, "SetUserName": { "": "Process", - "": "Process", + "": "PrevScreen", "": "Quit", "": "Quit", "": "Suspend" diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 3c77bc7..e3fbd9e 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -285,6 +285,9 @@ impl App { } 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::FindWimNetwork => { self.state.reset_network(); let mut wim_sources = self.state.wim_sources.lock().unwrap(); @@ -301,8 +304,17 @@ impl App { 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::SetUserName => { + self.action_tx.send(Action::SetMode(Mode::SelectWinImage))?; + } + _ => {} + }, Action::Process => match self.cur_mode { - Mode::Confirm => { + Mode::Confirm | Mode::ScanWinImages => { self.action_tx.send(Action::NextScreen)?; } Mode::Done => { @@ -310,6 +322,7 @@ impl App { } _ => {} }, + 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 @@ -347,9 +360,8 @@ impl App { }, Action::SetMode(mode) => { self.set_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(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))?; @@ -391,6 +403,37 @@ impl App { } } +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::SelectWinImage => String::from("(Enter) to select / (q) to quit"), + Mode::ScanWinImages => { + String::from("(Enter) to continue / (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; @@ -422,7 +465,7 @@ fn build_left_items(app: &App) -> Action { Mode::SelectWinImage => { select_type = SelectionType::One; title = String::from("Select Windows Image"); - // TODO: FIXME + // TODO: FIXME, I think this whole section could be better... } Mode::SelectDisks => { select_type = SelectionType::One; @@ -485,12 +528,7 @@ fn build_right_items(app: &App) -> Action { }]); start_index = 2; } - Mode::ScanWinImages - | Mode::SelectWinImage - | Mode::SetUserName - | Mode::SelectDisks - | Mode::SelectTableType - | Mode::Confirm => { + Mode::SelectDisks => { // Labels let dest_dv_line = DVLine { line_parts: vec![ @@ -520,6 +558,38 @@ fn build_right_items(app: &App) -> Action { .iter() .for_each(|disk| items.push(get_disk_description_right(disk, &None))); } + Mode::SelectTableType | Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { + // 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) From 2aed8d130b1ea2e80f69d23c670d26ffe01f95e4 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sun, 16 Nov 2025 20:39:43 -0800 Subject: [PATCH 07/27] Make progress --- win_installer/src/app.rs | 19 ++---- win_installer/src/components/wim_scan.rs | 84 ++++++++---------------- win_installer/src/state.rs | 2 + win_installer/src/wim.rs | 72 ++++++++++++-------- 4 files changed, 79 insertions(+), 98 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index e3fbd9e..7d312f5 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -45,7 +45,7 @@ use ratatui::{ use tokio::sync::mpsc; use tracing::{debug, info}; -use crate::{components::wim_scan::WimScan, state::State, wim::parse_wim_file}; +use crate::{components::wim_scan::WimScan, state::State, wim::scan_local_drives}; pub struct App { // TUI @@ -144,11 +144,11 @@ impl App { ))?; } Mode::ScanWinImages => { - // TODO: DELETEME - let mut wim_sources = self.state.wim_sources.lock().unwrap(); - wim_sources - .local - .push(parse_wim_file("/home/twoshirt/Projects/deja-vu/23H2.wim")?); + let disk_list_arc = self.state.disk_list.clone(); + let wim_sources_arc = self.state.wim_sources.clone(); + tokio::task::spawn(async move { + scan_local_drives(disk_list_arc, wim_sources_arc); + }); } Mode::Done => { self.action_tx @@ -290,13 +290,6 @@ impl App { } Action::FindWimNetwork => { self.state.reset_network(); - let mut wim_sources = self.state.wim_sources.lock().unwrap(); - wim_sources - .network - .push(parse_wim_file("/home/twoshirt/Projects/deja-vu/23H2.wim")?); - wim_sources - .network - .push(parse_wim_file("/home/twoshirt/Projects/deja-vu/24H2.wim")?); // TODO: Actually scan network! } Action::NextScreen => { diff --git a/win_installer/src/components/wim_scan.rs b/win_installer/src/components/wim_scan.rs index 039177e..af7ad6d 100644 --- a/win_installer/src/components/wim_scan.rs +++ b/win_installer/src/components/wim_scan.rs @@ -95,47 +95,29 @@ impl Component for WimScan { .direction(Direction::Vertical) .constraints([Constraint::Length(1), Constraint::Min(1)]) .areas(right); - let [local_area, network_area] = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .areas(left_body); // Titles let titles = vec![ - Paragraph::new(Line::from("Scanning").centered()) + Paragraph::new(Line::from("Local").centered()) .block(Block::default().borders(Borders::NONE)), - Paragraph::new(Line::from("Info").centered()) + 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); } - // Local Scan - let local_area = if self.scan_network { - local_area - } else { - left_body - }; - let local_words = "Lorem ipsum dolor sit amet".split_whitespace().to_owned(); - let local_list = List::new(local_words) - .block( - Block::default() - .borders(Borders::ALL) - .padding(Padding::new(1, 1, 1, 1)), - ) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().green().bold()) - .highlight_symbol(" --> ") - .repeat_highlight_symbol(false); - frame.render_widget(local_list, local_area); - - // Network Scan - if self.scan_network { - let network_words = "Adipiscing elit quisque faucibus ex sapien" - .split_whitespace() - .to_owned(); - let network_list = List::new(network_words) + // WIM Info + if let Ok(wim_sources) = self.wim_sources.lock() { + // Local + let mut left_list = Vec::new(); + 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) @@ -145,40 +127,28 @@ impl Component for WimScan { .highlight_style(Style::new().green().bold()) .highlight_symbol(" --> ") .repeat_highlight_symbol(false); - frame.render_widget(network_list, network_area); - } + frame.render_widget(left_list, left_body); - // WIM Info - let wim_sources = self.wim_sources.lock().unwrap(); - let mut right_list = Vec::new(); - if !wim_sources.network.is_empty() { - right_list.push(ListItem::new("-- Network --\n\n")); + // Network + let mut right_list = Vec::new(); right_list.extend( wim_sources .network .iter() .map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))), ); - right_list.push(ListItem::new("")); + let right_list = List::new(right_list) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().green().bold()) + .highlight_symbol(" --> ") + .repeat_highlight_symbol(false); + frame.render_widget(right_list, right_body); } - right_list.push(ListItem::new("-- Local --\n\n")); - right_list.extend( - wim_sources - .local - .iter() - .map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))), - ); - let right_list = List::new(right_list) - .block( - Block::default() - .borders(Borders::ALL) - .padding(Padding::new(1, 1, 1, 1)), - ) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().green().bold()) - .highlight_symbol(" --> ") - .repeat_highlight_symbol(false); - frame.render_widget(right_list, right_body); // Done Ok(()) diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 885485b..d85f8ba 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -37,8 +37,10 @@ pub struct State { impl State { pub fn new(disk_list: Arc>>) -> Self { + let wim_sources = WimSources::new(); State { disk_list, + wim_sources: Arc::new(Mutex::new(wim_sources)), ..Default::default() } } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 3e407be..7065d9c 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -1,3 +1,4 @@ +use core::system::disk::Disk; // This file is part of Deja-Vu. // // Deja-Vu is free software: you can redistribute it and/or modify it @@ -14,11 +15,17 @@ // along with Deja-Vu. If not, see . // use std::{ - collections::HashMap, fmt, fs::File, io::BufReader, path::Path, process::Command, - sync::LazyLock, + collections::HashMap, + fmt, + fs::{File, read_dir}, + io::BufReader, + path::Path, + process::Command, + sync::{Arc, LazyLock, Mutex}, }; use tempfile::NamedTempFile; +use tracing::info; use xml::reader::{EventReader, XmlEvent}; static WIN_BUILDS: LazyLock> = LazyLock::new(|| { @@ -139,13 +146,11 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result { let mut image = WimImage::new(); let parser = EventReader::new(file); - let mut depth = 0; for e in parser { match e { Ok(XmlEvent::StartElement { name, attributes, .. }) => { - depth += 1; current_element = name.local_name.to_uppercase(); if current_element == "IMAGE" { @@ -171,8 +176,6 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result { } } Ok(XmlEvent::EndElement { name }) => { - depth -= 1; - if name.local_name.to_uppercase() == "IMAGE" { // Append image to list if !image.build.is_empty() && !image.name.is_empty() && !image.index.is_empty() @@ -184,7 +187,7 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result { image.reset() } } - Err(e) => { + Err(_) => { break; } _ => {} @@ -198,24 +201,37 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result { Ok(wim_file) } -// fn main() -> std::io::Result<()> { -// let mut sources = WimSources::new(); -// if let Ok(wim_file) = parse_wim_file("./23H2.wim") -// && !wim_file.images.is_empty() -// { -// sources.local.push(wim_file); -// } -// if let Ok(wim_file) = parse_wim_file("./24H2.wim") { -// sources.local.push(wim_file); -// } -// dbg!(&sources); -// sources.local.iter().for_each(|f| { -// println!("-- {} --", f.path.to_string_lossy()); -// f.images.iter().for_each(|i| { -// println!("* {i}"); -// }); -// println!(); -// }); -// -// Ok(()) -// } +pub fn scan_local_drives( + disk_list_arc: Arc>>, + wim_sources_arc: Arc>, +) { + let mut to_check = vec![String::from(".")]; + + // 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() { + to_check.push(format!("{}/Images", &p.letter)); + } + }); + }) + } + + // Scan drives + to_check.iter().for_each(|scan_path| { + info!("Scanning: {}", &scan_path); + 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) + && let Ok(mut wim_sources) = wim_sources_arc.lock() + { + wim_sources.local.push(new_source); + } + }); + } + }); +} From cf3d71567e64062882bf7dc8191cb1e6eee80803 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 28 Nov 2025 19:25:18 -0800 Subject: [PATCH 08/27] Refactor scan_local_drives() --- Cargo.toml | 3 ++ win_installer/src/app.rs | 10 ++---- win_installer/src/state.rs | 68 ++++++++++++++++++++++++++++++++++---- win_installer/src/wim.rs | 56 ++++++++----------------------- 4 files changed, 81 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a6700c..e166af2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ 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 diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 7d312f5..71c249d 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -45,7 +45,7 @@ use ratatui::{ use tokio::sync::mpsc; use tracing::{debug, info}; -use crate::{components::wim_scan::WimScan, state::State, wim::scan_local_drives}; +use crate::{components::wim_scan::WimScan, state::State}; pub struct App { // TUI @@ -143,13 +143,7 @@ impl App { String::from("Scanning Disks..."), ))?; } - Mode::ScanWinImages => { - let disk_list_arc = self.state.disk_list.clone(); - let wim_sources_arc = self.state.wim_sources.clone(); - tokio::task::spawn(async move { - scan_local_drives(disk_list_arc, wim_sources_arc); - }); - } + Mode::ScanWinImages => self.state.scan_wim_local(), Mode::Done => { self.action_tx .send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?; diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index d85f8ba..1f56254 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -14,14 +14,27 @@ // along with Deja-Vu. If not, see . // -use std::sync::{Arc, Mutex}; - -use core::system::{ - disk::{Disk, PartitionTableType}, - drivers, +use std::{ + fs::read_dir, + sync::{Arc, Mutex}, }; -use crate::wim::WimSources; +use core::{ + action::Action, + components::popup::Type as PopupType, + config::Config, + system::{ + disk::{Disk, PartitionTableType}, + drivers, + }, +}; + +use tracing::info; + +use crate::{ + net::connect_network_share, + wim::{WimSources, parse_wim_file}, +}; #[derive(Debug, Default)] pub struct State { @@ -63,4 +76,47 @@ impl State { pub fn scan_drivers(&mut self) { self.driver_list = drivers::scan(); } + + pub fn scan_wim_local(&mut self) { + let disk_list_arc = self.disk_list.clone(); + let wim_sources_arc = self.wim_sources.clone(); + tokio::task::spawn(async move { + scan_local_drives(disk_list_arc, wim_sources_arc); + }); + } + +pub fn scan_local_drives( + disk_list_arc: Arc>>, + wim_sources_arc: Arc>, +) { + let mut to_check = vec![String::from(".")]; + + // 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() { + to_check.push(format!("{}:\\Images", &p.letter)); + } + }); + }) + } + + // Scan drives + to_check.iter().for_each(|scan_path| { + info!("Scanning: {}", &scan_path); + 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) + && let Ok(mut wim_sources) = wim_sources_arc.lock() + { + wim_sources.local.push(new_source); + } + }); + } + }); +} } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 7065d9c..b6bc057 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -1,4 +1,3 @@ -use core::system::disk::Disk; // This file is part of Deja-Vu. // // Deja-Vu is free software: you can redistribute it and/or modify it @@ -16,18 +15,26 @@ use core::system::disk::Disk; // use std::{ collections::HashMap, - fmt, - fs::{File, read_dir}, + env, fmt, + fs::File, io::BufReader, - path::Path, + path::{Path, PathBuf}, process::Command, - sync::{Arc, LazyLock, Mutex}, + sync::LazyLock, }; use tempfile::NamedTempFile; -use tracing::info; use xml::reader::{EventReader, XmlEvent}; +static WIMINFO_EXE: LazyLock = 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> = LazyLock::new(|| { HashMap::from([ // Windows 10 @@ -118,7 +125,7 @@ impl WimSources { fn get_wim_xml(wim_file: &str) -> std::io::Result { let tmp_file = NamedTempFile::new()?; - let _ = Command::new("wiminfo") + let _ = Command::new(&*WIMINFO_EXE) .args([ wim_file, "--extract-xml", @@ -200,38 +207,3 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result { Ok(wim_file) } - -pub fn scan_local_drives( - disk_list_arc: Arc>>, - wim_sources_arc: Arc>, -) { - let mut to_check = vec![String::from(".")]; - - // 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() { - to_check.push(format!("{}/Images", &p.letter)); - } - }); - }) - } - - // Scan drives - to_check.iter().for_each(|scan_path| { - info!("Scanning: {}", &scan_path); - 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) - && let Ok(mut wim_sources) = wim_sources_arc.lock() - { - wim_sources.local.push(new_source); - } - }); - } - }); -} From a7607732691a19f11f040e66a312339b07e518fc Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 28 Nov 2025 19:25:53 -0800 Subject: [PATCH 09/27] Add initial network scan code --- Cargo.lock | 16 +++++++++ config/config.json5 | 4 +++ core/src/config.rs | 24 +++++++++----- win_installer/Cargo.toml | 1 + win_installer/src/app.rs | 7 ++-- win_installer/src/main.rs | 1 + win_installer/src/net.rs | 68 ++++++++++++++++++++++++++++++++++++++ win_installer/src/state.rs | 38 +++++++++++++++++++-- 8 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 win_installer/src/net.rs diff --git a/Cargo.lock b/Cargo.lock index ab28626..4f89671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3145,6 +3145,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "vergen-gix", + "windows-sys 0.61.2", "xml", ] @@ -3198,6 +3199,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" @@ -3216,6 +3223,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" diff --git a/config/config.json5 b/config/config.json5 index 2eecb94..11e6d5b 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -213,4 +213,8 @@ "": "Suspend" }, }, + "network_server": "SERVER", + "network_share": "SHARE", + "network_user": "USER", + "network_pass": "PASS" } diff --git a/core/src/config.rs b/core/src/config.rs index 0cb107b..002c7ec 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -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), diff --git a/win_installer/Cargo.toml b/win_installer/Cargo.toml index d7bcc76..e801d4d 100644 --- a/win_installer/Cargo.toml +++ b/win_installer/Cargo.toml @@ -41,6 +41,7 @@ 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" [build-dependencies] diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 71c249d..ad3f845 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -67,9 +67,10 @@ pub struct App { impl App { pub fn new(tick_rate: f64, frame_rate: f64) -> Result { 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(disk_list_arc); + let state = State::new(config.clone(), disk_list_arc); let wim_sources = Arc::clone(&state.wim_sources); Ok(Self { // TUI @@ -83,7 +84,7 @@ impl App { Box::new(Footer::new()), Box::new(popup::Popup::new()), ], - config: Config::new()?, + config, frame_rate, last_tick_key_events: Vec::new(), should_quit: false, @@ -284,7 +285,7 @@ impl App { } Action::FindWimNetwork => { self.state.reset_network(); - // TODO: Actually scan network! + self.state.scan_wim_network(); } Action::NextScreen => { let next_mode = self.next_mode(); diff --git a/win_installer/src/main.rs b/win_installer/src/main.rs index ab05a63..8d21810 100644 --- a/win_installer/src/main.rs +++ b/win_installer/src/main.rs @@ -20,6 +20,7 @@ use crate::app::App; mod app; mod components; +mod net; mod state; mod wim; diff --git a/win_installer/src/net.rs b/win_installer/src/net.rs new file mode 100644 index 0000000..ea24c0c --- /dev/null +++ b/win_installer/src/net.rs @@ -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 . +// + +//#![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) + } +} diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 1f56254..1fa47f9 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -38,6 +38,7 @@ use crate::{ #[derive(Debug, Default)] pub struct State { + pub config: Config, pub disk_index_dest: Option, pub disk_list: Arc>>, pub driver: Option, @@ -49,11 +50,12 @@ pub struct State { } impl State { - pub fn new(disk_list: Arc>>) -> Self { - let wim_sources = WimSources::new(); + pub fn new(config: Config, disk_list: Arc>>) -> Self { + let wim_sources = Arc::new(Mutex::new(WimSources::new())); State { + config, disk_list, - wim_sources: Arc::new(Mutex::new(wim_sources)), + wim_sources, ..Default::default() } } @@ -85,6 +87,16 @@ impl State { }); } + pub fn scan_wim_network(&mut self) { + let config = self.config.clone(); + let disk_list_arc = self.disk_list.clone(); + let wim_sources_arc = self.wim_sources.clone(); + tokio::task::spawn(async move { + scan_network_share(config, disk_list_arc, wim_sources_arc); + }); + } +} + pub fn scan_local_drives( disk_list_arc: Arc>>, wim_sources_arc: Arc>, @@ -119,4 +131,24 @@ pub fn scan_local_drives( } }); } + +pub fn scan_network_share( + config: Config, + disk_list_arc: Arc>>, + wim_sources_arc: Arc>, +) { + let result = connect_network_share( + &config.network_server, + &config.network_share, + &config.network_user, + &config.network_pass, + ); + + // Connect to share + if result.is_err() { + return; + } + + // Scan share + let _ = 14; } From 70525ae6e0751823905d0f9cecd72b5d66a32469 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 28 Nov 2025 23:41:35 -0800 Subject: [PATCH 10/27] Implement network WIM scan --- win_installer/src/state.rs | 26 ++++++++++++++++++-------- win_installer/src/wim.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 1fa47f9..d679d48 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -89,10 +89,9 @@ impl State { pub fn scan_wim_network(&mut self) { let config = self.config.clone(); - let disk_list_arc = self.disk_list.clone(); let wim_sources_arc = self.wim_sources.clone(); tokio::task::spawn(async move { - scan_network_share(config, disk_list_arc, wim_sources_arc); + scan_network_share(config, wim_sources_arc); }); } } @@ -125,18 +124,14 @@ pub fn scan_local_drives( && let Ok(new_source) = parse_wim_file(path_str) && let Ok(mut wim_sources) = wim_sources_arc.lock() { - wim_sources.local.push(new_source); + wim_sources.add_local(new_source); } }); } }); } -pub fn scan_network_share( - config: Config, - disk_list_arc: Arc>>, - wim_sources_arc: Arc>, -) { +pub fn scan_network_share(config: Config, wim_sources_arc: Arc>) { let result = connect_network_share( &config.network_server, &config.network_share, @@ -150,5 +145,20 @@ pub fn scan_network_share( } // 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) + && let Ok(mut wim_sources) = wim_sources_arc.lock() + { + wim_sources.add_network(new_source); + } + }); + } + + // Done let _ = 14; } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index b6bc057..c222325 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -14,6 +14,7 @@ // along with Deja-Vu. If not, see . // use std::{ + cmp::Ordering, collections::HashMap, env, fmt, fs::File, @@ -67,6 +68,26 @@ pub struct WimFile { pub images: Vec, } +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 { + Some(self.cmp(other)) + } +} + #[derive(Clone, Debug, Default)] pub struct WimImage { pub build: String, @@ -113,6 +134,16 @@ impl WimSources { Default::default() } + pub fn add_local(&mut self, wim_file: WimFile) { + self.local.push(wim_file); + self.local.sort(); + } + + pub fn add_network(&mut self, wim_file: WimFile) { + self.network.push(wim_file); + self.network.sort(); + } + pub fn reset_all(&mut self) { self.local.clear(); self.network.clear(); From 89e768e3a41fff63da125a927d14da3326caab7c Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 29 Nov 2025 01:52:23 -0800 Subject: [PATCH 11/27] Add WIM file selection section --- boot_diags/src/app.rs | 6 +- config/config.json5 | 11 ++- core/src/state.rs | 3 +- deja_vu/src/app.rs | 12 ++-- win_installer/src/app.rs | 91 +++++++++++++++++++++--- win_installer/src/components/wim_scan.rs | 42 +++++------ win_installer/src/wim.rs | 36 +++++++++- 7 files changed, 156 insertions(+), 45 deletions(-) diff --git a/boot_diags/src/app.rs b/boot_diags/src/app.rs index d3e9e89..e804379 100644 --- a/boot_diags/src/app.rs +++ b/boot_diags/src/app.rs @@ -657,8 +657,9 @@ fn build_footer_string(cur_mode: Mode) -> String { | Mode::PEMenu | Mode::PreClone | Mode::PostClone - | Mode::ScanWinImages + | Mode::ScanWinSources | Mode::SelectTableType + | Mode::SelectWinSource | Mode::SelectWinImage | Mode::SetUserName => { panic!("This shouldn't happen?") @@ -774,7 +775,8 @@ fn build_left_items(app: &App) -> Action { | Mode::PreClone | Mode::Clone | Mode::PostClone - | Mode::ScanWinImages + | Mode::ScanWinSources + | Mode::SelectWinSource | Mode::SelectWinImage | Mode::SetUserName => { panic!("This shouldn't happen?") diff --git a/config/config.json5 b/config/config.json5 index 11e6d5b..d9ddca5 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -188,7 +188,7 @@ "": "Quit", "": "Suspend" }, - "ScanWinImages": { + "ScanWinSources": { "": "Process", "": "FindWimNetwork", "": "Quit", @@ -196,6 +196,15 @@ "": "Quit", "": "Suspend" }, + "SelectWinSource": { + "": "Process", + "": "KeyUp", + "": "KeyDown", + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend" + }, "SelectWinImage": { "": "Process", "": "KeyUp", diff --git a/core/src/state.rs b/core/src/state.rs index c0bcadd..e386a1c 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -43,7 +43,8 @@ pub enum Mode { SelectParts, PostClone, // Windows Installer - ScanWinImages, + ScanWinSources, + SelectWinSource, SelectWinImage, SetUserName, // WinPE diff --git a/deja_vu/src/app.rs b/deja_vu/src/app.rs index 75194ad..450f423 100644 --- a/deja_vu/src/app.rs +++ b/deja_vu/src/app.rs @@ -129,7 +129,8 @@ impl App { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::ScanWinImages + | Mode::ScanWinSources + | Mode::SelectWinSource | Mode::SelectWinImage | Mode::SetBootMode | Mode::SetUserName => panic!("This shouldn't happen?"), @@ -157,7 +158,8 @@ impl App { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::ScanWinImages + | Mode::ScanWinSources + | Mode::SelectWinSource | Mode::SelectWinImage | Mode::SetBootMode | Mode::SetUserName => panic!("This shouldn't happen?"), @@ -639,7 +641,8 @@ fn build_footer_string(cur_mode: Mode) -> String { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::ScanWinImages + | Mode::ScanWinSources + | Mode::SelectWinSource | Mode::SelectWinImage | Mode::SetBootMode | Mode::SetUserName => panic!("This shouldn't happen?"), @@ -715,7 +718,8 @@ fn build_left_items(app: &App) -> Action { | Mode::LogView | Mode::PEMenu | Mode::Process - | Mode::ScanWinImages + | Mode::ScanWinSources + | Mode::SelectWinSource | Mode::SelectWinImage | Mode::SetBootMode | Mode::SetUserName => panic!("This shouldn't happen?"), diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index ad3f845..c476050 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -102,8 +102,9 @@ impl App { Mode::Home | Mode::InstallDrivers => Mode::ScanDisks, Mode::ScanDisks => Mode::SelectDisks, Mode::SelectDisks => Mode::SelectTableType, - Mode::SelectTableType => Mode::ScanWinImages, - Mode::ScanWinImages => Mode::SelectWinImage, + 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 @@ -144,7 +145,7 @@ impl App { String::from("Scanning Disks..."), ))?; } - Mode::ScanWinImages => self.state.scan_wim_local(), + Mode::ScanWinSources => self.state.scan_wim_local(), Mode::Done => { self.action_tx .send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?; @@ -297,12 +298,13 @@ impl App { self.action_tx.send(Action::SetMode(Mode::SelectDisks))?; } Mode::SetUserName => { - self.action_tx.send(Action::SetMode(Mode::SelectWinImage))?; + self.action_tx + .send(Action::SetMode(Mode::SelectWinSource))?; } _ => {} }, Action::Process => match self.cur_mode { - Mode::Confirm | Mode::ScanWinImages => { + Mode::Confirm | Mode::ScanWinSources => { self.action_tx.send(Action::NextScreen)?; } Mode::Done => { @@ -338,7 +340,7 @@ impl App { } } } - Mode::SelectWinImage => { + Mode::SelectWinSource => { // TODO: FIXME // PLAN: Abuse Action::Select to send (file_index, image_index) to set all at once // self.state.wim_file_index = TODO; @@ -399,8 +401,9 @@ fn build_footer_string(cur_mode: Mode) -> String { "(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 => String::from("(Enter) to select / (q) to quit"), Mode::SelectWinImage => String::from("(Enter) to select / (q) to quit"), - Mode::ScanWinImages => { + Mode::ScanWinSources => { String::from("(Enter) to continue / (n) to scan network / (q) to quit") } Mode::SetUserName => String::from("(Enter) to continue / (Esc) to go back"), @@ -445,15 +448,25 @@ fn build_left_items(app: &App) -> Action { title = String::from("Processing"); // TODO: FIXME } - Mode::ScanWinImages => { + 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 => { select_type = SelectionType::One; title = String::from("Select Windows Image"); - // TODO: FIXME, I think this whole section could be better... + // TODO: FIXME } Mode::SelectDisks => { select_type = SelectionType::One; @@ -546,7 +559,65 @@ fn build_right_items(app: &App) -> Action { .iter() .for_each(|disk| items.push(get_disk_description_right(disk, &None))); } - Mode::SelectTableType | Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { + 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], + }, + ]; + 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::SelectTableType | Mode::SetUserName | Mode::Confirm => { // Labels let dest_dv_line = DVLine { line_parts: vec![ diff --git a/win_installer/src/components/wim_scan.rs b/win_installer/src/components/wim_scan.rs index af7ad6d..d2c6caf 100644 --- a/win_installer/src/components/wim_scan.rs +++ b/win_installer/src/components/wim_scan.rs @@ -77,7 +77,7 @@ impl Component for WimScan { } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - if self.mode != Mode::ScanWinImages { + if self.mode != Mode::ScanWinSources { return Ok(()); } frame.render_widget(Clear, area); @@ -117,36 +117,26 @@ impl Component for WimScan { .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)), - ) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().green().bold()) - .highlight_symbol(" --> ") - .repeat_highlight_symbol(false); + 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(); - right_list.extend( - wim_sources - .network - .iter() - .map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))), + 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)), ); - let right_list = List::new(right_list) - .block( - Block::default() - .borders(Borders::ALL) - .padding(Padding::new(1, 1, 1, 1)), - ) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().green().bold()) - .highlight_symbol(" --> ") - .repeat_highlight_symbol(false); frame.render_widget(right_list, right_body); } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index c222325..b971569 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -62,12 +62,30 @@ static WIN_BUILDS: LazyLock> = LazyLock::new(|| { ]) }); -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct WimFile { pub path: String, pub images: Vec, } +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 @@ -144,6 +162,22 @@ impl WimSources { self.network.sort(); } + 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 { + let mut list = self.local.clone(); + list.append(&mut self.network.clone()); + list + } + pub fn reset_all(&mut self) { self.local.clear(); self.network.clear(); From 88dfe52b07d6893b3ff2a21ca7c49ea03e3f4724 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 29 Nov 2025 02:33:24 -0800 Subject: [PATCH 12/27] Add WIM Image selection section --- win_installer/src/app.rs | 86 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index c476050..672ede3 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -341,10 +341,10 @@ impl App { } } Mode::SelectWinSource => { - // TODO: FIXME - // PLAN: Abuse Action::Select to send (file_index, image_index) to set all at once - // self.state.wim_file_index = TODO; - // self.state.wim_image_index = TODO; + self.state.wim_file_index = one; + } + Mode::SelectWinImage => { + self.state.wim_image_index = one; } _ => {} }, @@ -401,8 +401,9 @@ fn build_footer_string(cur_mode: Mode) -> String { "(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 => String::from("(Enter) to select / (q) to quit"), - Mode::SelectWinImage => String::from("(Enter) to select / (q) to quit"), + Mode::SelectWinSource | Mode::SelectWinImage => { + String::from("(Enter) to select / (q) to quit") + } Mode::ScanWinSources => { String::from("(Enter) to continue / (n) to scan network / (q) to quit") } @@ -466,7 +467,15 @@ fn build_left_items(app: &App) -> Action { Mode::SelectWinImage => { select_type = SelectionType::One; title = String::from("Select Windows Image"); - // TODO: FIXME + 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; @@ -606,6 +615,7 @@ fn build_right_items(app: &App) -> Action { line_parts: vec![String::from("Images")], line_colors: vec![Color::Blue], }, + DVLine::blank(), ]; source.images.iter().for_each(|image| { wim_dv_lines.push(DVLine { @@ -617,6 +627,68 @@ fn build_right_items(app: &App) -> Action { }); } } + Mode::SelectWinImage => { + let source; + if let Ok(wim_sources) = app.state.wim_sources.lock() + && let Some(index) = app.state.wim_file_index + { + source = 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![source.path.clone()], + line_colors: vec![Color::Reset], + }, + DVLine::blank(), + DVLine { + line_parts: vec![String::from("Image")], + line_colors: vec![Color::Blue], + }, + ]); + labels.push(label_dv_lines); + + // WIM Info + source.images.iter().for_each(|image| { + items.push(vec![DVLine { + line_parts: vec![format!("{image}")], + line_colors: vec![Color::Reset], + }]) + }); + } Mode::SelectTableType | Mode::SetUserName | Mode::Confirm => { // Labels let dest_dv_line = DVLine { From dd4733c991b46c1ddc5587919a26502799b19773 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 29 Nov 2025 03:25:22 -0800 Subject: [PATCH 13/27] Add username section --- Cargo.lock | 15 ++- config/config.json5 | 1 - core/src/action.rs | 1 + win_installer/Cargo.toml | 1 + win_installer/src/app.rs | 72 +++++++--- win_installer/src/components.rs | 1 + win_installer/src/components/set_username.rs | 135 +++++++++++++++++++ win_installer/src/state.rs | 1 + 8 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 win_installer/src/components/set_username.rs diff --git a/Cargo.lock b/Cargo.lock index 4f89671..f3ae8e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,7 +689,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2669,7 +2669,7 @@ dependencies = [ "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" @@ -3144,6 +3154,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "tui-input", "vergen-gix", "windows-sys 0.61.2", "xml", diff --git a/config/config.json5 b/config/config.json5 index d9ddca5..eb90ed6 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -215,7 +215,6 @@ "": "Suspend" }, "SetUserName": { - "": "Process", "": "PrevScreen", "": "Quit", "": "Quit", diff --git a/core/src/action.rs b/core/src/action.rs index 7675fd5..3559c2e 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -57,6 +57,7 @@ pub enum Action { Shutdown, // App (Win-Installer) FindWimNetwork, + SetUserName(String), // Screens DismissPopup, DisplayPopup(PopupType, String), diff --git a/win_installer/Cargo.toml b/win_installer/Cargo.toml index e801d4d..8dc0dd2 100644 --- a/win_installer/Cargo.toml +++ b/win_installer/Cargo.toml @@ -43,6 +43,7 @@ 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" diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 672ede3..0520d15 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -45,7 +45,11 @@ use ratatui::{ use tokio::sync::mpsc; use tracing::{debug, info}; -use crate::{components::wim_scan::WimScan, state::State}; +use crate::{ + components::{set_username::InputUsername, wim_scan::WimScan}, + state::State, + wim::WimImage, +}; pub struct App { // TUI @@ -81,6 +85,7 @@ impl App { 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()), ], @@ -356,6 +361,10 @@ impl App { 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)?, _ => {} } @@ -376,10 +385,10 @@ impl App { fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { - if let [header, _body, footer, center, left, right, popup] = + if let [header, _body, footer, center, left, right, username, popup] = get_chunks(frame.area())[..] { - let component_areas = vec![header, center, left, right, footer, popup]; + 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 @@ -464,7 +473,7 @@ fn build_left_items(app: &App) -> Action { .for_each(|wim_file| items.push(wim_file.path.clone())); } } - Mode::SelectWinImage => { + Mode::SelectWinImage | Mode::SetUserName => { select_type = SelectionType::One; title = String::from("Select Windows Image"); if let Ok(wim_sources) = app.state.wim_sources.lock() @@ -491,11 +500,6 @@ fn build_left_items(app: &App) -> Action { items.push(format!("{}", PartitionTableType::Guid)); items.push(format!("{}", PartitionTableType::Legacy)); } - Mode::SetUserName => { - select_type = SelectionType::Loop; - title = String::from("Customize"); - // TODO: FIXME - } Mode::Confirm => { select_type = SelectionType::Loop; title = String::from("Confirm Selections"); @@ -627,7 +631,8 @@ fn build_right_items(app: &App) -> Action { }); } } - Mode::SelectWinImage => { + Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { + info!("Building right items for: {:?}", &app.cur_mode); let source; if let Ok(wim_sources) = app.state.wim_sources.lock() && let Some(index) = app.state.wim_file_index @@ -679,17 +684,45 @@ fn build_right_items(app: &App) -> Action { line_colors: vec![Color::Blue], }, ]); - labels.push(label_dv_lines); // WIM Info - source.images.iter().for_each(|image| { - items.push(vec![DVLine { - line_parts: vec![format!("{image}")], - line_colors: vec![Color::Reset], - }]) - }); + match app.cur_mode { + Mode::SelectWinImage => { + source.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) = source.images.get(index) + { + label_dv_lines.append(&mut vec![ + DVLine { + line_parts: vec![format!("{image}")], + line_colors: vec![Color::Reset], + }, + DVLine::blank(), + ]); + } + if 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 + info!("label_dv_lines: {:?}", &label_dv_lines); + labels.push(label_dv_lines); } - Mode::SelectTableType | Mode::SetUserName | Mode::Confirm => { + Mode::SelectTableType => { // Labels let dest_dv_line = DVLine { line_parts: vec![ @@ -777,6 +810,9 @@ fn get_chunks(r: Rect) -> Vec { // Center chunks.push(center); + // Set username + chunks.push(centered_rect(60, 20, r)); + // Popup chunks.push(centered_rect(60, 25, r)); diff --git a/win_installer/src/components.rs b/win_installer/src/components.rs index 51833fd..92f5847 100644 --- a/win_installer/src/components.rs +++ b/win_installer/src/components.rs @@ -13,4 +13,5 @@ // You should have received a copy of the GNU General Public License // along with Deja-Vu. If not, see . // +pub mod set_username; pub mod wim_scan; diff --git a/win_installer/src/components/set_username.rs b/win_installer/src/components/set_username.rs new file mode 100644 index 0000000..ff63778 --- /dev/null +++ b/win_installer/src/components/set_username.rs @@ -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 . +// + +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>, + 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) -> 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) -> Result> { + 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 = 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> { + 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(()) + } +} diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index d679d48..68d9ebd 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -44,6 +44,7 @@ pub struct State { pub driver: Option, pub driver_list: Vec, pub table_type: Option, + pub username: Option, pub wim_file_index: Option, pub wim_image_index: Option, pub wim_sources: Arc>, From 09b204c0b0531c0b4b84749d73b18e6b8d57405c Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 29 Nov 2025 05:41:08 -0800 Subject: [PATCH 14/27] Add option to include local backup WIMs --- config/config.json5 | 2 ++ core/src/action.rs | 1 + win_installer/src/app.rs | 49 ++++++++++++++++++-------- win_installer/src/state.rs | 70 +++++++++++++++++++++++++++++++------- win_installer/src/wim.rs | 10 ++++-- 5 files changed, 101 insertions(+), 31 deletions(-) diff --git a/config/config.json5 b/config/config.json5 index eb90ed6..90af2f8 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -190,6 +190,7 @@ }, "ScanWinSources": { "": "Process", + "": "FindWimBackups", "": "FindWimNetwork", "": "Quit", "": "Quit", @@ -200,6 +201,7 @@ "": "Process", "": "KeyUp", "": "KeyDown", + "": "PrevScreen", "": "Quit", "": "Quit", "": "Quit", diff --git a/core/src/action.rs b/core/src/action.rs index 3559c2e..e79620d 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -56,6 +56,7 @@ pub enum Action { Restart, Shutdown, // App (Win-Installer) + FindWimBackups, FindWimNetwork, SetUserName(String), // Screens diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 0520d15..c15f3b1 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -47,8 +47,7 @@ use tracing::{debug, info}; use crate::{ components::{set_username::InputUsername, wim_scan::WimScan}, - state::State, - wim::WimImage, + state::{ScanType, State}, }; pub struct App { @@ -110,7 +109,19 @@ impl App { Mode::SelectTableType => Mode::ScanWinSources, Mode::ScanWinSources => Mode::SelectWinSource, Mode::SelectWinSource => Mode::SelectWinImage, - Mode::SelectWinImage => Mode::SetUserName, + Mode::SelectWinImage => { + let mut next_mode = Mode::SetUserName; + // TODO: FIXME - Race condition? + // 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_installer { + // next_mode = Mode::Confirm; + // } + // } + next_mode + } Mode::SetUserName => Mode::Confirm, Mode::Confirm => Mode::Process, // i.e. format, apply, etc Mode::Process | Mode::Done => Mode::Done, @@ -150,7 +161,7 @@ impl App { String::from("Scanning Disks..."), ))?; } - Mode::ScanWinSources => self.state.scan_wim_local(), + Mode::ScanWinSources => self.state.scan_wim_local(ScanType::WindowsInstallers), Mode::Done => { self.action_tx .send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?; @@ -289,6 +300,10 @@ impl App { 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(); @@ -302,7 +317,10 @@ impl App { Mode::SelectTableType => { self.action_tx.send(Action::SetMode(Mode::SelectDisks))?; } - Mode::SetUserName => { + Mode::SelectWinSource => { + self.action_tx.send(Action::SetMode(Mode::ScanWinSources))?; + } + Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { self.action_tx .send(Action::SetMode(Mode::SelectWinSource))?; } @@ -413,9 +431,9 @@ fn build_footer_string(cur_mode: Mode) -> String { Mode::SelectWinSource | Mode::SelectWinImage => { String::from("(Enter) to select / (q) to quit") } - Mode::ScanWinSources => { - String::from("(Enter) to continue / (n) to scan network / (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"), @@ -633,11 +651,11 @@ fn build_right_items(app: &App) -> Action { } Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { info!("Building right items for: {:?}", &app.cur_mode); - let source; + let wim_file; if let Ok(wim_sources) = app.state.wim_sources.lock() && let Some(index) = app.state.wim_file_index { - source = wim_sources.get_file(index); + wim_file = wim_sources.get_file(index); } else { panic!("Failed to get source WIM file"); } @@ -675,7 +693,7 @@ fn build_right_items(app: &App) -> Action { line_colors: vec![Color::Cyan], }, DVLine { - line_parts: vec![source.path.clone()], + line_parts: vec![wim_file.path.clone()], line_colors: vec![Color::Reset], }, DVLine::blank(), @@ -688,7 +706,7 @@ fn build_right_items(app: &App) -> Action { // WIM Info match app.cur_mode { Mode::SelectWinImage => { - source.images.iter().for_each(|image| { + wim_file.images.iter().for_each(|image| { items.push(vec![DVLine { line_parts: vec![format!("{image}")], line_colors: vec![Color::Reset], @@ -697,7 +715,7 @@ fn build_right_items(app: &App) -> Action { } Mode::Confirm => { if let Some(index) = app.state.wim_image_index - && let Some(image) = source.images.get(index) + && let Some(image) = wim_file.images.get(index) { label_dv_lines.append(&mut vec![ DVLine { @@ -707,7 +725,9 @@ fn build_right_items(app: &App) -> Action { DVLine::blank(), ]); } - if let Some(username) = &app.state.username { + if wim_file.is_installer + && 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], @@ -719,7 +739,6 @@ fn build_right_items(app: &App) -> Action { } // Done - info!("label_dv_lines: {:?}", &label_dv_lines); labels.push(label_dv_lines); } Mode::SelectTableType => { diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 68d9ebd..98428c3 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -20,8 +20,6 @@ use std::{ }; use core::{ - action::Action, - components::popup::Type as PopupType, config::Config, system::{ disk::{Disk, PartitionTableType}, @@ -33,9 +31,14 @@ use tracing::info; use crate::{ net::connect_network_share, - wim::{WimSources, parse_wim_file}, + wim::{WimFile, WimSources, parse_wim_file}, }; +pub enum ScanType { + GeneralWimFiles, // Includes Windows installer WIMs + WindowsInstallers, +} + #[derive(Debug, Default)] pub struct State { pub config: Config, @@ -70,6 +73,12 @@ impl State { } } + 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(); @@ -80,11 +89,11 @@ impl State { self.driver_list = drivers::scan(); } - pub fn scan_wim_local(&mut self) { + 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(); tokio::task::spawn(async move { - scan_local_drives(disk_list_arc, wim_sources_arc); + scan_local_drives(disk_list_arc, wim_sources_arc, scan_type); }); } @@ -97,18 +106,39 @@ impl State { } } +fn get_subfolders(path_str: &str) -> Vec { + 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>>, wim_sources_arc: Arc>, + scan_type: ScanType, ) { let mut to_check = vec![String::from(".")]; + let mut wim_files: Vec = 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() { - to_check.push(format!("{}:\\Images", &p.letter)); + match scan_type { + ScanType::GeneralWimFiles => { + to_check.append(&mut get_subfolders(&format!("{}:\\", &p.letter))); + } + ScanType::WindowsInstallers => { + to_check.push(format!("{}:\\Images", &p.letter)); + } + } } }); }) @@ -116,20 +146,28 @@ pub fn scan_local_drives( // Scan drives to_check.iter().for_each(|scan_path| { + let installer = scan_path.ends_with("\\Images"); info!("Scanning: {}", &scan_path); 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) - && let Ok(mut wim_sources) = wim_sources_arc.lock() + && let Ok(new_source) = parse_wim_file(path_str, installer) { - wim_sources.add_local(new_source); + 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>) { @@ -139,6 +177,7 @@ pub fn scan_network_share(config: Config, wim_sources_arc: Arc &config.network_user, &config.network_pass, ); + let mut wim_files: Vec = Vec::new(); // Connect to share if result.is_err() { @@ -152,14 +191,19 @@ pub fn scan_network_share(config: Config, wim_sources_arc: Arc 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) - && let Ok(mut wim_sources) = wim_sources_arc.lock() + && let Ok(new_source) = parse_wim_file(path_str, true) + // Assuming all network sources are installers { - wim_sources.add_network(new_source); + wim_files.push(new_source); } }); } // Done - let _ = 14; + 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)); + } } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index b971569..032ff86 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -66,6 +66,7 @@ static WIN_BUILDS: LazyLock> = LazyLock::new(|| { pub struct WimFile { pub path: String, pub images: Vec, + pub is_installer: bool, } impl WimFile { @@ -154,12 +155,10 @@ impl WimSources { pub fn add_local(&mut self, wim_file: WimFile) { self.local.push(wim_file); - self.local.sort(); } pub fn add_network(&mut self, wim_file: WimFile) { self.network.push(wim_file); - self.network.sort(); } pub fn get_file(&self, index: usize) -> WimFile { @@ -183,6 +182,10 @@ impl WimSources { self.network.clear(); } + pub fn reset_local(&mut self) { + self.local.clear(); + } + pub fn reset_network(&mut self) { self.network.clear(); } @@ -203,7 +206,7 @@ fn get_wim_xml(wim_file: &str) -> std::io::Result { Ok(file) } -pub fn parse_wim_file(wim_file: &str) -> std::io::Result { +pub fn parse_wim_file(wim_file: &str, installer: bool) -> std::io::Result { let mut wim_images: Vec = Vec::new(); if !Path::new(wim_file).exists() { return Err(std::io::Error::new( @@ -268,6 +271,7 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result { let wim_file = WimFile { path: wim_file.to_string(), images: wim_images, + is_installer: installer, }; Ok(wim_file) From e873ec96023dcaee87ee520da44df314650ea160 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sun, 30 Nov 2025 20:46:02 -0800 Subject: [PATCH 15/27] Refactor WIM setup-image logic --- win_installer/src/app.rs | 26 ++++++++++++-------------- win_installer/src/state.rs | 9 +++------ win_installer/src/wim.rs | 6 +++--- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index c15f3b1..2c9b953 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -109,19 +109,7 @@ impl App { Mode::SelectTableType => Mode::ScanWinSources, Mode::ScanWinSources => Mode::SelectWinSource, Mode::SelectWinSource => Mode::SelectWinImage, - Mode::SelectWinImage => { - let mut next_mode = Mode::SetUserName; - // TODO: FIXME - Race condition? - // 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_installer { - // next_mode = Mode::Confirm; - // } - // } - next_mode - } + Mode::SelectWinImage => Mode::SetUserName, Mode::SetUserName => Mode::Confirm, Mode::Confirm => Mode::Process, // i.e. format, apply, etc Mode::Process | Mode::Done => Mode::Done, @@ -162,6 +150,16 @@ impl App { ))?; } Mode::ScanWinSources => 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()))?; @@ -725,7 +723,7 @@ fn build_right_items(app: &App) -> Action { DVLine::blank(), ]); } - if wim_file.is_installer + if wim_file.is_setup && let Some(username) = &app.state.username { label_dv_lines.append(&mut vec![DVLine { diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 98428c3..4c948a8 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -27,8 +27,6 @@ use core::{ }, }; -use tracing::info; - use crate::{ net::connect_network_share, wim::{WimFile, WimSources, parse_wim_file}, @@ -123,7 +121,7 @@ pub fn scan_local_drives( wim_sources_arc: Arc>, scan_type: ScanType, ) { - let mut to_check = vec![String::from(".")]; + let mut to_check: Vec = Vec::new(); let mut wim_files: Vec = Vec::new(); // Get drive letters @@ -146,14 +144,13 @@ pub fn scan_local_drives( // Scan drives to_check.iter().for_each(|scan_path| { - let installer = scan_path.ends_with("\\Images"); - info!("Scanning: {}", &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, installer) + && let Ok(new_source) = parse_wim_file(path_str, is_setup) { wim_files.push(new_source); } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 032ff86..c5e7541 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -66,7 +66,7 @@ static WIN_BUILDS: LazyLock> = LazyLock::new(|| { pub struct WimFile { pub path: String, pub images: Vec, - pub is_installer: bool, + pub is_setup: bool, } impl WimFile { @@ -206,7 +206,7 @@ fn get_wim_xml(wim_file: &str) -> std::io::Result { Ok(file) } -pub fn parse_wim_file(wim_file: &str, installer: bool) -> std::io::Result { +pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result { let mut wim_images: Vec = Vec::new(); if !Path::new(wim_file).exists() { return Err(std::io::Error::new( @@ -271,7 +271,7 @@ pub fn parse_wim_file(wim_file: &str, installer: bool) -> std::io::Result Date: Sun, 30 Nov 2025 20:46:55 -0800 Subject: [PATCH 16/27] Include size in WimImage --- win_installer/src/wim.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index c5e7541..9a43f5c 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -1,3 +1,4 @@ +use core::system::disk::bytes_to_string; // This file is part of Deja-Vu. // // Deja-Vu is free software: you can redistribute it and/or modify it @@ -112,6 +113,7 @@ pub struct WimImage { pub build: String, pub index: String, pub name: String, + pub size: u64, pub spbuild: String, pub version: String, } @@ -138,7 +140,15 @@ impl fmt::Display for WimImage { } else { format!("{}, ", self.version) }; - write!(f, "{} ({}{}.{})", self.name, s, self.build, self.spbuild) + write!( + f, + "{} ({}{}.{}) [{}]", + self.name, + s, + self.build, + self.spbuild, + bytes_to_string(self.size) + ) } } @@ -249,12 +259,23 @@ pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result(); + 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.name.is_empty() && !image.index.is_empty() - { + if image.build.is_empty() { + image.build.push_str("-14"); + } + if !image.name.is_empty() && !image.index.is_empty() { wim_images.push(image.clone()); } From 4658624988e2967ff6b7c7dad2a342a518440c7b Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sun, 30 Nov 2025 21:03:24 -0800 Subject: [PATCH 17/27] Add missing back keys and footer text --- config/config.json5 | 1 + win_installer/src/app.rs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/config.json5 b/config/config.json5 index 90af2f8..3be6e80 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -211,6 +211,7 @@ "": "Process", "": "KeyUp", "": "KeyDown", + "": "PrevScreen", "": "Quit", "": "Quit", "": "Quit", diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 2c9b953..401ccd2 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -149,7 +149,10 @@ impl App { String::from("Scanning Disks..."), ))?; } - Mode::ScanWinSources => self.state.scan_wim_local(ScanType::WindowsInstallers), + 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 @@ -427,7 +430,7 @@ fn build_footer_string(cur_mode: Mode) -> String { ), Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"), Mode::SelectWinSource | Mode::SelectWinImage => { - String::from("(Enter) to select / (q) to quit") + 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", From c4c174b546ad030012ae26289b00f327b25b7d93 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sun, 30 Nov 2025 21:03:57 -0800 Subject: [PATCH 18/27] Handle missing WIM build/spbuild info Needed for manual backup captures --- win_installer/src/wim.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 9a43f5c..cb44c22 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -273,7 +273,10 @@ pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result Date: Thu, 4 Dec 2025 22:36:38 -0800 Subject: [PATCH 19/27] Track WIM scans to avoid stacking scans --- win_installer/src/app.rs | 3 +++ win_installer/src/components/wim_scan.rs | 34 +++++++++++++++--------- win_installer/src/state.rs | 24 ++++++++++++----- win_installer/src/wim.rs | 23 +++++++++++++++- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 401ccd2..c98d219 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -262,6 +262,9 @@ impl App { 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, diff --git a/win_installer/src/components/wim_scan.rs b/win_installer/src/components/wim_scan.rs index d2c6caf..cb5abc1 100644 --- a/win_installer/src/components/wim_scan.rs +++ b/win_installer/src/components/wim_scan.rs @@ -23,7 +23,7 @@ use color_eyre::Result; use crossterm::event::KeyEvent; use ratatui::{ prelude::*, - widgets::{Block, Borders, Clear, HighlightSpacing, List, ListItem, Padding, Paragraph}, + widgets::{Block, Borders, Clear, List, ListItem, Padding, Paragraph}, }; use tokio::sync::mpsc::UnboundedSender; @@ -111,12 +111,16 @@ impl Component for WimScan { if let Ok(wim_sources) = self.wim_sources.lock() { // Local let mut left_list = Vec::new(); - left_list.extend( - wim_sources - .local - .iter() - .map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))), - ); + 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) @@ -126,12 +130,16 @@ impl Component for WimScan { // Network let mut right_list = Vec::new(); - right_list.extend(wim_sources.network.iter().map(|wimfile| { - ListItem::new(format!( - "{}\n\n", - wimfile.path.split("\\").last().unwrap_or(&wimfile.path) - )) - })); + 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) diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 4c948a8..15536f6 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -90,17 +90,27 @@ impl State { 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(); - tokio::task::spawn(async move { - scan_local_drives(disk_list_arc, wim_sources_arc, scan_type); - }); + 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 config = self.config.clone(); let wim_sources_arc = self.wim_sources.clone(); - tokio::task::spawn(async move { - scan_network_share(config, wim_sources_arc); - }); + 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); + })); + } } } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index cb44c22..97117ef 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -1,4 +1,3 @@ -use core::system::disk::bytes_to_string; // This file is part of Deja-Vu. // // Deja-Vu is free software: you can redistribute it and/or modify it @@ -26,8 +25,11 @@ use std::{ }; use tempfile::NamedTempFile; +use tokio::task::JoinHandle; use xml::reader::{EventReader, XmlEvent}; +use core::system::disk::bytes_to_string; + static WIMINFO_EXE: LazyLock = LazyLock::new(|| { let program_files = PathBuf::from(env::var("PROGRAMFILES").expect("Failed to resolve %PROGRAMFILES%")); @@ -156,6 +158,8 @@ impl fmt::Display for WimImage { pub struct WimSources { pub local: Vec, pub network: Vec, + pub thread_local: Option>, + pub thread_network: Option>, } impl WimSources { @@ -187,6 +191,23 @@ impl WimSources { 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(); From cb7ba1a28596c9e33d13f7efb043956a961c276a Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 08:07:53 -0800 Subject: [PATCH 20/27] Invert backup/setup bool --- win_installer/src/app.rs | 4 ++-- win_installer/src/state.rs | 4 ++-- win_installer/src/wim.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index c98d219..b7f7178 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -158,7 +158,7 @@ impl App { && let Some(index) = self.state.wim_image_index { let image = wim_sources.get_file(index); - if !image.is_setup { + if image.is_backup { self.action_tx.send(Action::NextScreen)?; } } @@ -729,7 +729,7 @@ fn build_right_items(app: &App) -> Action { DVLine::blank(), ]); } - if wim_file.is_setup + if !wim_file.is_backup && let Some(username) = &app.state.username { label_dv_lines.append(&mut vec![DVLine { diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 15536f6..a6cf659 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -154,13 +154,13 @@ pub fn scan_local_drives( // Scan drives to_check.iter().for_each(|scan_path| { - let is_setup = scan_path.ends_with("\\Images"); + 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_setup) + && let Ok(new_source) = parse_wim_file(path_str, is_backup) { wim_files.push(new_source); } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 97117ef..6abfe8b 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -69,7 +69,7 @@ static WIN_BUILDS: LazyLock> = LazyLock::new(|| { pub struct WimFile { pub path: String, pub images: Vec, - pub is_setup: bool, + pub is_backup: bool, } impl WimFile { @@ -237,7 +237,7 @@ fn get_wim_xml(wim_file: &str) -> std::io::Result { Ok(file) } -pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result { +pub fn parse_wim_file(wim_file: &str, is_backup: bool) -> std::io::Result { let mut wim_images: Vec = Vec::new(); if !Path::new(wim_file).exists() { return Err(std::io::Error::new( @@ -316,7 +316,7 @@ pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result Date: Sat, 13 Dec 2025 08:09:12 -0800 Subject: [PATCH 21/27] Add win-installer partitioning/formatting logic --- core/src/system/diskpart.rs | 16 ++- deja_vu/src/app.rs | 8 +- win_installer/src/app.rs | 242 ++++++++++++++++++++++++++++++++---- 3 files changed, 242 insertions(+), 24 deletions(-) diff --git a/core/src/system/diskpart.rs b/core/src/system/diskpart.rs index 98fc74d..b1281a8 100644 --- a/core/src/system/diskpart.rs +++ b/core/src/system/diskpart.rs @@ -32,6 +32,12 @@ use crate::system::disk::{ static DEFAULT_MAX_DISKS: usize = 8; +#[derive(Debug, PartialEq)] +pub enum FormatUseCase { + ApplyWimImage, + Clone, +} + pub struct RegexList { detail_all_disks: OnceLock, detail_disk: OnceLock, @@ -205,7 +211,11 @@ pub fn get_partitions( } #[must_use] -pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType) -> String { +pub fn build_dest_format_script( + disk_id: usize, + part_type: &PartitionTableType, + format_use_case: FormatUseCase, +) -> String { let disk_id = format!("{disk_id}"); let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"]; match part_type { @@ -221,6 +231,10 @@ pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType) script.push("format fs=ntfs quick label=System"); } } + if format_use_case == FormatUseCase::ApplyWimImage { + script.push("create partition primary"); + script.push("format fs=ntfs quick label=Windows"); + } script.join("\r\n").replace("{disk_id}", &disk_id) } diff --git a/deja_vu/src/app.rs b/deja_vu/src/app.rs index 450f423..dc8ccd6 100644 --- a/deja_vu/src/app.rs +++ b/deja_vu/src/app.rs @@ -28,7 +28,10 @@ use core::{ line::{DVLine, get_disk_description_right, get_part_description}, state::Mode, system::{ - boot, cpu::get_cpu_name, disk::PartitionTableType, diskpart::build_dest_format_script, + boot, + cpu::get_cpu_name, + disk::PartitionTableType, + diskpart::{FormatUseCase, build_dest_format_script}, drivers, }, tasks::{Task, TaskResult, TaskType, Tasks}, @@ -209,7 +212,8 @@ impl App { && let Some(disk) = disk_list.get(disk_index) { let table_type = self.state.table_type.clone().unwrap(); - let diskpart_script = build_dest_format_script(disk.id, &table_type); + let diskpart_script = + build_dest_format_script(disk.id, &table_type, FormatUseCase::Clone); self.tasks.add(TaskType::Diskpart(diskpart_script)); } } diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index b7f7178..0a56f54 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -26,12 +26,20 @@ use core::{ config::Config, line::{DVLine, get_disk_description_right}, state::Mode, - system::{cpu::get_cpu_name, disk::PartitionTableType, drivers}, - tasks::{TaskType, Tasks}, + system::{ + boot, + cpu::get_cpu_name, + disk::PartitionTableType, + diskpart::{FormatUseCase, build_dest_format_script}, + drivers, + }, + tasks::{Task, TaskResult, TaskType, Tasks}, tui::{Event, Tui}, }; use std::{ + env, iter::zip, + path::PathBuf, sync::{Arc, Mutex}, }; @@ -111,20 +119,20 @@ impl App { 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::Confirm => Mode::PreClone, + Mode::PreClone => Mode::Clone, + Mode::Clone => Mode::PostClone, + Mode::PostClone | 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::Process | Mode::SelectParts | Mode::SetBootMode => panic!("This shouldn't happen?"), } @@ -139,6 +147,126 @@ impl App { // self.action_tx // .send(Action::DisplayPopup(popup::Type::Info, String::from("...")))?; // } + Mode::PreClone => { + self.action_tx.send(Action::DisplayPopup( + popup::Type::Info, + 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 + let disk_list = self.state.disk_list.lock().unwrap(); + if let Some(disk_index) = self.state.disk_index_dest + && let Some(disk) = disk_list.get(disk_index) + { + let table_type = self.state.table_type.clone().unwrap(); + let diskpart_script = build_dest_format_script( + disk.id, + &table_type, + FormatUseCase::ApplyWimImage, + ); + self.tasks.add(TaskType::Diskpart(diskpart_script)); + } + + // Update drive letters + self.tasks.add(TaskType::Sleep); + if let Some(dest_index) = self.state.disk_index_dest { + self.tasks.add(TaskType::UpdateDestDisk(dest_index)); + } + } + Mode::Clone => { + self.action_tx.send(Action::DisplayPopup( + popup::Type::Info, + String::from("Applying Image"), + ))?; + + // Get wimlib-imagex path + let program_files = PathBuf::from(get_program_files_path(&self.action_tx)); + let wimlib_imagex = program_files.join("wimlib\\wimlib-imagex.exe"); + + // Get image info + let wim_sources = self.state.wim_sources.lock().unwrap(); + let wim_file = wim_sources.get_file(self.state.wim_file_index.unwrap()); + let wim_index = format!("{}", self.state.wim_image_index.unwrap()); + + // Add actions + 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 num_parts = disk.parts.len(); + let dest_path = format!("{}:\\", disk.get_part_letter(num_parts - 1)); + self.tasks.add(TaskType::CommandWait( + wimlib_imagex, + vec![String::from("apply"), wim_file.path, wim_index, dest_path], + )); + } + } + Mode::PostClone => { + self.action_tx.send(Action::DisplayPopup( + popup::Type::Info, + String::from("Updating boot configuration"), + ))?; + + // Get System32 path + let system32 = get_system32_path(&self.action_tx); + + // Add actions + let disk_list = self.state.disk_list.lock().unwrap(); + if let Some(disk_index) = self.state.disk_index_dest + && let Some(disk) = disk_list.get(disk_index) + { + let table_type = self.state.table_type.clone().unwrap(); + let letter_boot = disk.get_part_letter(0); + let letter_os = + disk.get_part_letter(match self.state.table_type.clone().unwrap() { + PartitionTableType::Guid => 2, + PartitionTableType::Legacy => 1, + }); + info!("PostClone // Disk: {disk:?}"); + info!("\t\tBoot letter: {letter_boot}"); + info!("\t\tOS letter: {letter_os}"); + + // Safety check + if letter_boot.is_empty() || letter_os.is_empty() { + self.action_tx.send(Action::Error(String::from( + "ERROR\n\n\nFailed to get drive letters for the destination", + )))?; + return Ok(()); + } + + // Create boot files + for task in boot::configure_disk( + &letter_boot, + &letter_os, + boot::SafeMode::Enable, + &system32, + &table_type, + ) { + self.tasks.add(task); + } + + // Inject driver(s) (if selected) + if let Some(driver) = &self.state.driver { + if let Ok(task) = boot::inject_driver(driver, &letter_os, &system32) { + self.tasks.add(task); + } else { + self.action_tx.send(Action::Error(format!( + "Failed to inject driver:\n{}", + driver.name + )))?; + } + } + } + } Mode::ScanDisks => { self.state.reset_all(); if self.tasks.idle() { @@ -261,7 +389,9 @@ impl App { Action::Tick => { self.last_tick_key_events.drain(..); // Check background task(s) - self.tasks.poll()?; + if let Some(task) = self.tasks.poll()? { + self.handle_task(&task)?; + } if let Ok(mut wim_sources) = self.state.wim_sources.lock() { wim_sources.poll(); } @@ -317,6 +447,11 @@ impl App { self.action_tx.send(Action::DismissPopup)?; self.action_tx.send(Action::SetMode(next_mode))?; } + Action::DisplayPopup(ref popup_type, ref _popup_text) => { + if *popup_type == popup::Type::Error { + self.action_tx.send(Action::SetMode(Mode::Failed))?; + } + } Action::PrevScreen => match self.cur_mode { Mode::SelectTableType => { self.action_tx.send(Action::SetMode(Mode::SelectDisks))?; @@ -405,6 +540,39 @@ impl App { 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<()> { tui.draw(|frame| { if let [header, _body, footer, center, left, right, username, popup] = @@ -426,7 +594,9 @@ impl App { fn build_footer_string(cur_mode: Mode) -> String { match cur_mode { - Mode::Home | Mode::ScanDisks => String::from("(q) to quit"), + Mode::Home | Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { + 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", @@ -440,18 +610,16 @@ fn build_footer_string(cur_mode: Mode) -> String { ), 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"), + Mode::Done | Mode::Failed => 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::Process | Mode::SelectParts | Mode::SetBootMode => panic!("This shouldn't happen?"), } @@ -475,15 +643,9 @@ fn build_left_items(app: &App) -> Action { .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; @@ -539,6 +701,7 @@ fn build_left_items(app: &App) -> Action { | Mode::BootScan | Mode::BootSetup | Mode::DiagMenu + | Mode::Process | Mode::InjectDrivers | Mode::LogView | Mode::PEMenu @@ -653,7 +816,12 @@ fn build_right_items(app: &App) -> Action { }); } } - Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { + Mode::SelectWinImage + | Mode::SetUserName + | Mode::Confirm + | Mode::PreClone + | Mode::Clone + | Mode::PostClone => { info!("Building right items for: {:?}", &app.cur_mode); let wim_file; if let Ok(wim_sources) = app.state.wim_sources.lock() @@ -717,7 +885,7 @@ fn build_right_items(app: &App) -> Action { }]) }); } - Mode::Confirm => { + Mode::Confirm | Mode::PreClone | Mode::Clone | Mode::PostClone => { if let Some(index) = app.state.wim_image_index && let Some(image) = wim_file.images.get(index) { @@ -842,3 +1010,35 @@ fn get_chunks(r: Rect) -> Vec { // Done chunks } + +pub fn get_program_files_path(action_tx: &mpsc::UnboundedSender) -> String { + let mut program_files_path = String::from("."); + if cfg!(windows) { + if let Ok(path) = env::var("PROGRAMFILES") { + program_files_path = path; + } else { + action_tx + .send(Action::Error(String::from( + "ERROR\n\n\nFailed to find PROGRAMFILES", + ))) + .expect("Failed to find PROGRAMFILES and then failed to send action"); + } + } + program_files_path +} + +pub fn get_system32_path(action_tx: &mpsc::UnboundedSender) -> 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 +} From e5f476f48d3f94e21ab38b9a8680ea1b74b3c3e1 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 08:09:49 -0800 Subject: [PATCH 22/27] Add more log/display info --- core/src/components/popup.rs | 2 ++ core/src/line.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/core/src/components/popup.rs b/core/src/components/popup.rs index 742f9e0..dabaeab 100644 --- a/core/src/components/popup.rs +++ b/core/src/components/popup.rs @@ -22,6 +22,7 @@ use ratatui::{ use serde::{Deserialize, Serialize}; use strum::Display; use tokio::sync::mpsc::UnboundedSender; +use tracing::info; use super::Component; use crate::{action::Action, config::Config}; @@ -64,6 +65,7 @@ impl Component for Popup { match action { Action::DismissPopup => self.popup_text.clear(), Action::DisplayPopup(new_type, new_text) => { + info!("Show Popup ({new_type}): {new_text}"); self.popup_type = new_type; self.popup_text = format!("\n{new_text}"); } diff --git a/core/src/line.rs b/core/src/line.rs index 18fbdf6..7b87d41 100644 --- a/core/src/line.rs +++ b/core/src/line.rs @@ -96,6 +96,12 @@ pub fn get_disk_description_right( line_colors, }); }); + if disk.parts_description.is_empty() { + description.push(DVLine { + line_parts: vec![String::from("-None-")], + line_colors: vec![Color::Reset], + }); + } description } From 8a65313039a40030dd6f6f79ea77fad828028a95 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 08:10:38 -0800 Subject: [PATCH 23/27] Fix bug dropping selected disk info We don't need to reset disk_index_dest to None since it would be reset before it's used anyway --- win_installer/src/state.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index a6cf659..3962dca 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -63,7 +63,6 @@ impl State { } 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() { From 4a306b56d9d8e961564d0a251c5969ec695d69c6 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 16:57:10 -0800 Subject: [PATCH 24/27] Fix zero-based vs one-based indexing --- win_installer/src/app.rs | 9 +++++++-- win_installer/src/wim.rs | 15 +++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 0a56f54..a430e50 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -195,7 +195,7 @@ impl App { // Get image info let wim_sources = self.state.wim_sources.lock().unwrap(); let wim_file = wim_sources.get_file(self.state.wim_file_index.unwrap()); - let wim_index = format!("{}", self.state.wim_image_index.unwrap()); + let wim_index = self.state.wim_image_index.unwrap() + 1; // wimapply uses 1-based index // Add actions let disk_list = self.state.disk_list.lock().unwrap(); @@ -206,7 +206,12 @@ impl App { let dest_path = format!("{}:\\", disk.get_part_letter(num_parts - 1)); self.tasks.add(TaskType::CommandWait( wimlib_imagex, - vec![String::from("apply"), wim_file.path, wim_index, dest_path], + vec![ + String::from("apply"), + wim_file.path, + format!("{wim_index}"), + dest_path, + ], )); } } diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index 6abfe8b..bdb5bca 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -176,13 +176,20 @@ impl WimSources { } pub fn get_file(&self, index: usize) -> WimFile { + let rel_index: usize; let num_local = self.local.len(); - let index = if index < num_local { - index + let mut use_local = true; + if index < num_local { + rel_index = index; } else { - index - num_local + rel_index = index - num_local; + use_local = false; }; - self.local.get(index).unwrap().clone() + 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 { From 8495d62a06caddc6b3eec9ca66151d641c0fc518 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 16:58:47 -0800 Subject: [PATCH 25/27] Add unattend.xml sections --- config/unattend.xml | 83 ++++++++++++++++++++++++++++++++++++++++ win_installer/src/app.rs | 30 ++++++++++++++- win_installer/src/wim.rs | 6 +++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100755 config/unattend.xml diff --git a/config/unattend.xml b/config/unattend.xml new file mode 100755 index 0000000..fe7ea54 --- /dev/null +++ b/config/unattend.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + 1 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f + + + 2 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f + + + 3 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f + + + + + + + + + 1 + reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f + + + + + + + + 3 + + + + + NEWUSERNAME + NEWUSERNAME + Administrators;Power Users + + UABhAHMAcwB3AG8AcgBkAA== + 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> diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index a430e50..c6d5773 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -38,6 +38,8 @@ use core::{ }; use std::{ env, + fs::{File, create_dir_all}, + io::Write, iter::zip, path::PathBuf, sync::{Arc, Mutex}, @@ -51,11 +53,12 @@ use ratatui::{ style::Color, }; use tokio::sync::mpsc; -use tracing::{debug, info}; +use tracing::{debug, error, info}; use crate::{ components::{set_username::InputUsername, wim_scan::WimScan}, state::{ScanType, State}, + wim::gen_unattend_xml, }; pub struct App { @@ -220,6 +223,8 @@ impl App { popup::Type::Info, String::from("Updating boot configuration"), ))?; + let wim_sources = self.state.wim_sources.lock().unwrap(); + let wim_file = wim_sources.get_file(self.state.wim_file_index.unwrap()); // Get System32 path let system32 = get_system32_path(&self.action_tx); @@ -270,6 +275,29 @@ impl App { )))?; } } + + // Add unattend.xml (if applicable) + if let Some(username) = &self.state.username + && !wim_file.is_backup + { + let unattend_xml_str = gen_unattend_xml(username); + let panther_path = format!("{letter_os}:\\Windows\\Panther"); + if create_dir_all(PathBuf::from(&panther_path)).is_ok() { + if let Ok(mut unattend_xml) = + File::create(format!("{panther_path}\\unattend.xml")) + { + if unattend_xml.write_all(unattend_xml_str.as_bytes()).is_ok() { + info!("Created unattend.xml with username set to: {username}"); + } else { + error!("Failed to write to unattend.xml"); + } + } else { + error!("Failed to create unattend.xml"); + } + } else { + error!("Failed to create Panther dir"); + } + } } } Mode::ScanDisks => { diff --git a/win_installer/src/wim.rs b/win_installer/src/wim.rs index bdb5bca..433d0aa 100644 --- a/win_installer/src/wim.rs +++ b/win_installer/src/wim.rs @@ -30,6 +30,8 @@ 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%")); @@ -229,6 +231,10 @@ impl WimSources { } } +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) From b8fe43fd8fe91574a5240310d206a8339c4fd488 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 16:59:19 -0800 Subject: [PATCH 26/27] Lock is Safe Mode only for backup restores --- win_installer/src/app.rs | 7 ++++++- win_installer/src/state.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index c6d5773..c634696 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -254,10 +254,15 @@ impl App { } // Create boot files + let safe_mode = if wim_file.is_backup { + boot::SafeMode::Enable + } else { + boot::SafeMode::Disable + }; for task in boot::configure_disk( &letter_boot, &letter_os, - boot::SafeMode::Enable, + safe_mode, &system32, &table_type, ) { diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index 3962dca..fea2a26 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -197,7 +197,7 @@ pub fn scan_network_share(config: Config, wim_sources_arc: Arc<Mutex<WimSources> 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) + && let Ok(new_source) = parse_wim_file(path_str, false) // Assuming all network sources are installers { wim_files.push(new_source); From 3b975af2baa1b6bb0666033dfdda459c29bc4d43 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 13 Dec 2025 16:59:29 -0800 Subject: [PATCH 27/27] Fix network share connection --- win_installer/src/net.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/win_installer/src/net.rs b/win_installer/src/net.rs index ea24c0c..21990de 100644 --- a/win_installer/src/net.rs +++ b/win_installer/src/net.rs @@ -44,7 +44,8 @@ pub fn connect_network_share( lpRemoteName: remote_name.as_c_str().as_ptr() as *mut u8, }; - let username = to_cstr(username); + let username = format!("{server}\\{username}"); + let username = to_cstr(&username); let password = to_cstr(password); // mount