Move fast, break things
Data structures and logic was changing very fast. Don't have the time to break downt the individual sections ATM. Sorry future reader (me).
This commit is contained in:
parent
31de043b71
commit
7d4f28f950
18 changed files with 1331 additions and 72 deletions
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"keybindings": {
|
||||
"Home": {
|
||||
"<b>": "PrevScreen",
|
||||
"ScanDisks": {
|
||||
"<Enter>": "Process",
|
||||
"<q>": "Quit", // Quit the application
|
||||
"<Ctrl-d>": "Quit", // Another way to quit
|
||||
|
|
@ -9,14 +8,26 @@
|
|||
"<Ctrl-z>": "Suspend" // Suspend the application
|
||||
},
|
||||
"SelectDisks": {
|
||||
"<b>": "PrevScreen",
|
||||
"<r>": "ScanDisks",
|
||||
"<Enter>": "Process",
|
||||
"<Up>": "KeyUp",
|
||||
"<Down>": "KeyDown",
|
||||
"<q>": "Quit", // Quit the application
|
||||
"<Ctrl-d>": "Quit", // Another way to quit
|
||||
"<Ctrl-c>": "Quit", // Yet another way to quit
|
||||
"<Ctrl-z>": "Suspend" // Suspend the application
|
||||
},
|
||||
"SelectParts": {
|
||||
"<b>": "PrevScreen",
|
||||
"<Enter>": "Process",
|
||||
"<Up>": "KeyUp",
|
||||
"<Down>": "KeyDown",
|
||||
"<q>": "Quit", // Quit the application
|
||||
"<Ctrl-d>": "Quit", // Another way to quit
|
||||
"<Ctrl-c>": "Quit", // Yet another way to quit
|
||||
"<Ctrl-z>": "Suspend" // Suspend the application
|
||||
},
|
||||
"Confirm": {
|
||||
"<b>": "PrevScreen",
|
||||
"<Enter>": "Process",
|
||||
"<q>": "Quit", // Quit the application
|
||||
|
|
@ -29,7 +40,6 @@
|
|||
"<q>": "Quit", // Quit the application
|
||||
"<Ctrl-d>": "Quit", // Another way to quit
|
||||
"<Ctrl-c>": "Quit", // Yet another way to quit
|
||||
"<Ctrl-z>": "Suspend" // Suspend the application
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
Cargo.lock
generated
18
Cargo.lock
generated
|
|
@ -527,19 +527,24 @@ dependencies = [
|
|||
"json5",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"ratatui",
|
||||
"raw-cpuid",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"signal-hook",
|
||||
"strip-ansi-escapes",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"vergen-gix",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1881,6 +1886,15 @@ dependencies = [
|
|||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.7"
|
||||
|
|
@ -1903,9 +1917,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
|
|||
|
|
@ -43,18 +43,23 @@ human-panic = "2.0.1"
|
|||
json5 = "0.4.1"
|
||||
lazy_static = "1.5.0"
|
||||
libc = "0.2.158"
|
||||
once_cell = "1.20.2"
|
||||
pretty_assertions = "1.4.0"
|
||||
ratatui = { version = "0.28.1", features = ["serde", "macros"] }
|
||||
raw-cpuid = "11.2.0"
|
||||
regex = "1.11.1"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
signal-hook = "0.3.17"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
tempfile = "3.13.0"
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
tokio-util = "0.7.11"
|
||||
tracing = "0.1.40"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.86"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
|
||||
use crate::app::Mode;
|
||||
use crate::{app::Mode, system::disk::Disk};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
|
||||
pub enum Action {
|
||||
|
|
@ -27,11 +27,17 @@ pub enum Action {
|
|||
Resume,
|
||||
Quit,
|
||||
ClearScreen,
|
||||
DismissPopup,
|
||||
DisplayPopup(String),
|
||||
Error(String),
|
||||
Help,
|
||||
KeyUp,
|
||||
KeyDown,
|
||||
SetMode(Mode),
|
||||
PrevScreen,
|
||||
NextScreen,
|
||||
Process,
|
||||
SelectDisks(Option<u16>, Option<u16>), // indicies for (source, dest)
|
||||
SelectParts(Option<u16>, Option<u16>), // indicies for (boot, os)
|
||||
ScanDisks,
|
||||
Select(Option<usize>, Option<usize>), // indicies for (source, dest) or (boot, os)
|
||||
UpdateDiskList(Vec<Disk>),
|
||||
}
|
||||
|
|
|
|||
107
src/app.rs
107
src/app.rs
|
|
@ -13,7 +13,7 @@
|
|||
// 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 std::{collections::HashMap, iter::zip};
|
||||
use std::iter::zip;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
|
@ -40,10 +40,11 @@ pub struct App {
|
|||
tick_rate: f64,
|
||||
frame_rate: f64,
|
||||
components: Vec<Box<dyn Component>>,
|
||||
component_indices: HashMap<String, u64>,
|
||||
should_quit: bool,
|
||||
should_suspend: bool,
|
||||
mode: Mode,
|
||||
cur_mode: Mode,
|
||||
prev_mode: Mode,
|
||||
selections: Vec<Option<usize>>,
|
||||
last_tick_key_events: Vec<KeyEvent>,
|
||||
action_tx: mpsc::UnboundedSender<Action>,
|
||||
action_rx: mpsc::UnboundedReceiver<Action>,
|
||||
|
|
@ -52,32 +53,13 @@ pub struct App {
|
|||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Home,
|
||||
ScanDisks,
|
||||
SelectDisks,
|
||||
SelectParts,
|
||||
Confirm,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn next_screen(cur_mode: Mode) -> Mode {
|
||||
match cur_mode {
|
||||
Mode::Home => Mode::SelectDisks,
|
||||
Mode::SelectDisks => Mode::SelectParts,
|
||||
Mode::SelectParts => Mode::Done,
|
||||
Mode::Done => Mode::Done,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_screen(cur_mode: Mode) -> Mode {
|
||||
match cur_mode {
|
||||
Mode::Home => Mode::Home,
|
||||
Mode::SelectDisks => Mode::Home,
|
||||
Mode::SelectParts => Mode::SelectDisks,
|
||||
Mode::Done => Mode::SelectParts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
|
||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||
|
|
@ -92,24 +74,45 @@ impl App {
|
|||
Box::new(Footer::new()),
|
||||
Box::new(Popup::new()),
|
||||
],
|
||||
component_indices: HashMap::from([
|
||||
(String::from("title"), 0),
|
||||
(String::from("fps"), 1),
|
||||
(String::from("left"), 2),
|
||||
(String::from("right"), 3),
|
||||
(String::from("footer"), 4),
|
||||
(String::from("popup"), 5),
|
||||
]),
|
||||
should_quit: false,
|
||||
should_suspend: false,
|
||||
config: Config::new()?,
|
||||
mode: Mode::Home,
|
||||
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<Mode> {
|
||||
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 mut tui = Tui::new()?
|
||||
// .mouse(true) // uncomment this line to enable mouse support
|
||||
|
|
@ -169,7 +172,7 @@ impl App {
|
|||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
|
||||
let action_tx = self.action_tx.clone();
|
||||
let Some(keymap) = self.config.keybindings.get(&self.mode) else {
|
||||
let Some(keymap) = self.config.keybindings.get(&self.cur_mode) else {
|
||||
return Ok(());
|
||||
};
|
||||
match keymap.get(&vec![key]) {
|
||||
|
|
@ -207,13 +210,35 @@ impl App {
|
|||
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(Mode::prev_screen(self.mode)))?,
|
||||
Action::Process => self
|
||||
.action_tx
|
||||
.send(Action::SetMode(Mode::next_screen(self.mode)))?,
|
||||
Action::SetMode(new_mode) => self.mode = new_mode,
|
||||
Action::PrevScreen => {
|
||||
self.action_tx.send(Action::SetMode(self.prev_mode))?;
|
||||
self.action_tx.send(Action::Select(None, None))?;
|
||||
}
|
||||
Action::NextScreen => {
|
||||
if let Some(mode) = self.next_mode() {
|
||||
self.action_tx.send(Action::SetMode(mode))?;
|
||||
}
|
||||
}
|
||||
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;
|
||||
self.action_tx.send(Action::Select(None, None))?;
|
||||
self.action_tx.send(Action::ScanDisks)?;
|
||||
}
|
||||
Mode::Done => {
|
||||
self.action_tx.send(Action::DisplayPopup(String::from(
|
||||
"COMPLETE\n\nThank you for using this tool!",
|
||||
)))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub mod fps;
|
|||
pub mod left;
|
||||
pub mod popup;
|
||||
pub mod right;
|
||||
pub mod state;
|
||||
pub mod title;
|
||||
|
||||
/// `Component` is a trait that represents a visual and interactive element of the user interface.
|
||||
|
|
|
|||
|
|
@ -57,9 +57,13 @@ impl Component for Footer {
|
|||
}
|
||||
Action::SetMode(new_mode) => {
|
||||
self.footer_text = match new_mode {
|
||||
Mode::ScanDisks => String::from("(Enter) to start / (q) to quit"),
|
||||
Mode::SelectDisks => String::from(
|
||||
"(Enter) to select / (b) to go back / (i) to install driver / (q) to quit",
|
||||
"(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit",
|
||||
),
|
||||
Mode::Confirm => {
|
||||
String::from("(Enter) to confirm / (b) to go back / (q) to quit")
|
||||
}
|
||||
Mode::Done => String::from("(Enter) or (q) to quit"),
|
||||
_ => String::from("(Enter) to select / (b) to go back / (q) to quit"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,23 @@ use crossterm::event::KeyEvent;
|
|||
use ratatui::{prelude::*, widgets::*};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, app::Mode, config::Config};
|
||||
use super::{state::StatefulList, Component};
|
||||
use crate::{action::Action, app::Mode, config::Config, system::disk::Disk};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Left {
|
||||
command_tx: Option<UnboundedSender<Action>>,
|
||||
config: Config,
|
||||
title_text: String,
|
||||
item_list: StatefulList<Disk>,
|
||||
mode: Mode,
|
||||
selections: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
impl Left {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
selections: vec![None, None],
|
||||
title_text: String::from("Home"),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -62,12 +66,92 @@ impl Component for Left {
|
|||
Action::Render => {
|
||||
// add any logic here that should run on every render
|
||||
}
|
||||
Action::SetMode(new_mode) => match new_mode {
|
||||
Mode::Home => self.title_text = String::from("Home"),
|
||||
Mode::SelectDisks => self.title_text = String::from("Select Source Disk"),
|
||||
Mode::SelectParts => self.title_text = String::from("Select Boot Partition"),
|
||||
Mode::Done => self.title_text = String::from("Done"),
|
||||
Action::KeyUp => self.item_list.previous(),
|
||||
Action::KeyDown => self.item_list.next(),
|
||||
Action::Process => match self.mode {
|
||||
Mode::ScanDisks => {
|
||||
if let Some(command_tx) = self.command_tx.clone() {
|
||||
command_tx.send(Action::ScanDisks)?;
|
||||
}
|
||||
}
|
||||
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<usize> = None;
|
||||
let mut selection_two: Option<usize> = 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(None, None) => self.selections = vec![None, None],
|
||||
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(""),
|
||||
Mode::SelectDisks => {
|
||||
self.selections[0] = None;
|
||||
self.selections[1] = None;
|
||||
self.title_text = String::from("Select Source and Destination Disks")
|
||||
}
|
||||
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)
|
||||
|
|
@ -85,13 +169,61 @@ impl Component for Left {
|
|||
.block(Block::default().borders(Borders::NONE));
|
||||
frame.render_widget(title, title_area);
|
||||
|
||||
// Body
|
||||
let paragraph = Paragraph::new("Body text...").block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.padding(Padding::new(1, 1, 1, 1)),
|
||||
);
|
||||
frame.render_widget(paragraph, body_area);
|
||||
// Body (scan disks)
|
||||
if self.item_list.items.is_empty() {
|
||||
let paragraph = Paragraph::new(String::from("Press Enter to start!")).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::<ListItem>::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(())
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use clap::command;
|
||||
// This file is part of Deja-vu.
|
||||
//
|
||||
// Deja-vu is free software: you can redistribute it and/or modify it
|
||||
|
|
@ -18,17 +19,27 @@ use ratatui::{prelude::*, widgets::*};
|
|||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, config::Config};
|
||||
use crate::{
|
||||
action::Action,
|
||||
app::Mode,
|
||||
config::Config,
|
||||
system::disk::{get_disks, Disk},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Popup {
|
||||
command_tx: Option<UnboundedSender<Action>>,
|
||||
config: Config,
|
||||
disks: Vec<Disk>,
|
||||
popup_text: String,
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
Self {
|
||||
popup_text: String::from("Scanning Disks..."),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,16 +62,35 @@ impl Component for Popup {
|
|||
Action::Render => {
|
||||
// add any logic here that should run on every render
|
||||
}
|
||||
Action::DismissPopup => self.popup_text.clear(),
|
||||
Action::DisplayPopup(new_text) => self.popup_text = String::from(new_text),
|
||||
Action::ScanDisks => {
|
||||
if let Some(command_tx) = self.command_tx.clone() {
|
||||
self.disks = get_disks();
|
||||
command_tx.send(Action::NextScreen)?;
|
||||
}
|
||||
}
|
||||
Action::SetMode(Mode::SelectDisks) => {
|
||||
self.popup_text.clear();
|
||||
if let Some(command_tx) = self.command_tx.clone() {
|
||||
command_tx.send(Action::UpdateDiskList(self.disks.clone()))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.popup_text.is_empty() {
|
||||
// Only show popup if popup_text is set
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let popup_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().cyan().bold());
|
||||
let popup = Paragraph::new(vec![Line::from(Span::raw("Popup text..."))])
|
||||
let popup = Paragraph::new(vec![Line::from(Span::raw(&self.popup_text))])
|
||||
.block(popup_block)
|
||||
.centered()
|
||||
.wrap(Wrap { trim: false });
|
||||
|
|
|
|||
|
|
@ -19,17 +19,24 @@ use ratatui::{prelude::*, widgets::*};
|
|||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, config::Config};
|
||||
use crate::{action::Action, app::Mode, config::Config};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Right {
|
||||
command_tx: Option<UnboundedSender<Action>>,
|
||||
config: Config,
|
||||
cur_mode: Mode,
|
||||
prev_mode: Mode,
|
||||
all_modes: Vec<Mode>,
|
||||
selections: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
impl Right {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
Self {
|
||||
selections: vec![None, None],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +65,17 @@ impl Component for Right {
|
|||
Action::Render => {
|
||||
// add any logic here that should run on every render
|
||||
}
|
||||
Action::Select(one, two) => {
|
||||
self.selections[0] = one;
|
||||
self.selections[1] = two;
|
||||
}
|
||||
Action::SetMode(new_mode) => {
|
||||
if self.cur_mode != Mode::Confirm {
|
||||
self.prev_mode = self.cur_mode;
|
||||
}
|
||||
self.cur_mode = new_mode;
|
||||
self.all_modes.push(new_mode);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -76,7 +94,11 @@ impl Component for Right {
|
|||
frame.render_widget(title, title_area);
|
||||
|
||||
// Body
|
||||
let paragraph = Paragraph::new("Some info...").block(
|
||||
let paragraph = Paragraph::new(format!(
|
||||
"Prev Mode: {:?} // Cur Mode: {:?}\n{:?}\n{:?}",
|
||||
self.prev_mode, self.cur_mode, self.all_modes, self.selections,
|
||||
))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.padding(Padding::new(1, 1, 1, 1)),
|
||||
|
|
|
|||
|
|
@ -13,3 +13,116 @@
|
|||
// 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 std::collections::{hash_map, HashMap};
|
||||
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
pub last_selected: Option<usize>,
|
||||
pub selected: HashMap<usize, bool>,
|
||||
}
|
||||
|
||||
impl<T: Clone> StatefulList<T> {
|
||||
#[must_use]
|
||||
pub fn new() -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: Vec::new(),
|
||||
last_selected: None,
|
||||
selected: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_items(&mut self) {
|
||||
// Clear list and rebuild with provided items
|
||||
self.items.clear();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_selected(&self) -> Option<&T> {
|
||||
if let Some(i) = self.state.selected() {
|
||||
self.items.get(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_selected(&mut self) -> Option<T> {
|
||||
if let Some(i) = self.state.selected() {
|
||||
Some(self.items[i].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first_item(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
self.state.select(None);
|
||||
} else {
|
||||
self.state.select(Some(0));
|
||||
}
|
||||
self.last_selected = None;
|
||||
}
|
||||
|
||||
pub fn set_items(&mut self, items: Vec<T>) {
|
||||
// Clear list and rebuild with provided items
|
||||
self.clear_items();
|
||||
for item in items {
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
// Reset state and select first item (if available)
|
||||
self.state = ListState::default();
|
||||
self.select_first_item();
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
pub fn toggle_selected(&mut self) {
|
||||
if let Some(i) = self.state.selected() {
|
||||
if let hash_map::Entry::Vacant(e) = self.selected.entry(i) {
|
||||
// Was NOT selected, WILL be shortly
|
||||
self.last_selected = Some(i);
|
||||
e.insert(true);
|
||||
} else {
|
||||
// WAS selected, will NOT be shortly
|
||||
self.last_selected = None;
|
||||
self.selected.remove(&i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,7 +520,7 @@ mod tests {
|
|||
let c = Config::new()?;
|
||||
assert_eq!(
|
||||
c.keybindings
|
||||
.get(&Mode::Home)
|
||||
.get(&Mode::ScanDisks) // i.e. Home
|
||||
.unwrap()
|
||||
.get(&parse_key_sequence("<q>").unwrap_or_default())
|
||||
.unwrap(),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ mod components;
|
|||
mod config;
|
||||
mod errors;
|
||||
mod logging;
|
||||
mod system;
|
||||
mod tui;
|
||||
|
||||
#[tokio::main]
|
||||
|
|
|
|||
19
src/system.rs
Normal file
19
src/system.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// 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/>.
|
||||
//
|
||||
pub mod cpu;
|
||||
pub mod disk;
|
||||
pub mod diskpart;
|
||||
pub mod drivers;
|
||||
28
src/system/cpu.rs
Normal file
28
src/system/cpu.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// 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/>.
|
||||
//
|
||||
|
||||
/// CPU Functions
|
||||
#[must_use]
|
||||
pub fn get_cpu_name() -> String {
|
||||
let cpuid = raw_cpuid::CpuId::new();
|
||||
if let Some(name) = cpuid.get_processor_brand_string() {
|
||||
String::from(name.as_str())
|
||||
} else if let Some(vendor) = cpuid.get_vendor_info() {
|
||||
format!("{vendor}: Unknown Model")
|
||||
} else {
|
||||
String::from("Unknown")
|
||||
}
|
||||
}
|
||||
364
src/system/disk.rs
Normal file
364
src/system/disk.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
// 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 serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt,
|
||||
process::{Command, Stdio},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::system::diskpart;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Disk {
|
||||
pub conn_type: String,
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub part_type: PartitionTableType,
|
||||
pub parts: Vec<Partition>,
|
||||
pub parts_description: Vec<String>,
|
||||
pub sector_size: usize,
|
||||
pub serial: String,
|
||||
pub size: u64, // In bytes
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Partition {
|
||||
pub id: String,
|
||||
pub is_selected: bool,
|
||||
pub fs_type: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub letter: Option<String>,
|
||||
pub part_type: String,
|
||||
pub size: u64, // In bytes
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PartitionTableType {
|
||||
#[default]
|
||||
Guid,
|
||||
Legacy,
|
||||
}
|
||||
|
||||
impl Disk {
|
||||
pub fn generate_descriptions(&mut self) {
|
||||
for part in &self.parts {
|
||||
self.parts_description.push(format!("{part}"));
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn get_id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Disk {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if cfg!(windows) {
|
||||
write!(
|
||||
f,
|
||||
"Disk {:<3} {:>11} {:<4} {} ({})",
|
||||
self.id,
|
||||
bytes_to_string(&self.size),
|
||||
self.conn_type,
|
||||
self.model,
|
||||
self.serial,
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{:<14} {:>11} {:<4} {} ({})",
|
||||
self.id,
|
||||
bytes_to_string(&self.size),
|
||||
self.conn_type,
|
||||
self.model,
|
||||
self.serial,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Partition {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut s: String;
|
||||
let fs = if let Some(fs_type) = &self.fs_type {
|
||||
format!("({fs_type})")
|
||||
} else {
|
||||
String::from("(?)")
|
||||
};
|
||||
if cfg!(windows) {
|
||||
s = format!(
|
||||
"{:<8} {:>11} {:<7}",
|
||||
self.id,
|
||||
bytes_to_string(&self.size),
|
||||
fs
|
||||
);
|
||||
} else {
|
||||
s = format!(
|
||||
"{:<14} {:>11} {:<7}",
|
||||
self.id,
|
||||
bytes_to_string(&self.size),
|
||||
fs
|
||||
);
|
||||
}
|
||||
if let Some(l) = &self.label {
|
||||
s = format!("{s} \"{l}\"");
|
||||
}
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PartitionTableType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
PartitionTableType::Guid => write!(f, "GPT / UEFI"),
|
||||
PartitionTableType::Legacy => write!(f, "MBR / Legacy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_disks() -> Vec<Disk> {
|
||||
let disks;
|
||||
if cfg!(windows) {
|
||||
disks = diskpart::get_disks();
|
||||
} else {
|
||||
disks = get_fake_disks();
|
||||
sleep(Duration::from_secs(2));
|
||||
}
|
||||
disks
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[must_use]
|
||||
pub fn get_fake_disks() -> Vec<Disk> {
|
||||
let mut disks = vec![
|
||||
Disk {
|
||||
conn_type: "SATA".to_string(),
|
||||
id: "/dev/sda".to_string(),
|
||||
model: "Samsung Evo 870".to_string(),
|
||||
part_type: PartitionTableType::Legacy,
|
||||
parts: vec![
|
||||
Partition {
|
||||
id: String::from("1"),
|
||||
fs_type: Some(String::from("NTFS")),
|
||||
label: Some(String::from("System Reserved")),
|
||||
part_type: String::from("7"),
|
||||
size: 104_857_600,
|
||||
..Default::default()
|
||||
},
|
||||
Partition {
|
||||
id: String::from("2"),
|
||||
fs_type: None,
|
||||
label: None,
|
||||
part_type: String::from("5"),
|
||||
size: 267_806_310_400,
|
||||
..Default::default()
|
||||
},
|
||||
Partition {
|
||||
id: String::from("5"),
|
||||
fs_type: Some(String::from("NTFS")),
|
||||
label: Some(String::from("Win7")),
|
||||
part_type: String::from("7"),
|
||||
size: 267_701_452_800,
|
||||
..Default::default()
|
||||
},
|
||||
Partition {
|
||||
id: String::from("6"),
|
||||
fs_type: Some(String::from("NTFS")),
|
||||
label: Some(String::from("Tools")),
|
||||
part_type: String::from("7"),
|
||||
size: 524_288_000,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
serial: "MDZ1243".to_string(),
|
||||
size: 268_435_456_000,
|
||||
..Default::default()
|
||||
},
|
||||
Disk {
|
||||
conn_type: "SATA".to_string(),
|
||||
id: "Disk 2".to_string(),
|
||||
model: "ADATA Garbage".to_string(),
|
||||
part_type: PartitionTableType::Legacy,
|
||||
parts: vec![Partition {
|
||||
id: String::from("1"),
|
||||
fs_type: Some(String::from("NTFS")),
|
||||
label: Some(String::from("Scratch")),
|
||||
part_type: String::from("7"),
|
||||
size: 249_998_951_424,
|
||||
..Default::default()
|
||||
}],
|
||||
serial: "000010000".to_string(),
|
||||
size: 250_000_000_000,
|
||||
..Default::default()
|
||||
},
|
||||
Disk {
|
||||
conn_type: "NVMe".to_string(),
|
||||
id: "/dev/nvme0n1".to_string(),
|
||||
model: "Crucial P3 Plus".to_string(),
|
||||
part_type: PartitionTableType::Guid,
|
||||
parts: vec![
|
||||
Partition {
|
||||
id: String::from("1"),
|
||||
fs_type: Some(String::from("FAT32")),
|
||||
label: Some(String::from("ESP")),
|
||||
part_type: String::from("EFI"),
|
||||
size: 272_629_760,
|
||||
..Default::default()
|
||||
},
|
||||
Partition {
|
||||
id: String::from("2"),
|
||||
fs_type: None,
|
||||
label: None,
|
||||
part_type: String::from("MSR"),
|
||||
size: 16_777_216,
|
||||
..Default::default()
|
||||
},
|
||||
Partition {
|
||||
id: String::from("3"),
|
||||
fs_type: Some(String::from("NTFS")),
|
||||
label: Some(String::from("Win10")),
|
||||
part_type: String::from("MS Basic Data"),
|
||||
size: 824_340_119_552,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
serial: "80085".to_string(),
|
||||
size: 268_435_456_000,
|
||||
..Default::default()
|
||||
},
|
||||
Disk {
|
||||
conn_type: "IDE".to_string(),
|
||||
id: "/dev/hda".to_string(),
|
||||
model: "Fireball".to_string(),
|
||||
part_type: PartitionTableType::Guid,
|
||||
parts: vec![
|
||||
Partition {
|
||||
id: String::from("1"),
|
||||
fs_type: Some(String::from("FAT32")),
|
||||
label: Some(String::from("EFI Boot")),
|
||||
part_type: String::from("EFI"),
|
||||
size: 209_715_200,
|
||||
..Default::default()
|
||||
},
|
||||
Partition {
|
||||
id: String::from("2"),
|
||||
fs_type: None,
|
||||
label: None,
|
||||
part_type: String::from("{48465300-0000-11AA-AA11-00306543ECAC}"),
|
||||
size: 171_586_879_488,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
serial: "0xfff".to_string(),
|
||||
size: 171_798_691_840,
|
||||
..Default::default()
|
||||
},
|
||||
Disk {
|
||||
conn_type: "MISC".to_string(),
|
||||
id: "A:\\".to_string(),
|
||||
part_type: PartitionTableType::Legacy,
|
||||
parts: Vec::new(),
|
||||
model: "Iomega".to_string(),
|
||||
serial: "000".to_string(),
|
||||
size: 14,
|
||||
..Default::default()
|
||||
},
|
||||
Disk::default(),
|
||||
];
|
||||
for disk in &mut disks {
|
||||
disk.generate_descriptions();
|
||||
}
|
||||
disks
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_disk_serial_number(id: &str) -> String {
|
||||
let mut serial = String::new();
|
||||
if cfg!(windows) {
|
||||
let output = Command::new("wmic")
|
||||
.args([
|
||||
"diskdrive",
|
||||
"where",
|
||||
format!("index='{id}'").as_str(),
|
||||
"get",
|
||||
"serialNumber",
|
||||
"/value",
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.output();
|
||||
if let Ok(result) = output {
|
||||
let s = String::from_utf8_lossy(&result.stdout).trim().to_string();
|
||||
if s.len() >= 15 {
|
||||
serial = String::from(&s[14..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
serial
|
||||
}
|
||||
|
||||
/// Misc
|
||||
///
|
||||
/// Clippy exception is fine because this supports sizes up to 2 EiB
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
#[must_use]
|
||||
pub fn bytes_to_string(size: &u64) -> String {
|
||||
let units = "KMGTPEZY".chars();
|
||||
let scale = 1024.0;
|
||||
let mut size = *size as f64;
|
||||
let mut suffix: Option<char> = None;
|
||||
for u in units {
|
||||
if size < scale {
|
||||
break;
|
||||
}
|
||||
size /= scale;
|
||||
suffix = Some(u);
|
||||
}
|
||||
if let Some(s) = suffix {
|
||||
format!("{size:4.2} {s}iB")
|
||||
} else {
|
||||
format!("{size:4.2} B")
|
||||
}
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if s is not simliar to 32B, 64MB, etc...
|
||||
pub fn string_to_bytes(s: &str) -> u64 {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s+(\w+)B").unwrap());
|
||||
let base: u64 = 1024;
|
||||
let mut size: u64 = 0;
|
||||
for (_, [size_str, suffix]) in RE.captures_iter(s).map(|c| c.extract()) {
|
||||
let x: u64 = size_str.parse().unwrap();
|
||||
size += x;
|
||||
match suffix {
|
||||
"K" => size *= base,
|
||||
"M" => size *= base.pow(2),
|
||||
"G" => size *= base.pow(3),
|
||||
"T" => size *= base.pow(4),
|
||||
"P" => size *= base.pow(5),
|
||||
"E" => size *= base.pow(6),
|
||||
"Z" => size *= base.pow(7),
|
||||
"Y" => size *= base.pow(8),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
382
src/system/diskpart.rs
Normal file
382
src/system/diskpart.rs
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
// 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 std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::Write,
|
||||
process::{Command, Output, Stdio},
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use tempfile::tempdir;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::system::disk::{
|
||||
get_disk_serial_number, string_to_bytes, Disk, Partition, PartitionTableType,
|
||||
};
|
||||
|
||||
static DEFAULT_MAX_DISKS: usize = 8;
|
||||
|
||||
pub fn add_disk_details(disk: &mut Disk, disk_details: Option<&str>) {
|
||||
static RE_DETAILS: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(.*?)\r?\nDisk ID\s*:\s+(.*?)\r?\nType\s*:\s+(.*?)\r?\n").unwrap()
|
||||
});
|
||||
static RE_UUID: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"^\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}$").unwrap()
|
||||
});
|
||||
|
||||
// Get details
|
||||
let details: String;
|
||||
if let Some(details_str) = disk_details {
|
||||
details = String::from(details_str);
|
||||
} else {
|
||||
let script = format!("select disk {}\r\ndetail disk", disk.id);
|
||||
details = run_script(&script);
|
||||
};
|
||||
|
||||
// Parse details
|
||||
for (_, [model, part_type, conn_type]) in
|
||||
RE_DETAILS.captures_iter(&details).map(|c| c.extract())
|
||||
{
|
||||
disk.model = String::from(model);
|
||||
disk.conn_type = String::from(conn_type);
|
||||
if RE_UUID.is_match(part_type) {
|
||||
disk.part_type = PartitionTableType::Guid;
|
||||
} else {
|
||||
disk.part_type = PartitionTableType::Legacy;
|
||||
}
|
||||
disk.serial = get_disk_serial_number(&disk.id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_partition_details(
|
||||
disk: &mut Disk,
|
||||
disk_details: Option<&str>,
|
||||
part_details: Option<&str>,
|
||||
) {
|
||||
// TODO: Convert to get_partition_details(disk_id: &str, disk_details [..]){}
|
||||
// Drops need to have mutable access to disk
|
||||
static RE_LIS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"Partition\s+(\d+)\s+\w+\s+(\d+\s+\w+B)").unwrap());
|
||||
|
||||
// List partition
|
||||
let contents: String;
|
||||
if let Some(details) = disk_details {
|
||||
contents = String::from(details);
|
||||
} else {
|
||||
let script = format!("select disk {}\r\nlist partition", disk.id);
|
||||
contents = run_script(&script);
|
||||
};
|
||||
for (_, [number, size]) in RE_LIS.captures_iter(&contents).map(|c| c.extract()) {
|
||||
let part = Partition {
|
||||
id: String::from(number),
|
||||
size: string_to_bytes(size),
|
||||
..Default::default()
|
||||
};
|
||||
// pub fs_type: Option<String>,
|
||||
// pub label: Option<String>,
|
||||
// pub part_type: String,
|
||||
disk.parts.push(part);
|
||||
}
|
||||
|
||||
// Detail parititon
|
||||
let mut script = vec![format!("select disk {}", disk.id)];
|
||||
for part in &disk.parts {
|
||||
if part_details.is_some() {
|
||||
// Currently only used by tests
|
||||
break;
|
||||
}
|
||||
script.push(format!(
|
||||
// Remove/Assign included to ensure all (accessible) volumes have letters
|
||||
"\r\nselect partition {}\r\nremove noerr\r\nassign noerr\r\ndetail partition",
|
||||
part.id
|
||||
));
|
||||
}
|
||||
let part_contents: String;
|
||||
if let Some(details) = part_details {
|
||||
part_contents = String::from(details);
|
||||
} else {
|
||||
part_contents = run_script(script.join("\r\n").as_str());
|
||||
};
|
||||
parse_partition_details(disk, &part_contents);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build_dest_format_script(disk_id: &str, part_type: &PartitionTableType) -> String {
|
||||
let mut script = vec!["select disk {disk_id}", "clean"];
|
||||
match part_type {
|
||||
PartitionTableType::Guid => {
|
||||
script.push("convert gpt");
|
||||
script.push("create partition efi size=260");
|
||||
script.push("format fs=fat32 quick label=ESP");
|
||||
script.push("create partition msr size=16");
|
||||
}
|
||||
PartitionTableType::Legacy => {
|
||||
script.push("create partition primary size=100");
|
||||
script.push("active");
|
||||
script.push("format fs=ntfs quick label=System");
|
||||
}
|
||||
}
|
||||
script.join("\r\n").replace("{disk_id}", disk_id)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build_get_disk_script(disk_nums: Option<Vec<&str>>) -> String {
|
||||
let capacity = DEFAULT_MAX_DISKS * 3 + 1;
|
||||
let script: String;
|
||||
|
||||
// Get disk and partition details
|
||||
if let Some(disks) = disk_nums {
|
||||
// (Slower case)
|
||||
let mut script_parts = Vec::with_capacity(capacity);
|
||||
|
||||
// Get list of disks
|
||||
script_parts.push(String::from("list disk"));
|
||||
|
||||
// Add disks from provided list
|
||||
for num in disks {
|
||||
script_parts.push(format!("select disk {num}"));
|
||||
script_parts.push(String::from("detail disk"));
|
||||
script_parts.push(String::from("list partition"));
|
||||
}
|
||||
|
||||
// Done
|
||||
script = script_parts.join("\n");
|
||||
} else {
|
||||
// (Best case)
|
||||
let mut script_parts = Vec::with_capacity(capacity);
|
||||
|
||||
// Get list of disks
|
||||
script_parts.push("list disk");
|
||||
|
||||
// Assuming first disk number is zero
|
||||
script_parts.push("select disk 0");
|
||||
script_parts.push("detail disk");
|
||||
script_parts.push("list partition");
|
||||
|
||||
// Limit to 8 disks (if there's more the manual "worst" case will be used)
|
||||
let mut i = 0;
|
||||
while i < 8 {
|
||||
script_parts.push("select disk next");
|
||||
script_parts.push("detail disk");
|
||||
script_parts.push("list partition");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Done
|
||||
script = script_parts.join("\n");
|
||||
}
|
||||
|
||||
script
|
||||
}
|
||||
|
||||
pub fn get_disks() -> Vec<Disk> {
|
||||
static RE_DIS_DET: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?s)Disk (\d+) is now the selected disk.*?\r?\n\s*\r?\n(.*)").unwrap()
|
||||
});
|
||||
static RE_DIS_LIS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"Disk\s+(\d+)\s+(\w+)\s+(\d+\s+\w+B)").unwrap());
|
||||
let mut contents: String;
|
||||
let mut output;
|
||||
let mut script: String;
|
||||
|
||||
// Run diskpart and check result
|
||||
script = build_get_disk_script(None);
|
||||
output = run_script_raw(&script);
|
||||
contents = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
if let Some(return_code) = output.status.code() {
|
||||
let disk_nums = parse_disk_numbers(&contents);
|
||||
if return_code != 0 && !disk_nums.is_empty() {
|
||||
// The base assumptions were correct! skipping fallback method
|
||||
//
|
||||
// Since the return_code was not zero, and at least one disk was detected, that
|
||||
// means that the number of disks was less than DEFAULT_MAX_DISKS
|
||||
//
|
||||
// If the number of disks is exactly DEFAULT_MAX_DISKS then we may be omitting some
|
||||
// disk(s) from the list so a manual check is needed
|
||||
// This only adds one additional diskpart call, but avoiding it is preferred
|
||||
//
|
||||
// If zero disks were detected then either the first disk number wasn't zero or no
|
||||
// disks were actually connected
|
||||
// (this could be due to missing drivers but we're leaving that issue to the user)
|
||||
info!("Disk assumptions correct!");
|
||||
} else {
|
||||
// The base assumptions were wrong so we need to check which disk numbers are present
|
||||
warn!("Disk assumptions incorrect, falling back to manual method.");
|
||||
script = build_get_disk_script(Some(disk_nums));
|
||||
output = run_script_raw(&script);
|
||||
contents = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Split Diskpart output contents into sections
|
||||
let mut dp_sections = split_diskpart_disk_output(&contents);
|
||||
|
||||
// Build Disk structs
|
||||
// NOTE: A HashMap is used because it's possible to have gaps in the list of disks
|
||||
// i.e. 0, 1, 3, 4
|
||||
// For instance, this can happen if a drive is disconnected after startup
|
||||
let mut disks_map: HashMap<&str, Disk> = HashMap::with_capacity(DEFAULT_MAX_DISKS);
|
||||
for (_, [number, _status, size]) in RE_DIS_LIS
|
||||
.captures_iter(dp_sections.remove(0)) // This is the "list disk" section
|
||||
.map(|c| c.extract())
|
||||
{
|
||||
disks_map.insert(
|
||||
number,
|
||||
Disk {
|
||||
id: String::from(number),
|
||||
size: string_to_bytes(size),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Add Disk details
|
||||
let mut disks_raw: Vec<Disk> = Vec::with_capacity(DEFAULT_MAX_DISKS);
|
||||
for section in dp_sections {
|
||||
for (_, [id, details]) in RE_DIS_DET.captures_iter(section).map(|c| c.extract()) {
|
||||
if let Some(mut disk) = disks_map.remove(id) {
|
||||
// We remove the disk from the HashMap because we're moving it to the Vec
|
||||
add_disk_details(&mut disk, Some(details));
|
||||
add_partition_details(&mut disk, Some(details), None);
|
||||
disk.generate_descriptions();
|
||||
disks_raw.push(disk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return list of Disks
|
||||
disks_raw
|
||||
}
|
||||
|
||||
pub fn parse_disk_numbers(contents: &str) -> Vec<&str> {
|
||||
//Disk 0 is now the selected disk.
|
||||
//
|
||||
//Red Hat VirtIO SCSI Disk Device
|
||||
//Disk ID: {E9CE8DFA-46B2-43C1-99BB-850C661CEE6B}
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\s+Disk\s+(\d+).*\n.*\n.*\nDisk ID:").unwrap());
|
||||
|
||||
let mut disk_nums = Vec::new();
|
||||
for (_, [number]) in RE.captures_iter(contents).map(|c| c.extract()) {
|
||||
disk_nums.push(number);
|
||||
}
|
||||
disk_nums
|
||||
}
|
||||
|
||||
pub fn parse_partition_details(disk: &mut Disk, contents: &str) {
|
||||
// TODO: Update multiple fields at once?
|
||||
// https://stackoverflow.com/a/52905826
|
||||
static RE_PAR: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"Partition (\d+)\r?\nType\s*: (\S+)(\r?\n.*){5}\s*(Volume.*\r?\n.*\r?\n|There is no volume)(.*)",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
static RE_VOL: Lazy<Regex> = Lazy::new(|| {
|
||||
// Volume ### Ltr Label Fs Type Size Status Info
|
||||
// ---------- --- ----------- ----- ---------- ------- --------- --------
|
||||
// * Volume 1 S ESP FAT32 Partition 100 MB Healthy Hidden
|
||||
Regex::new(r"..Volume (\d.{2}) (.{3}) (.{11}) (.{5})").unwrap()
|
||||
});
|
||||
|
||||
for (part_index, (_, [_part_id, part_type, _, _vol_header, vol_line])) in RE_PAR
|
||||
.captures_iter(contents)
|
||||
.map(|c| c.extract())
|
||||
.enumerate()
|
||||
{
|
||||
let part = &mut disk.parts[part_index];
|
||||
|
||||
// Partition info
|
||||
if !part_type.trim().is_empty() {
|
||||
part.part_type = String::from(part_type.trim());
|
||||
}
|
||||
|
||||
// Volume info
|
||||
for (_, [_id, letter, label, fs_type]) in
|
||||
RE_VOL.captures_iter(vol_line).map(|c| c.extract())
|
||||
{
|
||||
if !label.trim().is_empty() {
|
||||
part.label = Some(String::from(label.trim()));
|
||||
}
|
||||
if !letter.trim().is_empty() {
|
||||
part.letter = Some(String::from(letter.trim()));
|
||||
}
|
||||
if !fs_type.trim().is_empty() {
|
||||
part.fs_type = Some(String::from(fs_type.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_disk_info(disk: &mut Disk) {
|
||||
// TODO: Needs refactor - assuming add_ functions are replaced with get_ variants
|
||||
disk.parts.clear();
|
||||
disk.parts_description.clear();
|
||||
add_disk_details(disk, None);
|
||||
add_partition_details(disk, None, None);
|
||||
disk.generate_descriptions();
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if diskpart fails to run the script or if the script if malformed
|
||||
pub fn run_script_raw(script: &str) -> Output {
|
||||
info!("Running Diskpart: {:?}", &script);
|
||||
let temp_dir = tempdir().expect("Failed to create temp dir");
|
||||
let script_path = temp_dir.path().join("diskpart.script");
|
||||
let mut script_file = File::create(&script_path).expect("Failed to create temp file");
|
||||
script_file
|
||||
.write_all(script.as_bytes())
|
||||
.expect("Failed to write script to disk");
|
||||
let output = Command::new("diskpart")
|
||||
.args(["/s", format!("{}", script_path.display()).as_str()])
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
output
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn run_script(script: &str) -> String {
|
||||
let output = run_script_raw(script);
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
}
|
||||
|
||||
pub fn split_diskpart_disk_output(contents: &str) -> Vec<&str> {
|
||||
// NOTE: A simple split isn't helpful since we want to include the matching lines
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"Disk \d+ is now the selected disk").unwrap());
|
||||
let mut sections = Vec::new();
|
||||
let mut starts: Vec<usize> = vec![0];
|
||||
let mut ends: Vec<usize> = Vec::new();
|
||||
let _: Vec<_> = RE
|
||||
.find_iter(contents)
|
||||
.map(|m| {
|
||||
ends.push(m.start() - 1);
|
||||
starts.push(m.start());
|
||||
})
|
||||
.collect();
|
||||
ends.push(contents.len());
|
||||
let ranges: Vec<(&usize, &usize)> = starts.iter().zip(ends.iter()).collect();
|
||||
for range in ranges {
|
||||
let start = *range.0;
|
||||
let end = *range.1;
|
||||
sections.push(&contents[start..end]);
|
||||
}
|
||||
sections
|
||||
}
|
||||
103
src/system/drivers.rs
Normal file
103
src/system/drivers.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// 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 std::{env, fmt, fs::read_dir, path::PathBuf, process::Command};
|
||||
|
||||
use tracing::info;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Driver {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub inf_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Driver {
|
||||
fn new() -> Driver {
|
||||
Driver {
|
||||
name: String::new(),
|
||||
path: PathBuf::new(),
|
||||
inf_paths: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Driver {
|
||||
fn default() -> Driver {
|
||||
Driver::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Driver {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if a driver fails to load
|
||||
pub fn load(inf_paths: &Vec<PathBuf>) {
|
||||
// Load drivers into live environment
|
||||
for inf in inf_paths {
|
||||
let inf = inf.clone();
|
||||
if let Ok(path) = inf.into_os_string().into_string() {
|
||||
info!("Installing driver: {}", &path);
|
||||
Command::new("drvload")
|
||||
.arg(path)
|
||||
.output()
|
||||
.expect("Failed to load driver");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan() -> Vec<Driver> {
|
||||
let mut drivers: Vec<Driver> = Vec::new();
|
||||
if let Ok(exe_path) = env::current_exe() {
|
||||
let driver_path = exe_path.with_file_name("drivers");
|
||||
if let Ok(dir_entry) = read_dir(driver_path) {
|
||||
for entry in dir_entry.flatten() {
|
||||
if entry.path().is_dir() {
|
||||
if let Ok(name) = entry.file_name().into_string() {
|
||||
drivers.push(Driver {
|
||||
name,
|
||||
path: entry.path(),
|
||||
inf_paths: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drivers.sort();
|
||||
drivers.reverse();
|
||||
for driver in &mut drivers {
|
||||
if &driver.name[..1] == "0" {
|
||||
driver.name = String::from(&driver.name[1..]);
|
||||
}
|
||||
for entry in WalkDir::new(&driver.path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext == "inf" {
|
||||
driver.inf_paths.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drivers
|
||||
}
|
||||
Loading…
Reference in a new issue