// 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 std::{ iter::zip, sync::{Arc, Mutex}, thread::{self, JoinHandle}, }; use color_eyre::Result; use crossterm::event::KeyEvent; use ratatui::{ layout::{Constraint, Direction, Layout}, prelude::Rect, }; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{debug, info}; use crate::{ action::Action, components::{ footer::Footer, fps::FpsCounter, left::Left, popup::{Popup, Type}, right::Right, title::Title, Component, }, config::Config, system::disk::{get_disks, Disk}, tui::{Event, Tui}, }; pub struct App { config: Config, tick_rate: f64, frame_rate: f64, components: Vec>, disks: Arc>>, should_quit: bool, should_suspend: bool, cur_mode: Mode, prev_mode: Mode, selections: Vec>, last_tick_key_events: Vec, action_tx: mpsc::UnboundedSender, action_rx: mpsc::UnboundedReceiver, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Mode { #[default] ScanDisks, SelectDisks, SelectParts, Confirm, Done, } 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, components: vec![ Box::new(Title::new()), Box::new(FpsCounter::default()), Box::new(Left::new()), Box::new(Right::new()), Box::new(Footer::new()), Box::new(Popup::new()), ], disks: Arc::new(Mutex::new(Vec::new())), should_quit: false, should_suspend: false, config: Config::new()?, cur_mode: Mode::ScanDisks, prev_mode: Mode::ScanDisks, selections: vec![None, None], last_tick_key_events: Vec::new(), action_tx, action_rx, }) } pub fn next_mode(&mut self) -> Option { let new_mode = match self.cur_mode { Mode::ScanDisks => Mode::SelectDisks, Mode::SelectDisks | 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, }, }; if new_mode != self.cur_mode { if self.cur_mode != Mode::Confirm { self.prev_mode = self.cur_mode; } Some(new_mode) } else { None } } pub async fn run(&mut self) -> Result<()> { let disk_wrapper = Arc::clone(&self.disks); let _ = lazy_get_disks(disk_wrapper); let mut tui = Tui::new()? // .mouse(true) // uncomment this line to enable mouse support .tick_rate(self.tick_rate) .frame_rate(self.frame_rate); tui.enter()?; for component in self.components.iter_mut() { component.register_action_handler(self.action_tx.clone())?; } for component in self.components.iter_mut() { component.register_config_handler(self.config.clone())?; } for component in self.components.iter_mut() { component.init(tui.size()?)?; } let action_tx = self.action_tx.clone(); loop { self.handle_events(&mut tui).await?; self.handle_actions(&mut tui)?; if self.should_suspend { tui.suspend()?; action_tx.send(Action::Resume)?; action_tx.send(Action::ClearScreen)?; // tui.mouse(true); tui.enter()?; } else if self.should_quit { tui.stop()?; break; } } tui.exit()?; Ok(()) } async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { let Some(event) = tui.next_event().await else { return Ok(()); }; let action_tx = self.action_tx.clone(); match event { Event::Quit => action_tx.send(Action::Quit)?, Event::Tick => action_tx.send(Action::Tick)?, Event::Render => action_tx.send(Action::Render)?, Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, Event::Key(key) => self.handle_key_event(key)?, _ => {} } for component in self.components.iter_mut() { if let Some(action) = component.handle_events(Some(event.clone()))? { action_tx.send(action)?; } } Ok(()) } fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { let action_tx = self.action_tx.clone(); let Some(keymap) = self.config.keybindings.get(&self.cur_mode) else { return Ok(()); }; match keymap.get(&vec![key]) { Some(action) => { info!("Got action: {action:?}"); action_tx.send(action.clone())?; } _ => { // If the key was not handled as a single key action, // then consider it for multi-key combinations. self.last_tick_key_events.push(key); // Check for multi-key combinations if let Some(action) = keymap.get(&self.last_tick_key_events) { info!("Got action: {action:?}"); action_tx.send(action.clone())?; } } } Ok(()) } fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> { while let Ok(action) = self.action_rx.try_recv() { if action != Action::Tick && action != Action::Render { debug!("{action:?}"); } 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)?; } } } Action::Quit => self.should_quit = true, Action::Suspend => self.should_suspend = true, Action::Resume => self.should_suspend = false, Action::ClearScreen => tui.terminal.clear()?, Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, Action::PrevScreen => { self.action_tx.send(Action::SetMode(self.prev_mode))?; } Action::NextScreen => { if let Some(mode) = self.next_mode() { self.action_tx.send(Action::SetMode(mode))?; } } Action::ScanDisks => { let disk_wrapper = Arc::clone(&self.disks); let _ = lazy_get_disks(disk_wrapper); self.action_tx.send(Action::SetMode(Mode::ScanDisks))?; } Action::Select(one, two) => { self.selections[0] = one; self.selections[1] = two; } Action::SetMode(new_mode) => { self.cur_mode = new_mode; match new_mode { Mode::ScanDisks => { self.prev_mode = self.cur_mode; } Mode::SelectDisks | Mode::SelectParts => { let disks = self.disks.lock().unwrap(); self.action_tx.send(Action::UpdateDiskList(disks.clone()))?; } Mode::Done => { self.action_tx.send(Action::DisplayPopup( Type::Success, String::from("COMPLETE\n\n\nThank you for using this tool!"), ))?; } _ => {} } } _ => {} } for component in self.components.iter_mut() { if let Some(action) = component.update(action.clone())? { self.action_tx.send(action)? }; } } Ok(()) } fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> { tui.resize(Rect::new(0, 0, w, h))?; self.render(tui)?; Ok(()) } fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] { let component_areas = vec![ header, // Title Bar header, // FPS Counter left, right, footer, popup, ]; for (component, area) in zip(self.components.iter_mut(), component_areas) { if let Err(err) = component.draw(frame, area) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } } }; })?; Ok(()) } } fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { // Cut the given rectangle into three vertical pieces let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]) .split(r); // Then cut the middle vertical piece into three width-wise pieces Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ]) .split(popup_layout[1])[1] // Return the middle chunk } fn get_chunks(r: Rect) -> Vec { let mut chunks: Vec = Vec::with_capacity(6); // Main sections chunks.extend( Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ]) .split(r) .to_vec(), ); // Left/Right chunks.extend( Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(centered_rect(90, 90, chunks[1])) .to_vec(), ); // Popup chunks.push(centered_rect(60, 25, r)); // Done chunks } fn lazy_get_disks(disk_wrapper: Arc>>) -> JoinHandle<()> { thread::spawn(move || { let mut disks = disk_wrapper.lock().unwrap(); *disks = get_disks(); }) }