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] 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