// 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 crossterm::event::KeyEvent; use ratatui::{ prelude::*, widgets::{Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph}, }; use tokio::sync::mpsc::UnboundedSender; use tracing::info; use super::{popup, state::StatefulList, Component}; use crate::{ action::Action, app::Mode, config::Config, system::{ disk::{Disk, Partition, PartitionTableType}, drivers::{self, Driver}, }, }; #[derive(Default)] pub struct Left { command_tx: Option>, config: Config, disk_id_dest: Option, table_type: Option, title_text: String, list_disks: StatefulList, list_drivers: StatefulList, list_parts: StatefulList, list_table_types: StatefulList, mode: Mode, selections: Vec>, } impl Left { pub fn new() -> Self { Self { selections: vec![None, None], title_text: String::from("Home"), ..Default::default() } } } impl Component for Left { fn handle_key_event(&mut self, key: KeyEvent) -> Result> { let _ = key; // to appease clippy Ok(None) } fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) } fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { 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 { // NOTE: Remove all variants except Mode::Confirm? Mode::Confirm => { if let Some(command_tx) = self.command_tx.clone() { command_tx.send(Action::NextScreen)?; } } Mode::InstallDrivers | Mode::SelectDisks | Mode::SelectTableType | Mode::SelectParts => { // Menu selection sections let selection: Option = match self.mode { Mode::InstallDrivers => self.list_drivers.selected(), Mode::SelectDisks => self.list_disks.selected(), Mode::SelectTableType => self.list_table_types.selected(), Mode::SelectParts => self.list_parts.selected(), _ => panic!("This shouldn't happen!"), }; if let Some(index) = selection { if let Some(command_tx) = self.command_tx.clone() { let mut selection_one: Option = None; let mut selection_two: Option = None; // Get selection(s) if self.selections[0].is_none() { // First selection selection_one = Some(index); selection_two = None; } else { // Second selection if let Some(source_index) = self.selections[0] { if index == source_index { // Toggle first selection selection_one = None; self.selections[0] = None; } else { selection_one = self.selections[0]; selection_two = Some(index); } } } // Send selection(s) if needed // NOTE: This is needed to keep the app and all components in sync match self.mode { Mode::InstallDrivers => { // Only need to select one entry if let Some(driver) = self.list_drivers.get_selected() { command_tx.send(Action::SelectDriver(driver.clone()))?; } } Mode::SelectTableType => { // Only need to select one entry if let Some(table_type) = self.list_table_types.get_selected() { self.table_type = Some(table_type.clone()); command_tx.send(Action::SelectTableType(table_type))?; } } Mode::SelectDisks | Mode::SelectParts => { command_tx .send(Action::Select(selection_one, selection_two))?; // Advance screen if both selections made if selection_two.is_some() { command_tx.send(Action::NextScreen)?; } } _ => {} }; } } } _ => {} }, Action::Select(Some(index), None) => self.selections[0] = Some(index), Action::Select(_, Some(index)) => { if self.mode == Mode::SelectDisks { self.disk_id_dest = Some(index); } } Action::SetMode(new_mode) => { let prev_mode = self.mode; self.mode = new_mode; match (prev_mode, new_mode) { (_, Mode::ScanDisks) => { self.list_disks.clear_items(); self.title_text = String::new(); } (_, Mode::InstallDrivers) => { self.list_drivers.set_items(drivers::scan()); self.selections[0] = None; self.selections[1] = None; self.title_text = String::from("Install Drivers"); if self.list_drivers.is_empty() { if let Some(command_tx) = self.command_tx.clone() { command_tx.send(Action::DisplayPopup( popup::Type::Error, String::from("No drivers available to install"), ))?; } } } (_, Mode::SelectDisks) => { self.selections[0] = None; self.selections[1] = None; self.title_text = String::from("Select Source and Destination Disks"); } (_, 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) => { self.title_text = String::from("Select Boot and OS Partitions"); } (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 | Mode::Failed) => self.title_text = String::from("Done"), // Invalid states (_, Mode::Confirm) => panic!("This shouldn't happen."), } } Action::UpdateDiskList(disks) => { info!("Updating disk list"); self.list_disks.set_items(disks); if self.mode == Mode::Clone { if let Some(index) = self.disk_id_dest { if let Some(disk) = self.list_disks.get(index) { self.list_parts.set_items(disk.get_parts()); // Auto-select first partition and highlight likely OS partition if let Some(table_type) = &self.table_type { match table_type { PartitionTableType::Guid => { if disk.num_parts() >= 3 { self.selections[0] = Some(0); self.list_parts.select(2); } } PartitionTableType::Legacy => { if disk.num_parts() >= 2 { self.selections[0] = Some(0); self.list_parts.select(1); } } } } } } } } _ => {} } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [title_area, body_area] = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(1), Constraint::Min(1)]) .areas(area); // Title let title = Paragraph::new(Line::from(Span::styled(&self.title_text, Style::default())).centered()) .block(Block::default().borders(Borders::NONE)); frame.render_widget(title, title_area); // Body match self.mode { Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone | Mode::Done | Mode::Failed => { // 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); // 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); // 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 = match self.mode { Mode::SelectDisks => " ~source disk~", Mode::SelectParts => " ~boot volume~", _ => "", } } } 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."), } } } // Done Ok(()) } }