// 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::*}; use tokio::sync::mpsc::UnboundedSender; use super::{state::StatefulList, Component}; use crate::{action::Action, app::Mode, config::Config, system::disk::Disk}; #[derive(Default)] pub struct Left { command_tx: Option>, config: Config, title_text: String, item_list: 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> { // TODO 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::Tick => { // add any logic here that should run on every tick } Action::Render => { // add any logic here that should run on every render } Action::KeyUp => self.item_list.previous(), Action::KeyDown => self.item_list.next(), Action::Process => match self.mode { Mode::Confirm => { if let Some(command_tx) = self.command_tx.clone() { command_tx.send(Action::NextScreen)?; } } Mode::SelectDisks | Mode::SelectParts => { if let Some(index) = self.item_list.state.selected() { 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 { selection_one = self.selections[0]; selection_two = Some(index); } } } // Send selection(s) // NOTE: This is needed to keep the app and all components in sync match self.mode { Mode::SelectDisks => { command_tx .send(Action::Select(selection_one, selection_two))?; } 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::SetMode(new_mode) => { let prev_mode = self.mode; self.mode = new_mode; match new_mode { Mode::ScanDisks => { self.title_text = String::from(""); self.item_list.clear_items(); } Mode::SelectDisks => { self.selections[0] = None; self.selections[1] = None; self.title_text = String::from("Select Source and Destination Disks"); if let Some(command_tx) = self.command_tx.clone() { command_tx.send(Action::DismissPopup)?; } } Mode::SelectParts => { 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"), } } Action::UpdateDiskList(disks) => self.item_list.set_items(disks), _ => {} } 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 (scan disks) if self.item_list.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( Block::default() .borders(Borders::ALL) .padding(Padding::new(1, 1, 1, 1)), ); frame.render_widget(paragraph, body_area); // Bail early return Ok(()); } // Body (list) let mut list_items = Vec::::new(); if !self.item_list.items.is_empty() { for (index, item) in self.item_list.items.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), ); } } 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.item_list.state); // Done Ok(()) } }