// 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, Padding, Paragraph, Wrap}, }; use tokio::sync::mpsc::UnboundedSender; use tracing::info; use super::{state::StatefulList, Component}; use crate::{ action::Action, app::Mode, config::Config, system::{ cpu::get_cpu_name, disk::{Disk, Partition, PartitionTableType}, }, }; #[derive(Default)] pub struct Right { command_tx: Option>, config: Config, cur_mode: Mode, list_disks: StatefulList, list_parts: StatefulList, prev_mode: Mode, selected_disks: Vec>, selections: Vec>, table_type: Option, } impl Right { pub fn new() -> Self { Self { selected_disks: vec![None, None], selections: vec![None, None], ..Default::default() } } } impl Component for Right { 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.cur_mode { Mode::SelectDisks => self.list_disks.previous(), Mode::SelectParts => self.list_parts.previous(), _ => {} }, Action::KeyDown => match self.cur_mode { Mode::SelectDisks => self.list_disks.next(), Mode::SelectParts => self.list_parts.next(), _ => {} }, Action::Process => { if self.prev_mode == Mode::SelectDisks && self.cur_mode == Mode::Confirm { self.selected_disks = self.selections.clone(); } } Action::Select(one, two) => { self.selections[0] = one; self.selections[1] = two; } Action::SelectTableType(table_type) => self.table_type = Some(table_type), Action::SetMode(new_mode) => { self.prev_mode = self.cur_mode; self.cur_mode = new_mode; match self.cur_mode { Mode::SelectDisks => { self.selections[0] = None; self.selections[1] = None; self.selected_disks[0] = None; self.selected_disks[1] = None; } Mode::SelectParts => { self.selections[0] = None; self.selections[1] = None; } Mode::SelectTableType => { self.selections[0] = None; self.selections[1] = None; self.table_type = None; } _ => {} } } Action::UpdateDiskList(disks) => { info!("Updating disk list"); self.list_disks.set_items(disks); if self.cur_mode == Mode::Clone { if let Some(index) = self.selected_disks[1] { 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(index) = self.selected_disks[1] { if let Some(disk) = self.list_disks.get(index) { 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_text = String::from("Info"); let title = Paragraph::new(Line::from(title_text).centered()) .block(Block::default().borders(Borders::NONE)); frame.render_widget(title, title_area); // Body let mut body_text = Vec::new(); match (self.prev_mode, self.cur_mode) { (_, Mode::InstallDrivers) => { body_text.push(Line::from(Span::raw(format!("CPU: {}", get_cpu_name())))); } (_, Mode::SelectDisks | Mode::SelectTableType) | (Mode::SelectDisks | Mode::SelectTableType, Mode::Confirm) => { // Source Disk body_text.push(Line::from(Span::styled( "Source:", Style::default().cyan().bold(), ))); body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<4} {:<4} {}", "Disk ID", "Size", "Conn", "Type", "Model (Serial)" ), Style::new().green().bold(), ))); let index = if self.selected_disks[0].is_some() { // Selected in prior mode self.selected_disks[0] } else if self.selections[0].is_some() { // Selected in this mode self.selections[0] } else { // Highlighted entry self.list_disks.selected() }; if let Some(i) = index { if let Some(disk) = self.list_disks.get(i) { body_text.push(Line::from(Span::raw(&disk.description))); // Source parts body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<7} {}", "Part ID", "Size", "(FS)", "\"Label\"" ), Style::new().blue().bold(), ))); for line in &disk.parts_description { body_text.push(Line::from(Span::raw(line))); } } } // Destination Disk let index = if self.selected_disks[1].is_some() { // Selected in prior mode self.selected_disks[1] } else { // Select(ed) in this mode match (self.selections[0], self.selections[1]) { (Some(one), None) => { // First selected if let Some(two) = self.selections[1] { if one == two { None } else { self.selections[1] } } else { self.list_disks.selected() } } (Some(_), Some(_)) => { // Both selected self.selections[1] } (_, _) => None, } }; if let Some(i) = index { // Divider body_text.push(Line::from("")); body_text.push(Line::from(str::repeat("─", (body_area.width - 4) as usize))); body_text.push(Line::from("")); // Disk if let Some(disk) = self.list_disks.get(i) { body_text.push(Line::from(vec![ Span::styled("Dest:", Style::default().cyan().bold()), Span::styled( " (WARNING: ALL DATA WILL BE DELETED!)", Style::default().red().bold(), ), ])); if let Some(table_type) = &self.table_type { body_text.push(Line::from(Span::styled( format!(" (Will be formatted {table_type})"), Style::default().yellow().bold(), ))); } body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<4} {:<4} {}", "Disk ID", "Size", "Conn", "Type", "Model (Serial)" ), Style::new().green().bold(), ))); body_text.push(Line::from(Span::raw(&disk.description))); // Destination parts body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<7} {}", "Part ID", "Size", "(FS)", "\"Label\"" ), Style::new().blue().bold(), ))); for line in &disk.parts_description { body_text.push(Line::from(Span::raw(line))); } } } } (_, Mode::SelectParts) | (Mode::SelectParts, Mode::Confirm) => { // Disk if let Some(index) = self.selected_disks[1] { if let Some(disk) = self.list_disks.get(index) { body_text.push(Line::from(Span::styled( "Dest:", Style::default().cyan().bold(), ))); body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<4} {:<4} {}", "Disk ID", "Size", "Conn", "Type", "Model (Serial)" ), Style::new().green().bold(), ))); body_text.push(Line::from(Span::raw(&disk.description))); // Destination parts body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<7} {}", "Part ID", "Size", "(FS)", "\"Label\"" ), Style::new().blue().bold(), ))); for line in &disk.parts_description { body_text.push(Line::from(Span::raw(line))); } } // Divider body_text.push(Line::from("")); body_text.push(Line::from(str::repeat("─", (body_area.width - 4) as usize))); body_text.push(Line::from("")); // Boot Partition // i.e. either the previously selected part or the highlighted one (if possible) let mut boot_index = self.selections[0]; if boot_index.is_none() { if let Some(i) = self.list_parts.selected() { boot_index = Some(i); } } if let Some(i) = boot_index { if let Some(part) = self.list_parts.get(i) { body_text.push(Line::from(Span::styled( "Boot:", Style::default().cyan().bold(), ))); body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<7} {}", "Part ID", "Size", "(FS)", "\"Label\"" ), Style::new().blue().bold(), ))); body_text.push(Line::from(Span::raw(format!("{part}")))); } } // OS Partition // i.e. either the previously selected part or the highlighted one (if needed) let mut os_index = self.selections[1]; if os_index.is_none() { if let Some(boot_index) = self.selections[0] { if let Some(i) = self.list_parts.selected() { if boot_index != i { os_index = Some(i); } } } } if let Some(i) = os_index { if let Some(part) = self.list_parts.get(i) { body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( "OS:", Style::default().cyan().bold(), ))); body_text.push(Line::from("")); body_text.push(Line::from(Span::styled( format!( "{:<8} {:>11} {:<7} {}", "Part ID", "Size", "(FS)", "\"Label\"" ), Style::new().blue().bold(), ))); body_text.push(Line::from(Span::raw(format!("{part}")))); } } } } _ => {} } let body = Paragraph::new(body_text) .style(Style::default().fg(Color::Gray)) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .padding(Padding::new(1, 1, 1, 1)), ); frame.render_widget(body, body_area); // Done Ok(()) } }