// 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, SelectionType}, popup, right::Right, title::Title, }, config::Config, line::{DVLine, get_disk_description_right}, state::Mode, system::{cpu::get_cpu_name, disk::PartitionTableType, drivers}, tasks::{TaskType, Tasks}, tui::{Event, Tui}, }; use std::{ iter::zip, sync::{Arc, Mutex}, }; use color_eyre::Result; use ratatui::{ crossterm::event::KeyEvent, layout::{Constraint, Direction, Layout}, prelude::Rect, style::Color, }; use tokio::sync::mpsc; use tracing::{debug, info}; use crate::{ components::{set_username::InputUsername, wim_scan::WimScan}, state::{ScanType, State}, }; pub struct App { // TUI action_rx: mpsc::UnboundedReceiver, action_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, cur_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 config = Config::new()?; let disk_list_arc = Arc::new(Mutex::new(Vec::new())); let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); let state = State::new(config.clone(), disk_list_arc); let wim_sources = Arc::clone(&state.wim_sources); Ok(Self { // TUI action_rx, action_tx, components: vec![ Box::new(Title::new("Windows Install Tool")), Box::new(Left::new()), Box::new(Right::new()), Box::new(WimScan::new(wim_sources)), Box::new(InputUsername::new()), Box::new(Footer::new()), Box::new(popup::Popup::new()), ], config, frame_rate, last_tick_key_events: Vec::new(), should_quit: false, should_suspend: false, tick_rate, // App cur_mode: Mode::default(), state, tasks, }) } pub fn next_mode(&mut self) -> Mode { match self.cur_mode { Mode::Home | Mode::InstallDrivers => Mode::ScanDisks, Mode::ScanDisks => Mode::SelectDisks, Mode::SelectDisks => Mode::SelectTableType, Mode::SelectTableType => Mode::ScanWinSources, Mode::ScanWinSources => Mode::SelectWinSource, Mode::SelectWinSource => Mode::SelectWinImage, Mode::SelectWinImage => Mode::SetUserName, Mode::SetUserName => Mode::Confirm, Mode::Confirm => Mode::Process, // i.e. format, apply, etc Mode::Process | Mode::Done => Mode::Done, Mode::Failed => Mode::Failed, // Invalid States Mode::BootDiags | Mode::BootScan | Mode::BootSetup | Mode::Clone | Mode::DiagMenu | Mode::InjectDrivers | Mode::LogView | Mode::PEMenu | Mode::PreClone | Mode::PostClone | Mode::SelectParts | Mode::SetBootMode => panic!("This shouldn't happen?"), } } pub fn set_mode(&mut self, new_mode: Mode) -> Result<()> { info!("Setting mode to {new_mode:?}"); self.cur_mode = new_mode; match new_mode { Mode::InstallDrivers => self.state.scan_drivers(), // Mode::Process => { // self.action_tx // .send(Action::DisplayPopup(popup::Type::Info, String::from("...")))?; // } Mode::ScanDisks => { self.state.reset_all(); if self.tasks.idle() { self.tasks.add(TaskType::ScanDisks); } self.action_tx.send(Action::DisplayPopup( popup::Type::Info, String::from("Scanning Disks..."), ))?; } Mode::ScanWinSources => { self.state.reset_all(); self.state.scan_wim_local(ScanType::WindowsInstallers); } Mode::SetUserName => { if let Ok(wim_sources) = self.state.wim_sources.lock() && let Some(index) = self.state.wim_image_index { let image = wim_sources.get_file(index); if !image.is_setup { self.action_tx.send(Action::NextScreen)?; } } } Mode::Done => { self.action_tx .send(Action::DisplayPopup(popup::Type::Success, popup::fortune()))?; } _ => {} } Ok(()) } pub async fn run(&mut self) -> Result<()> { let mut tui = Tui::new()? // .mouse(true) // uncomment this line to enable mouse support .tick_rate(self.tick_rate) .frame_rate(self.frame_rate); tui.enter()?; for component in &mut self.components { component.register_action_handler(self.action_tx.clone())?; } for component in &mut self.components { component.register_config_handler(self.config.clone())?; } for component in &mut self.components { component.init(tui.size()?)?; } let action_tx = self.action_tx.clone(); action_tx.send(Action::SetMode(Mode::ScanDisks))?; loop { self.handle_events(&mut tui).await?; self.handle_actions(&mut tui)?; if self.should_suspend { tui.suspend()?; action_tx.send(Action::Resume)?; action_tx.send(Action::ClearScreen)?; // tui.mouse(true); tui.enter()?; } else if self.should_quit { tui.stop()?; break; } } tui.exit()?; Ok(()) } async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { let Some(event) = tui.next_event().await else { return Ok(()); }; let action_tx = self.action_tx.clone(); match event { Event::Quit => action_tx.send(Action::Quit)?, Event::Tick => action_tx.send(Action::Tick)?, Event::Render => action_tx.send(Action::Render)?, Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, Event::Key(key) => self.handle_key_event(key)?, _ => {} } for component in &mut self.components { if let Some(action) = component.handle_events(Some(event.clone()))? { action_tx.send(action)?; } } Ok(()) } fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { let action_tx = self.action_tx.clone(); let Some(keymap) = self.config.keybindings.get(&self.cur_mode) else { return Ok(()); }; if let Some(action) = keymap.get(&vec![key]) { info!("Got action: {action:?}"); action_tx.send(action.clone())?; } else { // If the key was not handled as a single key action, // then consider it for multi-key combinations. self.last_tick_key_events.push(key); // Check for multi-key combinations if let Some(action) = keymap.get(&self.last_tick_key_events) { info!("Got action: {action:?}"); action_tx.send(action.clone())?; } } Ok(()) } fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> { while let Ok(action) = self.action_rx.try_recv() { if action != Action::Tick && action != Action::Render { debug!("{action:?}"); } match action { Action::Tick => { self.last_tick_key_events.drain(..); // Check background task(s) self.tasks.poll()?; } Action::Quit => self.should_quit = true, Action::Suspend => self.should_suspend = true, Action::Resume => self.should_suspend = false, Action::ClearScreen => tui.terminal.clear()?, // Action::KeyUp => { // self.list.previous(); // if let Some(tool) = self.list.get_selected() // && tool.separator // { // // Skip over separator // self.list.previous(); // if let Some(index) = self.list.selected() { // self.action_tx.send(Action::Highlight(index))?; // } // } // } // Action::KeyDown => { // self.list.next(); // if let Some(tool) = self.list.get_selected() // && tool.separator // { // // Skip over separator // self.list.next(); // if let Some(index) = self.list.selected() { // self.action_tx.send(Action::Highlight(index))?; // } // } // } Action::Error(ref msg) => { self.action_tx .send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?; self.action_tx.send(Action::SetMode(Mode::Failed))?; } Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, Action::InstallDriver => { self.action_tx.send(Action::SetMode(Mode::InstallDrivers))?; } Action::FindWimBackups => { self.state.reset_local(); self.state.scan_wim_local(ScanType::GeneralWimFiles); } Action::FindWimNetwork => { self.state.reset_network(); self.state.scan_wim_network(); } Action::NextScreen => { let next_mode = self.next_mode(); self.action_tx.send(Action::DismissPopup)?; self.action_tx.send(Action::SetMode(next_mode))?; } Action::PrevScreen => match self.cur_mode { Mode::SelectTableType => { self.action_tx.send(Action::SetMode(Mode::SelectDisks))?; } Mode::SelectWinSource => { self.action_tx.send(Action::SetMode(Mode::ScanWinSources))?; } Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { self.action_tx .send(Action::SetMode(Mode::SelectWinSource))?; } _ => {} }, Action::Process => match self.cur_mode { Mode::Confirm | Mode::ScanWinSources => { self.action_tx.send(Action::NextScreen)?; } Mode::Done => { self.action_tx.send(Action::Quit)?; } _ => {} }, Action::ScanDisks => self.action_tx.send(Action::SetMode(Mode::ScanDisks))?, Action::Select(one, _two) => match self.cur_mode { Mode::InstallDrivers => { if let Some(index) = one && let Some(driver) = self.state.driver_list.get(index).cloned() { drivers::load(&driver.inf_paths); self.state.driver = Some(driver); } } Mode::SelectDisks => { self.state.disk_index_dest = one; } Mode::SelectTableType => { self.state.table_type = { if let Some(index) = one { match index { 0 => Some(PartitionTableType::Guid), 1 => Some(PartitionTableType::Legacy), index => { panic!("Failed to select PartitionTableType: {index}") } } } else { None } } } Mode::SelectWinSource => { self.state.wim_file_index = one; } Mode::SelectWinImage => { self.state.wim_image_index = one; } _ => {} }, Action::SetMode(mode) => { self.set_mode(mode)?; self.action_tx .send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?; self.action_tx.send(build_left_items(self))?; self.action_tx.send(build_right_items(self))?; self.action_tx.send(Action::Select(None, None))?; } Action::SetUserName(ref name) => { self.state.username = Some(name.clone()); self.action_tx.send(Action::NextScreen)?; } Action::TasksComplete => self.action_tx.send(Action::NextScreen)?, _ => {} } for component in &mut self.components { if let Some(action) = component.update(action.clone())? { self.action_tx.send(action)?; }; } } Ok(()) } fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> { tui.resize(Rect::new(0, 0, w, h))?; self.render(tui)?; Ok(()) } fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { if let [header, _body, footer, center, left, right, username, popup] = get_chunks(frame.area())[..] { let component_areas = vec![header, center, left, right, username, footer, popup]; for (component, area) in zip(self.components.iter_mut(), component_areas) { if let Err(err) = component.draw(frame, area) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {err:?}"))); } } }; })?; Ok(()) } } fn build_footer_string(cur_mode: Mode) -> String { match cur_mode { Mode::Home | Mode::ScanDisks => String::from("(q) to quit"), Mode::InstallDrivers => String::from("(Enter) to select / (q) to quit"), Mode::SelectDisks => String::from( "(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit", ), Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"), Mode::SelectWinSource | Mode::SelectWinImage => { String::from("(Enter) to select / (b) to go back / (q) to quit") } Mode::ScanWinSources => String::from( "(Enter) to continue / (b) to scan for backups / (n) to scan network / (q) to quit", ), Mode::SetUserName => String::from("(Enter) to continue / (Esc) to go back"), Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"), Mode::Done | Mode::Failed | Mode::Process => String::from("(Enter) or (q) to quit"), // Invalid States Mode::BootDiags | Mode::BootScan | Mode::BootSetup | Mode::Clone | Mode::DiagMenu | Mode::InjectDrivers | Mode::LogView | Mode::PEMenu | Mode::PreClone | Mode::PostClone | Mode::SelectParts | Mode::SetBootMode => panic!("This shouldn't happen?"), } } fn build_left_items(app: &App) -> Action { let select_type: SelectionType; let title: String; let mut items = Vec::new(); let mut labels: Vec = Vec::new(); match app.cur_mode { Mode::Home => { select_type = SelectionType::Loop; title = String::from("Home"); } Mode::InstallDrivers => { select_type = SelectionType::One; title = String::from("Install Drivers"); app.state .driver_list .iter() .for_each(|driver| items.push(driver.to_string())); } Mode::Process => { select_type = SelectionType::Loop; title = String::from("Processing"); // TODO: FIXME } Mode::ScanWinSources => { select_type = SelectionType::Loop; title = String::from("Scanning"); // TODO: FIXME } Mode::SelectWinSource => { select_type = SelectionType::One; title = String::from("Select Windows Source"); if let Ok(wim_sources) = app.state.wim_sources.lock() { wim_sources .get_file_list() .iter() .for_each(|wim_file| items.push(wim_file.path.clone())); } } Mode::SelectWinImage | Mode::SetUserName => { select_type = SelectionType::One; title = String::from("Select Windows Image"); if let Ok(wim_sources) = app.state.wim_sources.lock() && let Some(index) = app.state.wim_file_index { wim_sources .get_file(index) .images .iter() .for_each(|image| items.push(format!("{image}"))); } } Mode::SelectDisks => { select_type = SelectionType::One; title = String::from("Select Destination Disk"); let disk_list = app.state.disk_list.lock().unwrap(); disk_list .iter() .for_each(|disk| items.push(disk.description.to_string())); } Mode::SelectTableType => { select_type = SelectionType::One; title = String::from("Select Partition Table Type"); items.push(format!("{}", PartitionTableType::Guid)); items.push(format!("{}", PartitionTableType::Legacy)); } Mode::Confirm => { select_type = SelectionType::Loop; title = String::from("Confirm Selections"); } Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { select_type = SelectionType::Loop; title = String::from("Processing"); } Mode::Done | Mode::Failed => { select_type = SelectionType::Loop; title = String::from("Done"); } // Invalid states Mode::BootDiags | Mode::BootScan | Mode::BootSetup | Mode::DiagMenu | Mode::InjectDrivers | Mode::LogView | Mode::PEMenu | Mode::SelectParts | Mode::SetBootMode => panic!("This shouldn't happen?"), }; Action::UpdateLeft(title, labels, items, select_type) } fn build_right_items(app: &App) -> Action { let mut items = Vec::new(); let mut labels: Vec> = Vec::new(); let mut start_index = 0; match app.cur_mode { Mode::InstallDrivers => { items.push(vec![DVLine { line_parts: vec![String::from("CPU")], line_colors: vec![Color::Cyan], }]); items.push(vec![DVLine { line_parts: vec![get_cpu_name()], line_colors: vec![Color::Reset], }]); start_index = 2; } Mode::SelectDisks => { // Labels let dest_dv_line = DVLine { line_parts: vec![ String::from("Dest"), String::from(" (WARNING: ALL DATA WILL BE DELETED!)"), ], line_colors: vec![Color::Cyan, Color::Red], }; if let Some(table_type) = &app.state.table_type { // Show table type let type_str = match table_type { PartitionTableType::Guid => "GPT", PartitionTableType::Legacy => "MBR", }; labels.push(vec![ dest_dv_line, DVLine { line_parts: vec![format!(" (Will be formatted {type_str})")], line_colors: vec![Color::Yellow], }, ]); } else { labels.push(vec![dest_dv_line]); } let disk_list = app.state.disk_list.lock().unwrap(); disk_list .iter() .for_each(|disk| items.push(get_disk_description_right(disk, &None))); } Mode::SelectWinSource => { // Disk Info let type_str = match app.state.table_type.clone().unwrap() { PartitionTableType::Guid => "GPT", PartitionTableType::Legacy => "MBR", }; let mut label_dv_lines = vec![ DVLine { line_parts: vec![ String::from("Dest"), String::from(" (WARNING: ALL DATA WILL BE DELETED!)"), ], line_colors: vec![Color::Cyan, Color::Red], }, DVLine { line_parts: vec![format!(" (Will be formatted {type_str})")], line_colors: vec![Color::Yellow], }, DVLine::blank(), ]; let disk_list = app.state.disk_list.lock().unwrap(); if let Some(index) = app.state.disk_index_dest && let Some(disk) = disk_list.get(index) { get_disk_description_right(disk, &None) .into_iter() .for_each(|dv_line| label_dv_lines.push(dv_line)); } labels.push(label_dv_lines); // WIM Info if let Ok(wim_sources) = app.state.wim_sources.lock() { wim_sources.get_file_list().iter().for_each(|source| { let mut wim_dv_lines = vec![ DVLine { line_parts: vec![String::from("WIM Info")], line_colors: vec![Color::Cyan], }, DVLine { line_parts: vec![source.path.clone()], line_colors: vec![Color::Reset], }, DVLine::blank(), DVLine { line_parts: vec![String::from("Images")], line_colors: vec![Color::Blue], }, DVLine::blank(), ]; source.images.iter().for_each(|image| { wim_dv_lines.push(DVLine { line_parts: vec![format!("{image}")], line_colors: vec![Color::Reset], }) }); items.push(wim_dv_lines); }); } } Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { info!("Building right items for: {:?}", &app.cur_mode); let wim_file; if let Ok(wim_sources) = app.state.wim_sources.lock() && let Some(index) = app.state.wim_file_index { wim_file = wim_sources.get_file(index); } else { panic!("Failed to get source WIM file"); } // Disk Info let type_str = match app.state.table_type.clone().unwrap() { PartitionTableType::Guid => "GPT", PartitionTableType::Legacy => "MBR", }; let mut label_dv_lines = vec![ DVLine { line_parts: vec![ String::from("Dest"), String::from(" (WARNING: ALL DATA WILL BE DELETED!)"), ], line_colors: vec![Color::Cyan, Color::Red], }, DVLine { line_parts: vec![format!(" (Will be formatted {type_str})")], line_colors: vec![Color::Yellow], }, DVLine::blank(), ]; let disk_list = app.state.disk_list.lock().unwrap(); if let Some(index) = app.state.disk_index_dest && let Some(disk) = disk_list.get(index) { get_disk_description_right(disk, &None) .into_iter() .for_each(|dv_line| label_dv_lines.push(dv_line)); } label_dv_lines.append(&mut vec![ DVLine::blank(), DVLine { line_parts: vec![String::from("WIM Info")], line_colors: vec![Color::Cyan], }, DVLine { line_parts: vec![wim_file.path.clone()], line_colors: vec![Color::Reset], }, DVLine::blank(), DVLine { line_parts: vec![String::from("Image")], line_colors: vec![Color::Blue], }, ]); // WIM Info match app.cur_mode { Mode::SelectWinImage => { wim_file.images.iter().for_each(|image| { items.push(vec![DVLine { line_parts: vec![format!("{image}")], line_colors: vec![Color::Reset], }]) }); } Mode::Confirm => { if let Some(index) = app.state.wim_image_index && let Some(image) = wim_file.images.get(index) { label_dv_lines.append(&mut vec![ DVLine { line_parts: vec![format!("{image}")], line_colors: vec![Color::Reset], }, DVLine::blank(), ]); } if wim_file.is_setup && let Some(username) = &app.state.username { label_dv_lines.append(&mut vec![DVLine { line_parts: vec![String::from("Username: "), username.clone()], line_colors: vec![Color::Green, Color::Reset], }]); } items.push(vec![DVLine::blank()]); } _ => {} } // Done labels.push(label_dv_lines); } Mode::SelectTableType => { // Labels let dest_dv_line = DVLine { line_parts: vec![ String::from("Dest"), String::from(" (WARNING: ALL DATA WILL BE DELETED!)"), ], line_colors: vec![Color::Cyan, Color::Red], }; if let Some(table_type) = &app.state.table_type { // Show table type let type_str = match table_type { PartitionTableType::Guid => "GPT", PartitionTableType::Legacy => "MBR", }; labels.push(vec![ dest_dv_line, DVLine { line_parts: vec![format!(" (Will be formatted {type_str})")], line_colors: vec![Color::Yellow], }, ]); } else { labels.push(vec![dest_dv_line]); } let disk_list = app.state.disk_list.lock().unwrap(); if let Some(index) = app.state.disk_index_dest && let Some(disk) = disk_list.get(index) { items.push(get_disk_description_right(disk, &None)); } } _ => {} } Action::UpdateRight(labels, start_index, items) } fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { // Cut the given rectangle into three vertical pieces let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]) .split(r); // Then cut the middle vertical piece into three width-wise pieces Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ]) .split(popup_layout[1])[1] // Return the middle chunk } fn get_chunks(r: Rect) -> Vec { 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(), ); let center = centered_rect(90, 90, chunks[1]); // Left/Right chunks.extend( Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(center) .to_vec(), ); // Center chunks.push(center); // Set username chunks.push(centered_rect(60, 20, r)); // Popup chunks.push(centered_rect(60, 25, r)); // Done chunks }