Compare commits

...

7 commits

17 changed files with 767 additions and 134 deletions

31
Cargo.lock generated
View file

@ -689,7 +689,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -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,7 +3154,9 @@ dependencies = [
"tracing",
"tracing-error",
"tracing-subscriber",
"tui-input",
"vergen-gix",
"windows-sys 0.61.2",
"xml",
]
@ -3198,6 +3210,12 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3216,6 +3234,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"

View file

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

View file

@ -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?")

View file

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

View file

@ -56,7 +56,9 @@ pub enum Action {
Restart,
Shutdown,
// App (Win-Installer)
FindWimBackups,
FindWimNetwork,
SetUserName(String),
// Screens
DismissPopup,
DisplayPopup(PopupType, String),

View file

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

View file

@ -43,7 +43,8 @@ pub enum Mode {
SelectParts,
PostClone,
// Windows Installer
ScanWinImages,
ScanWinSources,
SelectWinSource,
SelectWinImage,
SetUserName,
// WinPE

View file

@ -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?"),

View file

@ -41,7 +41,9 @@ tracing = "0.1.41"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tempfile = "3.23.0"
windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] }
xml = "1.1.0"
tui-input = "0.14.0"
[build-dependencies]
anyhow = "1.0.86"

View file

@ -45,7 +45,10 @@ 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::{set_username::InputUsername, wim_scan::WimScan},
state::{ScanType, State},
};
pub struct App {
// TUI
@ -67,9 +70,10 @@ pub struct App {
impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let config = Config::new()?;
let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
let state = State::new(disk_list_arc);
let state = State::new(config.clone(), disk_list_arc);
let wim_sources = Arc::clone(&state.wim_sources);
Ok(Self {
// TUI
@ -80,10 +84,11 @@ 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()),
],
config: Config::new()?,
config,
frame_rate,
last_tick_key_events: Vec::new(),
should_quit: false,
@ -101,9 +106,22 @@ 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::SelectWinImage => Mode::SetUserName,
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::SetUserName => Mode::Confirm,
Mode::Confirm => Mode::Process, // i.e. format, apply, etc
Mode::Process | Mode::Done => Mode::Done,
@ -143,13 +161,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::ScanWinSources => self.state.scan_wim_local(ScanType::WindowsInstallers),
Mode::Done => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?;
@ -288,9 +300,13 @@ 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();
// TODO: Actually scan network!
self.state.scan_wim_network();
}
Action::NextScreen => {
let next_mode = self.next_mode();
@ -301,13 +317,17 @@ impl App {
Mode::SelectTableType => {
self.action_tx.send(Action::SetMode(Mode::SelectDisks))?;
}
Mode::SetUserName => {
self.action_tx.send(Action::SetMode(Mode::SelectWinImage))?;
Mode::SelectWinSource => {
self.action_tx.send(Action::SetMode(Mode::ScanWinSources))?;
}
Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => {
self.action_tx
.send(Action::SetMode(Mode::SelectWinSource))?;
}
_ => {}
},
Action::Process => match self.cur_mode {
Mode::Confirm | Mode::ScanWinImages => {
Mode::Confirm | Mode::ScanWinSources => {
self.action_tx.send(Action::NextScreen)?;
}
Mode::Done => {
@ -343,11 +363,11 @@ impl App {
}
}
}
Mode::SelectWinSource => {
self.state.wim_file_index = one;
}
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;
self.state.wim_image_index = one;
}
_ => {}
},
@ -359,6 +379,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)?,
_ => {}
}
@ -379,10 +403,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
@ -404,10 +428,12 @@ 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::SelectWinImage => String::from("(Enter) to select / (q) to quit"),
Mode::ScanWinImages => {
String::from("(Enter) to continue / (n) to scan network / (q) to quit")
Mode::SelectWinSource | Mode::SelectWinImage => {
String::from("(Enter) to select / (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"),
@ -450,15 +476,33 @@ 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::SelectWinImage => {
Mode::SelectWinSource => {
select_type = SelectionType::One;
title = String::from("Select Windows Source");
if let Ok(wim_sources) = app.state.wim_sources.lock() {
wim_sources
.get_file_list()
.iter()
.for_each(|wim_file| items.push(wim_file.path.clone()));
}
}
Mode::SelectWinImage | Mode::SetUserName => {
select_type = SelectionType::One;
title = String::from("Select Windows Image");
// TODO: FIXME, I think this whole section could be better...
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;
@ -474,11 +518,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");
@ -551,7 +590,158 @@ 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],
},
DVLine::blank(),
];
source.images.iter().for_each(|image| {
wim_dv_lines.push(DVLine {
line_parts: vec![format!("{image}")],
line_colors: vec![Color::Reset],
})
});
items.push(wim_dv_lines);
});
}
}
Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => {
info!("Building right items for: {:?}", &app.cur_mode);
let wim_file;
if let Ok(wim_sources) = app.state.wim_sources.lock()
&& let Some(index) = app.state.wim_file_index
{
wim_file = wim_sources.get_file(index);
} else {
panic!("Failed to get source WIM file");
}
// Disk Info
let type_str = match app.state.table_type.clone().unwrap() {
PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR",
};
let mut label_dv_lines = vec![
DVLine {
line_parts: vec![
String::from("Dest"),
String::from(" (WARNING: ALL DATA WILL BE DELETED!)"),
],
line_colors: vec![Color::Cyan, Color::Red],
},
DVLine {
line_parts: vec![format!(" (Will be formatted {type_str})")],
line_colors: vec![Color::Yellow],
},
DVLine::blank(),
];
let disk_list = app.state.disk_list.lock().unwrap();
if let Some(index) = app.state.disk_index_dest
&& let Some(disk) = disk_list.get(index)
{
get_disk_description_right(disk, &None)
.into_iter()
.for_each(|dv_line| label_dv_lines.push(dv_line));
}
label_dv_lines.append(&mut vec![
DVLine::blank(),
DVLine {
line_parts: vec![String::from("WIM Info")],
line_colors: vec![Color::Cyan],
},
DVLine {
line_parts: vec![wim_file.path.clone()],
line_colors: vec![Color::Reset],
},
DVLine::blank(),
DVLine {
line_parts: vec![String::from("Image")],
line_colors: vec![Color::Blue],
},
]);
// WIM Info
match app.cur_mode {
Mode::SelectWinImage => {
wim_file.images.iter().for_each(|image| {
items.push(vec![DVLine {
line_parts: vec![format!("{image}")],
line_colors: vec![Color::Reset],
}])
});
}
Mode::Confirm => {
if let Some(index) = app.state.wim_image_index
&& let Some(image) = wim_file.images.get(index)
{
label_dv_lines.append(&mut vec![
DVLine {
line_parts: vec![format!("{image}")],
line_colors: vec![Color::Reset],
},
DVLine::blank(),
]);
}
if wim_file.is_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],
}]);
}
items.push(vec![DVLine::blank()]);
}
_ => {}
}
// Done
labels.push(label_dv_lines);
}
Mode::SelectTableType => {
// Labels
let dest_dv_line = DVLine {
line_parts: vec![
@ -639,6 +829,9 @@ fn get_chunks(r: Rect) -> Vec<Rect> {
// Center
chunks.push(center);
// Set username
chunks.push(centered_rect(60, 20, r));
// Popup
chunks.push(centered_rect(60, 25, r));

View file

@ -13,4 +13,5 @@
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
pub mod set_username;
pub mod wim_scan;

View file

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

View file

@ -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);
}

View file

@ -20,6 +20,7 @@ use crate::app::App;
mod app;
mod components;
mod net;
mod state;
mod wim;

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

@ -0,0 +1,68 @@
// This file is part of Deja-Vu.
//
// Deja-Vu is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Deja-Vu is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
//#![windows_subsystem = "windows"]
use std::ffi::CString;
use windows_sys::Win32::Foundation::NO_ERROR;
use windows_sys::Win32::NetworkManagement::WNet;
fn to_cstr(s: &str) -> CString {
CString::new(s).unwrap()
}
pub fn connect_network_share(
server: &str,
share: &str,
username: &str,
password: &str,
) -> Result<(), u32> {
let remote_name = to_cstr(&format!("\\\\{server}\\{share}"));
// init resources
let mut resources = WNet::NETRESOURCEA {
dwDisplayType: WNet::RESOURCEDISPLAYTYPE_SHAREADMIN,
dwScope: WNet::RESOURCE_GLOBALNET,
dwType: WNet::RESOURCETYPE_DISK,
dwUsage: WNet::RESOURCEUSAGE_ALL,
lpComment: std::ptr::null_mut(),
lpLocalName: std::ptr::null_mut(), // PUT a volume here if you want to mount as a windows volume
lpProvider: std::ptr::null_mut(),
lpRemoteName: remote_name.as_c_str().as_ptr() as *mut u8,
};
let username = to_cstr(username);
let password = to_cstr(password);
// mount
let result = unsafe {
let username_ptr = username.as_ptr();
let password_ptr = password.as_ptr();
WNet::WNetAddConnection2A(
&mut resources as *mut WNet::NETRESOURCEA,
password_ptr as *const u8,
username_ptr as *const u8,
//WNet::CONNECT_INTERACTIVE, // Interactive will show a system dialog in case credentials are wrong to retry with the password. Put 0 if you don't want it
0,
)
};
if result == NO_ERROR {
Ok(())
} else {
Err(result)
}
}

View file

@ -14,33 +14,52 @@
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
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::{
config::Config,
system::{
disk::{Disk, PartitionTableType},
drivers,
},
};
use tracing::info;
use crate::{
net::connect_network_share,
wim::{WimFile, WimSources, parse_wim_file},
};
pub enum ScanType {
GeneralWimFiles, // Includes Windows installer WIMs
WindowsInstallers,
}
#[derive(Debug, Default)]
pub struct State {
pub config: Config,
pub disk_index_dest: Option<usize>,
pub disk_list: Arc<Mutex<Vec<Disk>>>,
pub driver: Option<drivers::Driver>,
pub driver_list: Vec<drivers::Driver>,
pub table_type: Option<PartitionTableType>,
pub username: Option<String>,
pub wim_file_index: Option<usize>,
pub wim_image_index: Option<usize>,
pub wim_sources: Arc<Mutex<WimSources>>,
}
impl State {
pub fn new(disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
let wim_sources = WimSources::new();
pub fn new(config: Config, disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
let wim_sources = Arc::new(Mutex::new(WimSources::new()));
State {
config,
disk_list,
wim_sources: Arc::new(Mutex::new(wim_sources)),
wim_sources,
..Default::default()
}
}
@ -54,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();
@ -63,4 +88,122 @@ impl State {
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
pub fn scan_wim_local(&mut self, scan_type: ScanType) {
let disk_list_arc = self.disk_list.clone();
let wim_sources_arc = self.wim_sources.clone();
tokio::task::spawn(async move {
scan_local_drives(disk_list_arc, wim_sources_arc, 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);
});
}
}
fn get_subfolders(path_str: &str) -> Vec<String> {
if let Ok(read_dir) = read_dir(path_str) {
read_dir
.filter_map(|item| item.ok())
.map(|item| item.path().to_string_lossy().into_owned())
.collect()
} else {
// TODO: Use better error handling here?
Vec::new()
}
}
pub fn scan_local_drives(
disk_list_arc: Arc<Mutex<Vec<Disk>>>,
wim_sources_arc: Arc<Mutex<WimSources>>,
scan_type: ScanType,
) {
let mut to_check = vec![String::from(".")];
let mut wim_files: Vec<WimFile> = Vec::new();
// Get drive letters
if let Ok(disk_list) = disk_list_arc.lock() {
disk_list.iter().for_each(|d| {
d.parts.iter().for_each(|p| {
if !p.letter.is_empty() {
match scan_type {
ScanType::GeneralWimFiles => {
to_check.append(&mut get_subfolders(&format!("{}:\\", &p.letter)));
}
ScanType::WindowsInstallers => {
to_check.push(format!("{}:\\Images", &p.letter));
}
}
}
});
})
}
// Scan drives
to_check.iter().for_each(|scan_path| {
let 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, installer)
{
wim_files.push(new_source);
}
});
}
});
// Done
wim_files.sort();
if let Ok(mut wim_sources) = wim_sources_arc.lock() {
wim_files
.into_iter()
.for_each(|file| wim_sources.add_local(file));
}
}
pub fn scan_network_share(config: Config, wim_sources_arc: Arc<Mutex<WimSources>>) {
let result = connect_network_share(
&config.network_server,
&config.network_share,
&config.network_user,
&config.network_pass,
);
let mut wim_files: Vec<WimFile> = Vec::new();
// Connect to share
if result.is_err() {
return;
}
// Scan share
let share_dir = format!("\\\\{}\\{}", &config.network_server, &config.network_share);
if let Ok(read_dir) = read_dir(share_dir) {
read_dir.for_each(|item| {
if let Ok(item) = item
&& item.file_name().to_string_lossy().ends_with(".wim")
&& let Some(path_str) = item.path().to_str()
&& let Ok(new_source) = parse_wim_file(path_str, true)
// Assuming all network sources are installers
{
wim_files.push(new_source);
}
});
}
// Done
wim_files.sort();
if let Ok(mut wim_sources) = wim_sources_arc.lock() {
wim_files
.into_iter()
.for_each(|file| wim_sources.add_network(file));
}
}

View file

@ -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
@ -15,19 +14,28 @@ use core::system::disk::Disk;
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
use std::{
cmp::Ordering,
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<String> = LazyLock::new(|| {
let program_files =
PathBuf::from(env::var("PROGRAMFILES").expect("Failed to resolve %PROGRAMFILES%"));
program_files
.join("wimlib/wiminfo.cmd")
.to_string_lossy()
.into_owned()
});
static WIN_BUILDS: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from([
// Windows 10
@ -54,10 +62,49 @@ static WIN_BUILDS: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
])
});
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct WimFile {
pub path: String,
pub images: Vec<WimImage>,
pub is_installer: bool,
}
impl WimFile {
pub fn summary(&self) -> String {
let mut s = format!("{self}");
self.images.iter().for_each(|image| {
let image = format!("\n\t\t{image}");
s.push_str(&image);
});
s
}
}
impl fmt::Display for WimFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.path.split("\\").last().unwrap_or(&self.path))
}
}
impl PartialEq for WimFile {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for WimFile {}
impl Ord for WimFile {
fn cmp(&self, other: &Self) -> Ordering {
self.path.cmp(&other.path)
}
}
impl PartialOrd for WimFile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Default)]
@ -106,11 +153,39 @@ impl WimSources {
Default::default()
}
pub fn add_local(&mut self, wim_file: WimFile) {
self.local.push(wim_file);
}
pub fn add_network(&mut self, wim_file: WimFile) {
self.network.push(wim_file);
}
pub fn get_file(&self, index: usize) -> WimFile {
let num_local = self.local.len();
let index = if index < num_local {
index
} else {
index - num_local
};
self.local.get(index).unwrap().clone()
}
pub fn get_file_list(&self) -> Vec<WimFile> {
let mut list = self.local.clone();
list.append(&mut self.network.clone());
list
}
pub fn reset_all(&mut self) {
self.local.clear();
self.network.clear();
}
pub fn reset_local(&mut self) {
self.local.clear();
}
pub fn reset_network(&mut self) {
self.network.clear();
}
@ -118,7 +193,7 @@ impl WimSources {
fn get_wim_xml(wim_file: &str) -> std::io::Result<File> {
let tmp_file = NamedTempFile::new()?;
let _ = Command::new("wiminfo")
let _ = Command::new(&*WIMINFO_EXE)
.args([
wim_file,
"--extract-xml",
@ -131,7 +206,7 @@ fn get_wim_xml(wim_file: &str) -> std::io::Result<File> {
Ok(file)
}
pub fn parse_wim_file(wim_file: &str) -> std::io::Result<WimFile> {
pub fn parse_wim_file(wim_file: &str, installer: bool) -> std::io::Result<WimFile> {
let mut wim_images: Vec<WimImage> = Vec::new();
if !Path::new(wim_file).exists() {
return Err(std::io::Error::new(
@ -196,42 +271,8 @@ pub fn parse_wim_file(wim_file: &str) -> std::io::Result<WimFile> {
let wim_file = WimFile {
path: wim_file.to_string(),
images: wim_images,
is_installer: installer,
};
Ok(wim_file)
}
pub fn scan_local_drives(
disk_list_arc: Arc<Mutex<Vec<Disk>>>,
wim_sources_arc: Arc<Mutex<WimSources>>,
) {
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);
}
});
}
});
}