From 3c0c12fa7a7846ddd0b3b8a18642c529bd91feec Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 15 Jan 2025 19:17:05 -0800 Subject: [PATCH] Refactor right pane Added DVLine struct to use instead of passing an Arc>. This allows the right into pane to be agnostic of any app details. --- deja_vu/src/action.rs | 4 + deja_vu/src/app.rs | 132 ++++++++++- deja_vu/src/components/right.rs | 389 +++++++------------------------- deja_vu/src/line.rs | 94 ++++++++ deja_vu/src/main.rs | 1 + 5 files changed, 312 insertions(+), 308 deletions(-) create mode 100644 deja_vu/src/line.rs diff --git a/deja_vu/src/action.rs b/deja_vu/src/action.rs index 29b4fa5..b007f1b 100644 --- a/deja_vu/src/action.rs +++ b/deja_vu/src/action.rs @@ -18,6 +18,7 @@ use strum::Display; use crate::{ components::popup::Type, + line::DVLine, state::Mode, system::{ disk::{Disk, PartitionTableType}, @@ -28,6 +29,7 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { // App + Highlight(usize), InstallDriver, Process, ScanDisks, @@ -35,6 +37,8 @@ pub enum Action { SelectDriver(Driver), SelectTableType(PartitionTableType), UpdateDiskList(Vec), + UpdateLeft(Vec>, usize, Vec>), // (labels, start_index, lines) - lines before start are always shown + UpdateRight(Vec>, usize, Vec>), // Same as above // Screens DismissPopup, DisplayPopup(Type, String), diff --git a/deja_vu/src/app.rs b/deja_vu/src/app.rs index 4f3dfa7..8a472b6 100644 --- a/deja_vu/src/app.rs +++ b/deja_vu/src/app.rs @@ -25,6 +25,7 @@ use crossterm::event::KeyEvent; use ratatui::{ layout::{Constraint, Direction, Layout}, prelude::Rect, + style::Color, }; use tokio::sync::mpsc; use tracing::{debug, info}; @@ -32,14 +33,15 @@ use tracing::{debug, info}; use crate::{ action::Action, components::{ - footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, title::Title, Component, + footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, state::StatefulList, + title::Title, Component, }, config::Config, + line::{get_disk_description_right, get_part_description, DVLine}, state::{CloneSettings, Mode}, system::{ - boot, - diskpart::build_dest_format_script, - drivers::{self}, + boot, cpu::get_cpu_name, disk::PartitionTableType, diskpart::build_dest_format_script, + drivers, }, tasks::{Task, Tasks}, tui::{Event, Tui}, @@ -59,6 +61,7 @@ pub struct App { // App clone: CloneSettings, cur_mode: Mode, + list: StatefulList, prev_mode: Mode, selections: Vec>, tasks: Tasks, @@ -91,6 +94,7 @@ impl App { // App clone: CloneSettings::new(disk_list_arc), cur_mode: Mode::ScanDisks, + list: StatefulList::default(), prev_mode: Mode::ScanDisks, selections: vec![None, None], tasks, @@ -240,7 +244,10 @@ impl App { String::from("COMPLETE\n\n\nThank you for using this tool!"), ))?; } - _ => {} + _ => { + //TODO + //FIXME + } } Ok(()) } @@ -344,6 +351,8 @@ impl App { Action::Suspend => self.should_suspend = true, Action::Resume => self.should_suspend = false, Action::ClearScreen => tui.terminal.clear()?, + Action::KeyUp => self.list.previous(), + Action::KeyDown => self.list.next(), Action::Error(ref msg) => { self.action_tx .send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?; @@ -396,7 +405,34 @@ impl App { self.selections[0] = one; self.selections[1] = two; } - Action::SetMode(new_mode) => self.set_mode(new_mode)?, + Action::SetMode(new_mode) => { + self.set_mode(new_mode)?; + let (labels, start, lines) = + get_right_selections(self, self.prev_mode, self.cur_mode); + self.action_tx + .send(Action::UpdateRight(labels, start, lines))?; + match new_mode { + Mode::InstallDrivers | Mode::SelectDisks => { + self.action_tx.send(Action::Select(None, None))? + } + Mode::SelectParts => { + // Select first partition as boot partition + self.action_tx.send(Action::Select(Some(0), None))?; + + // Highlight 2nd or 3rd partition as OS partition + let index = if let Some(table_type) = &self.clone.table_type { + match table_type { + PartitionTableType::Guid => 2, + PartitionTableType::Legacy => 1, + } + } else { + 1 + }; + self.action_tx.send(Action::Highlight(index))?; + } + _ => {} + }; + } _ => {} } for component in &mut self.components { @@ -488,3 +524,87 @@ fn get_chunks(r: Rect) -> Vec { // Done chunks } + +fn get_right_selections( + app: &App, + prev_mode: Mode, + cur_mode: Mode, +) -> (Vec>, usize, Vec>) { + let mut labels: Vec> = Vec::new(); + let mut selections = Vec::new(); + let mut start_index = 0; + match (prev_mode, cur_mode) { + (_, Mode::InstallDrivers) => { + selections.push(vec![DVLine { + line_parts: vec![String::from("CPU")], + line_colors: vec![Color::Cyan], + }]); + selections.push(vec![DVLine { + line_parts: vec![get_cpu_name()], + line_colors: vec![Color::Reset], + }]); + start_index = 2; + } + (_, Mode::SelectDisks | Mode::SelectTableType) + | (Mode::SelectDisks | Mode::SelectTableType, Mode::Confirm) => { + // Labels + labels.push(vec![DVLine { + line_parts: vec![String::from("Source")], + line_colors: vec![Color::Cyan], + }]); + 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], + }; + match (prev_mode, cur_mode) { + (Mode::SelectTableType, Mode::Confirm) => { + if let Some(table_type) = &app.clone.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], + }, + ]); + } + } + _ => labels.push(vec![dest_dv_line]), + } + let disk_list = app.clone.disk_list.lock().unwrap(); + disk_list + .iter() + .for_each(|disk| selections.push(get_disk_description_right(&disk))); + } + (_, Mode::SelectParts) | (Mode::SelectParts, Mode::Confirm) => { + vec!["Boot", "OS"].iter().for_each(|s| { + labels.push(vec![DVLine { + line_parts: vec![String::from(*s)], + line_colors: vec![Color::Cyan], + }]) + }); + if let Some(index) = app.clone.disk_index_dest { + start_index = 1; + let disk_list = app.clone.disk_list.lock().unwrap(); + if let Some(disk) = disk_list.get(index) { + // Disk Details + selections.push(get_disk_description_right(&disk)); + + // Partition Details + disk.parts + .iter() + .for_each(|part| selections.push(get_part_description(&part))); + } + } + } + _ => {} + } + (labels, start_index, selections) +} diff --git a/deja_vu/src/components/right.rs b/deja_vu/src/components/right.rs index 1e1fc0f..960f5af 100644 --- a/deja_vu/src/components/right.rs +++ b/deja_vu/src/components/right.rs @@ -20,40 +20,58 @@ use ratatui::{ widgets::{Block, Borders, Padding, Paragraph, Wrap}, }; use tokio::sync::mpsc::UnboundedSender; -use tracing::info; use super::{state::StatefulList, Component}; -use crate::{ - action::Action, - config::Config, - state::Mode, - system::{ - cpu::get_cpu_name, - disk::{Disk, Partition, PartitionTableType}, - }, -}; +use crate::{action::Action, config::Config, line::DVLine, state::Mode}; #[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>, + list_header: Vec, + list_labels: Vec>, + list: StatefulList>, selections: Vec>, - table_type: Option, + selections_saved: Vec>, + title: String, } impl Right { pub fn new() -> Self { Self { - selected_disks: vec![None, None], selections: vec![None, None], + selections_saved: vec![None, None], + title: String::from("Info"), ..Default::default() } } + + fn get_first(&self) -> Option { + if self.selections_saved[0].is_some() { + self.selections_saved[0] + } else if self.selections[0].is_some() { + self.selections[0] + } else { + self.list.selected() + } + } + + fn get_second(&self) -> Option { + if self.selections_saved[1].is_some() { + self.selections_saved[1] + } else if self.selections[1].is_some() { + self.selections[1] + } else if self.selections[0].is_some() && self.selections[0] != self.list.selected() { + self.list.selected() + } else { + None + } + } + + fn set_highlight(&mut self, index: usize) { + self.list.select(index); + } } impl Component for Right { @@ -74,80 +92,24 @@ impl Component for Right { 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::Highlight(index) => self.set_highlight(index), + Action::KeyUp => self.list.previous(), + Action::KeyDown => self.list.next(), Action::Select(one, two) => { self.selections[0] = one; self.selections[1] = two; + self.selections_saved[0] = one; + self.selections_saved[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; - } - _ => {} - } + self.selections[0] = None; + self.selections[1] = 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); - } - } - } - } - } - } - } - } - } + Action::UpdateRight(labels, start_index, list) => { + self.list_header = list[..start_index].iter().flatten().cloned().collect(); + self.list_labels = labels; + self.list.set_items(list[start_index..].to_vec()); } _ => {} } @@ -161,230 +123,53 @@ impl Component for Right { .areas(area); // Title - let title_text = String::from("Info"); - let title = Paragraph::new(Line::from(title_text).centered()) + let title = Paragraph::new(Line::from(self.title.as_str()).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 mut body_text: Vec = Vec::new(); + if !self.list_header.is_empty() { + // Static Header + self.list_header + .iter() + .for_each(|dv| body_text.push(dv.as_line())); + body_text.push(Line::from("")); + body_text.push(Line::from(str::repeat("─", (body_area.width - 4) as usize))); + body_text.push(Line::from("")); } + + // First selection + if let Some(first_index) = self.get_first() { + if let Some(first_desc) = self.list.get(first_index) { + self.list_labels[0] + .iter() + .for_each(|dv| body_text.push(dv.as_line().bold())); + body_text.push(Line::from("")); + first_desc + .iter() + .for_each(|dv| body_text.push(dv.as_line())); + } + } + + // Second selection + if let Some(second_index) = self.get_second() { + if let Some(second_desc) = self.list.get(second_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("")); + self.list_labels[1] + .iter() + .for_each(|dv| body_text.push(dv.as_line().bold())); + body_text.push(Line::from("")); + second_desc + .iter() + .for_each(|dv| body_text.push(dv.as_line())); + } + } + + // Build Paragraph let body = Paragraph::new(body_text) .style(Style::default().fg(Color::Gray)) .wrap(Wrap { trim: false }) diff --git a/deja_vu/src/line.rs b/deja_vu/src/line.rs new file mode 100644 index 0000000..038a848 --- /dev/null +++ b/deja_vu/src/line.rs @@ -0,0 +1,94 @@ +// 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 ratatui::{ + style::{Color, Style}, + text::{Line, Span}, +}; +use serde::{Deserialize, Serialize}; +use std::iter::zip; + +use crate::system::disk::{Disk, Partition}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DVLine { + pub line_parts: Vec, + pub line_colors: Vec, +} + +impl DVLine { + /// Convert to Line with colored span(s) + pub fn as_line(&self) -> Line { + let mut spans = Vec::new(); + zip(self.line_parts.clone(), self.line_colors.clone()) + .for_each(|(part, color)| spans.push(Span::styled(part, Style::default().fg(color)))); + Line::from(spans) + } + + pub fn blank() -> Self { + Self { + line_parts: vec![String::new()], + line_colors: vec![Color::Reset], + } + } +} + +pub fn get_disk_description_right(disk: &Disk) -> Vec { + let mut description: Vec = vec![ + DVLine { + line_parts: vec![format!( + "{:<8} {:>11} {:<4} {:<4} {}", + "Disk ID", "Size", "Conn", "Type", "Model (Serial)" + )], + line_colors: vec![Color::Green], + }, + DVLine { + line_parts: vec![disk.description.clone()], + line_colors: vec![Color::Reset], + }, + DVLine::blank(), + DVLine { + line_parts: vec![format!( + "{:<8} {:>11} {:<7} {}", + "Part ID", "Size", "(FS)", "\"Label\"" + )], + line_colors: vec![Color::Blue], + }, + ]; + for line in &disk.parts_description { + description.push(DVLine { + line_parts: vec![line.clone()], + line_colors: vec![Color::Reset], + }); + } + description +} + +pub fn get_part_description(part: &Partition) -> Vec { + let description: Vec = vec![ + DVLine { + line_parts: vec![format!( + "{:<8} {:>11} {:<7} {}", + "Part ID", "Size", "(FS)", "\"Label\"" + )], + line_colors: vec![Color::Blue], + }, + DVLine { + line_parts: vec![format!("{part}")], + line_colors: vec![Color::Reset], + }, + ]; + description +} diff --git a/deja_vu/src/main.rs b/deja_vu/src/main.rs index 085974a..c5e3a71 100644 --- a/deja_vu/src/main.rs +++ b/deja_vu/src/main.rs @@ -25,6 +25,7 @@ mod cli; mod components; mod config; mod errors; +mod line; mod logging; mod state; mod system;