Compare commits

..

4 commits

Author SHA1 Message Date
8a65313039
Fix bug dropping selected disk info
We don't need to reset disk_index_dest to None since it would be reset
before it's used anyway
2025-12-13 08:10:38 -08:00
e5f476f48d
Add more log/display info 2025-12-13 08:09:49 -08:00
a75911cb32
Add win-installer partitioning/formatting logic 2025-12-13 08:09:12 -08:00
cb7ba1a285
Invert backup/setup bool 2025-12-13 08:07:53 -08:00
7 changed files with 257 additions and 32 deletions

View file

@ -22,6 +22,7 @@ use ratatui::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tracing::info;
use super::Component; use super::Component;
use crate::{action::Action, config::Config}; use crate::{action::Action, config::Config};
@ -64,6 +65,7 @@ impl Component for Popup {
match action { match action {
Action::DismissPopup => self.popup_text.clear(), Action::DismissPopup => self.popup_text.clear(),
Action::DisplayPopup(new_type, new_text) => { Action::DisplayPopup(new_type, new_text) => {
info!("Show Popup ({new_type}): {new_text}");
self.popup_type = new_type; self.popup_type = new_type;
self.popup_text = format!("\n{new_text}"); self.popup_text = format!("\n{new_text}");
} }

View file

@ -96,6 +96,12 @@ pub fn get_disk_description_right(
line_colors, line_colors,
}); });
}); });
if disk.parts_description.is_empty() {
description.push(DVLine {
line_parts: vec![String::from("-None-")],
line_colors: vec![Color::Reset],
});
}
description description
} }

View file

@ -32,6 +32,12 @@ use crate::system::disk::{
static DEFAULT_MAX_DISKS: usize = 8; static DEFAULT_MAX_DISKS: usize = 8;
#[derive(Debug, PartialEq)]
pub enum FormatUseCase {
ApplyWimImage,
Clone,
}
pub struct RegexList { pub struct RegexList {
detail_all_disks: OnceLock<Regex>, detail_all_disks: OnceLock<Regex>,
detail_disk: OnceLock<Regex>, detail_disk: OnceLock<Regex>,
@ -205,7 +211,11 @@ pub fn get_partitions(
} }
#[must_use] #[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 disk_id = format!("{disk_id}");
let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"]; let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"];
match part_type { 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"); script.push("format fs=ntfs quick label=System");
} }
} }
if format_use_case == FormatUseCase::ApplyWimImage {
script.push("create partition primary");
script.push("format fs=ntfs quick label=Windows");
}
script.join("\r\n").replace("{disk_id}", &disk_id) script.join("\r\n").replace("{disk_id}", &disk_id)
} }

View file

@ -28,7 +28,10 @@ use core::{
line::{DVLine, get_disk_description_right, get_part_description}, line::{DVLine, get_disk_description_right, get_part_description},
state::Mode, state::Mode,
system::{ 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, drivers,
}, },
tasks::{Task, TaskResult, TaskType, Tasks}, tasks::{Task, TaskResult, TaskType, Tasks},
@ -209,7 +212,8 @@ impl App {
&& let Some(disk) = disk_list.get(disk_index) && let Some(disk) = disk_list.get(disk_index)
{ {
let table_type = self.state.table_type.clone().unwrap(); let table_type = self.state.table_type.clone().unwrap();
let diskpart_script = build_dest_format_script(disk.id, &table_type); let diskpart_script =
build_dest_format_script(disk.id, &table_type, FormatUseCase::Clone);
self.tasks.add(TaskType::Diskpart(diskpart_script)); self.tasks.add(TaskType::Diskpart(diskpart_script));
} }
} }

View file

@ -26,12 +26,20 @@ use core::{
config::Config, config::Config,
line::{DVLine, get_disk_description_right}, line::{DVLine, get_disk_description_right},
state::Mode, state::Mode,
system::{cpu::get_cpu_name, disk::PartitionTableType, drivers}, system::{
tasks::{TaskType, Tasks}, boot,
cpu::get_cpu_name,
disk::PartitionTableType,
diskpart::{FormatUseCase, build_dest_format_script},
drivers,
},
tasks::{Task, TaskResult, TaskType, Tasks},
tui::{Event, Tui}, tui::{Event, Tui},
}; };
use std::{ use std::{
env,
iter::zip, iter::zip,
path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -111,20 +119,20 @@ impl App {
Mode::SelectWinSource => Mode::SelectWinImage, Mode::SelectWinSource => Mode::SelectWinImage,
Mode::SelectWinImage => Mode::SetUserName, Mode::SelectWinImage => Mode::SetUserName,
Mode::SetUserName => Mode::Confirm, Mode::SetUserName => Mode::Confirm,
Mode::Confirm => Mode::Process, // i.e. format, apply, etc Mode::Confirm => Mode::PreClone,
Mode::Process | Mode::Done => Mode::Done, Mode::PreClone => Mode::Clone,
Mode::Clone => Mode::PostClone,
Mode::PostClone | Mode::Done => Mode::Done,
Mode::Failed => Mode::Failed, Mode::Failed => Mode::Failed,
// Invalid States // Invalid States
Mode::BootDiags Mode::BootDiags
| Mode::BootScan | Mode::BootScan
| Mode::BootSetup | Mode::BootSetup
| Mode::Clone
| Mode::DiagMenu | Mode::DiagMenu
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::PreClone | Mode::Process
| Mode::PostClone
| Mode::SelectParts | Mode::SelectParts
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
} }
@ -139,6 +147,126 @@ impl App {
// self.action_tx // self.action_tx
// .send(Action::DisplayPopup(popup::Type::Info, String::from("...")))?; // .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 => { Mode::ScanDisks => {
self.state.reset_all(); self.state.reset_all();
if self.tasks.idle() { if self.tasks.idle() {
@ -158,7 +286,7 @@ impl App {
&& let Some(index) = self.state.wim_image_index && let Some(index) = self.state.wim_image_index
{ {
let image = wim_sources.get_file(index); let image = wim_sources.get_file(index);
if !image.is_setup { if image.is_backup {
self.action_tx.send(Action::NextScreen)?; self.action_tx.send(Action::NextScreen)?;
} }
} }
@ -261,7 +389,9 @@ impl App {
Action::Tick => { Action::Tick => {
self.last_tick_key_events.drain(..); self.last_tick_key_events.drain(..);
// Check background task(s) // 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() { if let Ok(mut wim_sources) = self.state.wim_sources.lock() {
wim_sources.poll(); wim_sources.poll();
} }
@ -317,6 +447,11 @@ impl App {
self.action_tx.send(Action::DismissPopup)?; self.action_tx.send(Action::DismissPopup)?;
self.action_tx.send(Action::SetMode(next_mode))?; 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 { Action::PrevScreen => match self.cur_mode {
Mode::SelectTableType => { Mode::SelectTableType => {
self.action_tx.send(Action::SetMode(Mode::SelectDisks))?; self.action_tx.send(Action::SetMode(Mode::SelectDisks))?;
@ -405,6 +540,39 @@ impl App {
Ok(()) Ok(())
} }
fn handle_task(&mut self, task: &Task) -> Result<()> {
match task.task_type {
TaskType::CommandWait(_, _) | TaskType::Diskpart(_) => {
// Check result
if let Some(result) = &task.result {
match result {
TaskResult::Error(msg) => {
self.action_tx
.send(Action::Error(format!("{} Failed: {msg}", task.task_type)))?;
}
TaskResult::Output(stdout, stderr, success) => {
if !success {
let msg = if !stdout.is_empty() {
stdout.clone()
} else if !stderr.is_empty() {
stderr.clone()
} else {
String::from("Unknown Error")
};
self.action_tx.send(Action::Error(format!(
"{} Failed: {msg}",
task.task_type
)))?;
}
}
}
}
}
_ => {}
}
Ok(())
}
fn render(&mut self, tui: &mut Tui) -> Result<()> { fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| { tui.draw(|frame| {
if let [header, _body, footer, center, left, right, username, popup] = if let [header, _body, footer, center, left, right, username, popup] =
@ -426,7 +594,9 @@ impl App {
fn build_footer_string(cur_mode: Mode) -> String { fn build_footer_string(cur_mode: Mode) -> String {
match cur_mode { match cur_mode {
Mode::Home | Mode::ScanDisks => 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::InstallDrivers => String::from("(Enter) to select / (q) to quit"),
Mode::SelectDisks => String::from( Mode::SelectDisks => String::from(
"(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit", "(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit",
@ -440,18 +610,16 @@ fn build_footer_string(cur_mode: Mode) -> String {
), ),
Mode::SetUserName => String::from("(Enter) to continue / (Esc) to go back"), 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::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 // Invalid States
Mode::BootDiags Mode::BootDiags
| Mode::BootScan | Mode::BootScan
| Mode::BootSetup | Mode::BootSetup
| Mode::Clone
| Mode::DiagMenu | Mode::DiagMenu
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::PreClone | Mode::Process
| Mode::PostClone
| Mode::SelectParts | Mode::SelectParts
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
} }
@ -475,15 +643,9 @@ fn build_left_items(app: &App) -> Action {
.iter() .iter()
.for_each(|driver| items.push(driver.to_string())); .for_each(|driver| items.push(driver.to_string()));
} }
Mode::Process => {
select_type = SelectionType::Loop;
title = String::from("Processing");
// TODO: FIXME
}
Mode::ScanWinSources => { Mode::ScanWinSources => {
select_type = SelectionType::Loop; select_type = SelectionType::Loop;
title = String::from("Scanning"); title = String::from("Scanning");
// TODO: FIXME
} }
Mode::SelectWinSource => { Mode::SelectWinSource => {
select_type = SelectionType::One; select_type = SelectionType::One;
@ -539,6 +701,7 @@ fn build_left_items(app: &App) -> Action {
| Mode::BootScan | Mode::BootScan
| Mode::BootSetup | Mode::BootSetup
| Mode::DiagMenu | Mode::DiagMenu
| Mode::Process
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView | Mode::LogView
| Mode::PEMenu | 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); info!("Building right items for: {:?}", &app.cur_mode);
let wim_file; let wim_file;
if let Ok(wim_sources) = app.state.wim_sources.lock() 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 if let Some(index) = app.state.wim_image_index
&& let Some(image) = wim_file.images.get(index) && let Some(image) = wim_file.images.get(index)
{ {
@ -729,7 +897,7 @@ fn build_right_items(app: &App) -> Action {
DVLine::blank(), DVLine::blank(),
]); ]);
} }
if wim_file.is_setup if !wim_file.is_backup
&& let Some(username) = &app.state.username && let Some(username) = &app.state.username
{ {
label_dv_lines.append(&mut vec![DVLine { label_dv_lines.append(&mut vec![DVLine {
@ -842,3 +1010,35 @@ fn get_chunks(r: Rect) -> Vec<Rect> {
// Done // Done
chunks chunks
} }
pub fn get_program_files_path(action_tx: &mpsc::UnboundedSender<Action>) -> 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<Action>) -> String {
let mut system32_path = String::from(".");
if cfg!(windows) {
if let Ok(path) = env::var("SYSTEMROOT") {
system32_path = format!("{path}/System32");
} else {
action_tx
.send(Action::Error(String::from(
"ERROR\n\n\nFailed to find SYSTEMROOT",
)))
.expect("Failed to find SYSTEMROOT and then failed to send action");
}
}
system32_path
}

View file

@ -63,7 +63,6 @@ impl State {
} }
pub fn reset_all(&mut self) { pub fn reset_all(&mut self) {
self.disk_index_dest = None;
self.wim_file_index = None; self.wim_file_index = None;
self.wim_image_index = None; self.wim_image_index = None;
if let Ok(mut sources) = self.wim_sources.lock() { if let Ok(mut sources) = self.wim_sources.lock() {
@ -154,13 +153,13 @@ pub fn scan_local_drives(
// Scan drives // Scan drives
to_check.iter().for_each(|scan_path| { 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) { if let Ok(read_dir) = read_dir(scan_path) {
read_dir.for_each(|item| { read_dir.for_each(|item| {
if let Ok(item) = item if let Ok(item) = item
&& item.file_name().to_string_lossy().ends_with(".wim") && item.file_name().to_string_lossy().ends_with(".wim")
&& let Some(path_str) = item.path().to_str() && 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); wim_files.push(new_source);
} }

View file

@ -69,7 +69,7 @@ static WIN_BUILDS: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
pub struct WimFile { pub struct WimFile {
pub path: String, pub path: String,
pub images: Vec<WimImage>, pub images: Vec<WimImage>,
pub is_setup: bool, pub is_backup: bool,
} }
impl WimFile { impl WimFile {
@ -237,7 +237,7 @@ fn get_wim_xml(wim_file: &str) -> std::io::Result<File> {
Ok(file) Ok(file)
} }
pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result<WimFile> { pub fn parse_wim_file(wim_file: &str, is_backup: bool) -> std::io::Result<WimFile> {
let mut wim_images: Vec<WimImage> = Vec::new(); let mut wim_images: Vec<WimImage> = Vec::new();
if !Path::new(wim_file).exists() { if !Path::new(wim_file).exists() {
return Err(std::io::Error::new( return Err(std::io::Error::new(
@ -316,7 +316,7 @@ pub fn parse_wim_file(wim_file: &str, is_setup: bool) -> std::io::Result<WimFile
let wim_file = WimFile { let wim_file = WimFile {
path: wim_file.to_string(), path: wim_file.to_string(),
images: wim_images, images: wim_images,
is_setup, is_backup,
}; };
Ok(wim_file) Ok(wim_file)