From 2f1f04dcab60b47c74a8fe405a21bfdde711a165 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Mon, 4 Nov 2024 23:33:02 -0800 Subject: [PATCH] Add remaining modes - Refactored to use more pattern matching instead of if/then blocks - Fixed a bug in left.rs that always used the disk list --- config/config.json5 | 43 ++++++- src/action.rs | 45 +++++--- src/app.rs | 128 ++++++++++++++------- src/components/footer.rs | 18 ++- src/components/left.rs | 241 +++++++++++++++++++++++++-------------- 5 files changed, 315 insertions(+), 160 deletions(-) diff --git a/config/config.json5 b/config/config.json5 index d2f61e0..dd8289b 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -1,15 +1,15 @@ { "keybindings": { - "InstallDrivers": { - "": "Process", - "": "KeyUp", - "": "KeyDown", + "ScanDisks": { "": "Quit", // Quit the application "": "Quit", // Another way to quit "": "Quit", // Yet another way to quit "": "Suspend" // Suspend the application }, - "ScanDisks": { + "InstallDrivers": { + "": "Process", + "": "KeyUp", + "": "KeyDown", "": "Quit", // Quit the application "": "Quit", // Another way to quit "": "Quit", // Yet another way to quit @@ -26,7 +26,7 @@ "": "Quit", // Yet another way to quit "": "Suspend" // Suspend the application }, - "SelectParts": { + "SelectTableType": { "": "PrevScreen", "": "Process", "": "KeyUp", @@ -44,11 +44,42 @@ "": "Quit", // Yet another way to quit "": "Suspend" // Suspend the application }, + "PreClone": { + "": "Process", + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application + }, + "Clone": { + "": "Process", + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application + }, + "SelectParts": { + "": "Process", + "": "KeyUp", + "": "KeyDown", + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application + }, + "PostClone": { + "": "Process", + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application + }, "Done": { "": "Quit", "": "Quit", // Quit the application "": "Quit", // Another way to quit "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application }, } } diff --git a/src/action.rs b/src/action.rs index 78849e2..98824fc 100644 --- a/src/action.rs +++ b/src/action.rs @@ -19,31 +19,40 @@ use strum::Display; use crate::{ app::Mode, components::popup::Type, - system::{disk::Disk, drivers::Driver}, + system::{ + disk::{Disk, PartitionTableType}, + drivers::Driver, + }, }; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { - Tick, - Render, - Resize(u16, u16), - Suspend, - Resume, - Quit, - ClearScreen, - DismissPopup, - DisplayPopup(Type, String), - Error(String), - Help, + // App + Command(String, String, Vec), // (label, command, args) + Diskpart(String, String), // (label, script_as_string) InstallDriver, - SelectDriver(Driver), - KeyUp, - KeyDown, - SetMode(Mode), - PrevScreen, - NextScreen, Process, ScanDisks, Select(Option, Option), // indicies for (source, dest) or (boot, os) + SelectDriver(Driver), + SelectTableType(PartitionTableType), UpdateDiskList(Vec), + // Screens + DismissPopup, + DisplayPopup(Type, String), + NextScreen, + PrevScreen, + SetMode(Mode), + // TUI + ClearScreen, + Error(String), + Help, + KeyDown, + KeyUp, + Quit, + Render, + Resize(u16, u16), + Resume, + Suspend, + Tick, } diff --git a/src/app.rs b/src/app.rs index 6fc28a0..3793479 100644 --- a/src/app.rs +++ b/src/app.rs @@ -42,27 +42,33 @@ use crate::{ }, config::Config, system::{ - disk::{get_disks, Disk}, + disk::{get_disks, Disk, PartitionTableType}, drivers::{self, Driver}, }, tui::{Event, Tui}, }; pub struct App { - config: Config, - tick_rate: f64, - frame_rate: f64, + // TUI + action_rx: mpsc::UnboundedReceiver, + action_tx: mpsc::UnboundedSender, components: Vec>, - disks: Arc>>, - driver: Option, + config: Config, + frame_rate: f64, + last_tick_key_events: Vec, should_quit: bool, should_suspend: bool, + tick_rate: f64, + // App cur_mode: Mode, + disk_dest: Option, + disk_list: Arc>>, + disk_source: Option, + driver: Option, prev_mode: Mode, selections: Vec>, - last_tick_key_events: Vec, - action_tx: mpsc::UnboundedSender, - action_rx: mpsc::UnboundedReceiver, + table_type: Option, + task_handles: Vec>, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -71,8 +77,12 @@ pub enum Mode { ScanDisks, InstallDrivers, SelectDisks, - SelectParts, + SelectTableType, Confirm, + PreClone, + Clone, + SelectParts, + PostClone, Done, } @@ -80,8 +90,9 @@ impl App { pub fn new(tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); Ok(Self { - tick_rate, - frame_rate, + // TUI + action_rx, + action_tx, components: vec![ Box::new(Title::new()), Box::new(FpsCounter::default()), @@ -90,41 +101,56 @@ impl App { Box::new(Footer::new()), Box::new(Popup::new()), ], - disks: Arc::new(Mutex::new(Vec::new())), - driver: None, + config: Config::new()?, + frame_rate, + last_tick_key_events: Vec::new(), should_quit: false, should_suspend: false, - config: Config::new()?, + tick_rate, + // App cur_mode: Mode::ScanDisks, + disk_dest: None, + disk_list: Arc::new(Mutex::new(Vec::new())), + disk_source: None, + driver: None, prev_mode: Mode::ScanDisks, selections: vec![None, None], - last_tick_key_events: Vec::new(), - action_tx, - action_rx, + table_type: None, + task_handles: Vec::new(), }) } pub fn next_mode(&mut self) -> Option { - let new_mode = match self.cur_mode { - Mode::InstallDrivers => Mode::ScanDisks, - Mode::ScanDisks => Mode::SelectDisks, - Mode::SelectDisks | Mode::SelectParts => { + let new_mode = match (self.prev_mode, self.cur_mode) { + (_, Mode::InstallDrivers) => Mode::ScanDisks, + (_, Mode::ScanDisks) => Mode::SelectDisks, + (_, Mode::SelectDisks | Mode::SelectTableType | Mode::SelectParts) => { if self.selections[1].is_some() { Mode::Confirm } else { self.cur_mode } } - Mode::Done => Mode::Done, - Mode::Confirm => match self.prev_mode { - Mode::SelectDisks => Mode::SelectParts, - Mode::SelectParts => Mode::Done, - _ => self.cur_mode, - }, + (Mode::SelectDisks, Mode::Confirm) => Mode::SelectTableType, + (Mode::SelectTableType, Mode::Confirm) => Mode::PreClone, + (_, Mode::PreClone) => Mode::Clone, + (_, Mode::Clone) => Mode::SelectParts, + (Mode::SelectParts, Mode::Confirm) => Mode::PostClone, + (_, Mode::PostClone) => Mode::Done, + (_, Mode::Done) => Mode::Done, + // Invalid states + (_, Mode::Confirm) => panic!("This shouldn't happen."), }; + if new_mode != self.cur_mode { - if self.cur_mode != Mode::Confirm { - self.prev_mode = self.cur_mode; + match self.cur_mode { + // Update prev_mode if appropriate + Mode::Confirm => {} + Mode::PreClone | Mode::Clone | Mode::PostClone | Mode::Done => { + // Override since we're past the point of no return + self.prev_mode = self.cur_mode; + } + _ => self.prev_mode = self.cur_mode, } Some(new_mode) } else { @@ -133,8 +159,8 @@ impl App { } pub async fn run(&mut self) -> Result<()> { - let disk_wrapper = Arc::clone(&self.disks); - let _ = lazy_get_disks(disk_wrapper); + let disk_list_arc = Arc::clone(&self.disk_list); + self.task_handles.push(lazy_get_disks(disk_list_arc)); let mut tui = Tui::new()? // .mouse(true) // uncomment this line to enable mouse support .tick_rate(self.tick_rate) @@ -224,11 +250,24 @@ impl App { match action { Action::Tick => { self.last_tick_key_events.drain(..); - // Continue to next screen if shared disks has been set - if self.cur_mode == Mode::ScanDisks { - if let Ok(_) = &self.disks.try_lock() { - self.action_tx.send(Action::NextScreen)?; + match self.cur_mode { + Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { + // Check background task + if let Ok(_) = &self.disk_list.try_lock() { + if self.task_handles.is_empty() { + // All tasks complete + self.action_tx.send(Action::NextScreen)?; + } else { + // Tasks remain, check if current is complete + if let Some(handle) = self.task_handles.first() { + if handle.is_finished() { + self.task_handles.remove(0); + } + } + } + } } + _ => {} } } Action::Quit => self.should_quit = true, @@ -243,6 +282,10 @@ impl App { drivers::load(&driver.inf_paths); self.action_tx.send(Action::NextScreen)?; } + Action::SelectTableType(ref table_type) => { + self.table_type = Some(table_type.clone()); + self.action_tx.send(Action::NextScreen)?; + } Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, Action::PrevScreen => { @@ -254,8 +297,8 @@ impl App { } } Action::ScanDisks => { - let disk_wrapper = Arc::clone(&self.disks); - let _ = lazy_get_disks(disk_wrapper); + let disk_list_arc = Arc::clone(&self.disk_list); + self.task_handles.push(lazy_get_disks(disk_list_arc)); self.action_tx.send(Action::SetMode(Mode::ScanDisks))?; } Action::Select(one, two) => { @@ -269,8 +312,9 @@ impl App { self.prev_mode = self.cur_mode; } Mode::SelectDisks | Mode::SelectParts => { - let disks = self.disks.lock().unwrap(); - self.action_tx.send(Action::UpdateDiskList(disks.clone()))?; + let disk_list = self.disk_list.lock().unwrap(); + self.action_tx + .send(Action::UpdateDiskList(disk_list.clone()))?; } Mode::Done => { self.action_tx.send(Action::DisplayPopup( @@ -373,9 +417,9 @@ fn get_chunks(r: Rect) -> Vec { chunks } -fn lazy_get_disks(disk_wrapper: Arc>>) -> JoinHandle<()> { +fn lazy_get_disks(disk_list_arc: Arc>>) -> JoinHandle<()> { thread::spawn(move || { - let mut disks = disk_wrapper.lock().unwrap(); + let mut disks = disk_list_arc.lock().unwrap(); *disks = get_disks(); }) } diff --git a/src/components/footer.rs b/src/components/footer.rs index cc17393..95117c9 100644 --- a/src/components/footer.rs +++ b/src/components/footer.rs @@ -57,16 +57,22 @@ impl Component for Footer { } Action::SetMode(new_mode) => { self.footer_text = match new_mode { + Mode::ScanDisks => String::from("(q) to quit"), + Mode::InstallDrivers + | Mode::PreClone + | Mode::Clone + | Mode::PostClone + | Mode::SelectParts => 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::Confirm => { String::from("(Enter) to confirm / (b) to go back / (q) to quit") } Mode::Done => String::from("(Enter) or (q) to quit"), - Mode::InstallDrivers => String::from("(Enter) to select / (q) to quit"), - Mode::ScanDisks => String::from("(q) to quit"), - Mode::SelectDisks => String::from( - "(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit", - ), - _ => String::from("(Enter) to select / (b) to go back / (q) to quit"), } } _ => {} diff --git a/src/components/left.rs b/src/components/left.rs index 0171c40..6102273 100644 --- a/src/components/left.rs +++ b/src/components/left.rs @@ -24,7 +24,7 @@ use crate::{ app::Mode, config::Config, system::{ - disk::{Disk, Partition}, + disk::{Disk, Partition, PartitionTableType}, drivers::{self, Driver}, }, }; @@ -37,6 +37,7 @@ pub struct Left { list_disks: StatefulList, list_drivers: StatefulList, list_parts: StatefulList, + list_table_types: StatefulList, mode: Mode, selections: Vec>, } @@ -76,20 +77,38 @@ impl Component for Left { Action::Render => { // add any logic here that should run on every render } - Action::KeyUp => self.list_disks.previous(), - Action::KeyDown => self.list_disks.next(), + Action::KeyUp => match self.mode { + Mode::InstallDrivers => self.list_drivers.previous(), + Mode::SelectDisks => self.list_disks.previous(), + Mode::SelectTableType => self.list_table_types.previous(), + Mode::SelectParts => self.list_parts.previous(), + _ => {} + }, + Action::KeyDown => match self.mode { + Mode::InstallDrivers => self.list_drivers.next(), + Mode::SelectDisks => self.list_disks.next(), + Mode::SelectTableType => self.list_table_types.next(), + Mode::SelectParts => self.list_parts.next(), + _ => {} + }, Action::Process => match self.mode { - Mode::Confirm => { + // NOTE: Remove all variants except Mode::Confirm? + // Mode::Confirm => { + Mode::Confirm | Mode::PreClone | Mode::Clone | Mode::PostClone => { if let Some(command_tx) = self.command_tx.clone() { command_tx.send(Action::NextScreen)?; } } - Mode::InstallDrivers | Mode::SelectDisks | Mode::SelectParts => { + Mode::InstallDrivers + | Mode::SelectDisks + | Mode::SelectTableType + | Mode::SelectParts => { // Menu selection sections let selection: Option; match self.mode { Mode::InstallDrivers => selection = self.list_drivers.state.selected(), Mode::SelectDisks => selection = self.list_disks.state.selected(), + Mode::SelectTableType => selection = self.list_table_types.state.selected(), Mode::SelectParts => selection = self.list_parts.state.selected(), _ => panic!("This shouldn't happen!"), } @@ -122,6 +141,12 @@ impl Component for Left { command_tx.send(Action::SelectDriver(driver.clone()))?; } } + Mode::SelectTableType => { + // Only need to select one entry + if let Some(table_type) = self.list_table_types.pop_selected() { + command_tx.send(Action::SelectTableType(table_type))?; + } + } Mode::SelectDisks | Mode::SelectParts => { command_tx .send(Action::Select(selection_one, selection_two))?; @@ -142,8 +167,12 @@ impl Component for Left { Action::SetMode(new_mode) => { let prev_mode = self.mode; self.mode = new_mode; - match new_mode { - Mode::InstallDrivers => { + match (prev_mode, new_mode) { + (_, Mode::ScanDisks) => { + self.list_disks.clear_items(); + self.title_text = String::from(""); + } + (_, Mode::InstallDrivers) => { self.list_drivers.set_items(drivers::scan()); self.selections[0] = None; self.selections[1] = None; @@ -157,11 +186,7 @@ impl Component for Left { } } } - Mode::ScanDisks => { - self.list_disks.clear_items(); - self.title_text = String::from(""); - } - Mode::SelectDisks => { + (_, Mode::SelectDisks) => { self.selections[0] = None; self.selections[1] = None; self.title_text = String::from("Select Source and Destination Disks"); @@ -169,20 +194,31 @@ impl Component for Left { command_tx.send(Action::DismissPopup)?; } } - Mode::SelectParts => { + (_, Mode::SelectTableType) => { + self.list_table_types + .set_items(vec![PartitionTableType::Guid, PartitionTableType::Legacy]); + self.selections[0] = None; + self.selections[1] = None; + self.title_text = String::from("Select Partition Table Type"); + } + (_, Mode::PreClone | Mode::Clone | Mode::PostClone) => { + self.title_text = String::from("Processing"); + } + (_, Mode::SelectParts) => { + // TODO: Get list of partitions self.selections[0] = None; self.selections[1] = None; self.title_text = String::from("Select Boot and OS Partitions") } - Mode::Confirm => match prev_mode { - Mode::SelectDisks => self.title_text = String::from("Confirm Selections"), - Mode::SelectParts => { - self.title_text = String::from("Confirm Selections (Again)") - } - _ => {} - }, - - Mode::Done => self.title_text = String::from("Done"), + (Mode::SelectDisks | Mode::SelectParts, Mode::Confirm) => { + self.title_text = String::from("Confirm Selections") + } + (Mode::SelectTableType, Mode::Confirm) => { + self.title_text = String::from("Confirm Selections (Again)") + } + (_, Mode::Done) => self.title_text = String::from("Done"), + // Invalid states + (_, Mode::Confirm) => panic!("This shouldn't happen."), } } Action::UpdateDiskList(disks) => self.list_disks.set_items(disks), @@ -203,82 +239,111 @@ impl Component for Left { .block(Block::default().borders(Borders::NONE)); frame.render_widget(title, title_area); - // Body (scan disks) - if self.list_disks.items.is_empty() { - let paragraph = Paragraph::new(String::new()).block( - Block::default() - .borders(Borders::ALL) - .padding(Padding::new(1, 1, 1, 1)), - ); - frame.render_widget(paragraph, body_area); - - // Bail early - return Ok(()); - } - - // Body (confirm) - if self.mode == Mode::Confirm { - let paragraph = Paragraph::new(String::from("Are the listed selections correct?")) - .block( + // Body + match self.mode { + Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone | Mode::Done => { + // Leave blank + let paragraph = Paragraph::new(String::new()).block( Block::default() .borders(Borders::ALL) .padding(Padding::new(1, 1, 1, 1)), ); - frame.render_widget(paragraph, body_area); + frame.render_widget(paragraph, body_area); - // Bail early - return Ok(()); - } + // Bail early + return Ok(()); + } + Mode::Confirm => { + // Nag the user + let paragraph = Paragraph::new(String::from("Are the listed selections correct?")) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ); + frame.render_widget(paragraph, body_area); - // Body (list) - let mut list_items = Vec::::new(); - let list_items_strings: Vec = match self.mode { - Mode::InstallDrivers => self - .list_drivers - .items - .iter() - .map(|i| format!("{i}")) - .collect(), - Mode::SelectDisks => self - .list_disks - .items - .iter() - .map(|i| format!("{i}")) - .collect(), - Mode::SelectParts => self - .list_parts - .items - .iter() - .map(|i| format!("{i}")) - .collect(), - _ => panic!("This shouldn't happen."), - }; - if !list_items_strings.is_empty() { - for (index, item) in list_items_strings.iter().enumerate() { - let mut item_style = Style::default(); - let mut item_text_tail = ""; - if let Some(selection_one) = self.selections[0] { - if selection_one == index { - item_style = Style::new().yellow(); - item_text_tail = " ~already selected~"; + // Bail early + return Ok(()); + } + Mode::InstallDrivers + | Mode::SelectDisks + | Mode::SelectTableType + | Mode::SelectParts => { + // List modes + let mut list_items = Vec::::new(); + let list_items_strings: Vec = match self.mode { + Mode::InstallDrivers => self + .list_drivers + .items + .iter() + .map(|i| format!("{i}")) + .collect(), + Mode::SelectDisks => self + .list_disks + .items + .iter() + .map(|i| format!("{i}")) + .collect(), + Mode::SelectTableType => self + .list_table_types + .items + .iter() + .map(|i| format!("{i}")) + .collect(), + Mode::SelectParts => self + .list_parts + .items + .iter() + .map(|i| format!("{i}")) + .collect(), + _ => panic!("This shouldn't happen."), + }; + if !list_items_strings.is_empty() { + for (index, item) in list_items_strings.iter().enumerate() { + let mut item_style = Style::default(); + let mut item_text_tail = ""; + if let Some(selection_one) = self.selections[0] { + if selection_one == index { + item_style = Style::new().yellow(); + item_text_tail = " ~already selected~"; + } + } + list_items.push( + ListItem::new(format!(" {item}\n{item_text_tail}\n\n")) + .style(item_style), + ); } } - list_items.push( - ListItem::new(format!(" {item}\n{item_text_tail}\n\n")).style(item_style), - ); + let list = List::new(list_items) + .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); + match self.mode { + Mode::InstallDrivers => { + frame.render_stateful_widget(list, body_area, &mut self.list_drivers.state) + } + Mode::SelectDisks => { + frame.render_stateful_widget(list, body_area, &mut self.list_disks.state) + } + Mode::SelectTableType => frame.render_stateful_widget( + list, + body_area, + &mut self.list_table_types.state, + ), + Mode::SelectParts => { + frame.render_stateful_widget(list, body_area, &mut self.list_parts.state) + } + _ => panic!("This shouldn't happen."), + } } } - let list = List::new(list_items) - .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_stateful_widget(list, body_area, &mut self.list_disks.state); // Done Ok(())