401 lines
17 KiB
Rust
401 lines
17 KiB
Rust
// 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 <https://www.gnu.org/licenses/>.
|
|
//
|
|
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<UnboundedSender<Action>>,
|
|
config: Config,
|
|
cur_mode: Mode,
|
|
list_disks: StatefulList<Disk>,
|
|
list_parts: StatefulList<Partition>,
|
|
prev_mode: Mode,
|
|
selected_disks: Vec<Option<usize>>,
|
|
selections: Vec<Option<usize>>,
|
|
table_type: Option<PartitionTableType>,
|
|
}
|
|
|
|
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<Option<Action>> {
|
|
let _ = key; // to appease clippy
|
|
Ok(None)
|
|
}
|
|
|
|
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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<Option<Action>> {
|
|
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(())
|
|
}
|
|
}
|