Compare commits

...

3 commits

Author SHA1 Message Date
b9ade066e7
Show current mode in title row
This may be later isolated to debug builds
2025-05-05 22:31:39 -07:00
713dc8c2de
Use Enum for left selection type 2025-05-05 22:06:49 -07:00
2aede33db6
Improve handling of pe tool toml files 2025-05-05 21:15:50 -07:00
6 changed files with 134 additions and 64 deletions

View file

@ -17,8 +17,14 @@ use crate::diags;
use core::{ use core::{
action::Action, action::Action,
components::{ components::{
Component, footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, Component,
state::StatefulList, title::Title, footer::Footer,
fps::FpsCounter,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
}, },
config::Config, config::Config,
line::{DVLine, get_disk_description_right, get_part_description}, line::{DVLine, get_disk_description_right, get_part_description},
@ -740,15 +746,15 @@ fn build_footer_string(cur_mode: Mode) -> String {
fn build_left_items(app: &App) -> Action { fn build_left_items(app: &App) -> Action {
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels = vec![String::new(), String::new()]; let mut labels = vec![String::new(), String::new()];
let select_num: usize; let select_type: SelectionType;
let title: String; let title: String;
match app.cur_mode { match app.cur_mode {
Mode::Home => { Mode::Home => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Home"); title = String::from("Home");
} }
Mode::DiagMenu => { Mode::DiagMenu => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Troubleshooting"); title = String::from("Troubleshooting");
app.list.items.iter().for_each(|mode| { app.list.items.iter().for_each(|mode| {
let (name, _) = get_mode_strings(*mode); let (name, _) = get_mode_strings(*mode);
@ -756,7 +762,7 @@ fn build_left_items(app: &App) -> Action {
}); });
} }
Mode::InstallDrivers => { Mode::InstallDrivers => {
select_num = 1; select_type = SelectionType::One;
title = String::from("Install Drivers"); title = String::from("Install Drivers");
app.clone app.clone
.driver_list .driver_list
@ -764,7 +770,7 @@ fn build_left_items(app: &App) -> Action {
.for_each(|driver| items.push(driver.to_string())); .for_each(|driver| items.push(driver.to_string()));
} }
Mode::InjectDrivers => { Mode::InjectDrivers => {
select_num = 1; select_type = SelectionType::One;
title = String::from("Select Drivers"); title = String::from("Select Drivers");
app.clone app.clone
.driver_list .driver_list
@ -772,7 +778,7 @@ fn build_left_items(app: &App) -> Action {
.for_each(|driver| items.push(driver.to_string())); .for_each(|driver| items.push(driver.to_string()));
} }
Mode::SelectDisks => { Mode::SelectDisks => {
select_num = 1; select_type = SelectionType::One;
title = String::from("Select Disk"); title = String::from("Select Disk");
let disk_list = app.clone.disk_list.lock().unwrap(); let disk_list = app.clone.disk_list.lock().unwrap();
disk_list disk_list
@ -780,11 +786,11 @@ fn build_left_items(app: &App) -> Action {
.for_each(|disk| items.push(disk.description.to_string())); .for_each(|disk| items.push(disk.description.to_string()));
} }
Mode::BootScan | Mode::ScanDisks => { Mode::BootScan | Mode::ScanDisks => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Processing"); title = String::from("Processing");
} }
Mode::SelectParts => { Mode::SelectParts => {
select_num = 2; select_type = SelectionType::Two;
title = String::from("Select Boot and OS Partitions"); title = String::from("Select Boot and OS Partitions");
labels[0] = String::from("boot"); labels[0] = String::from("boot");
labels[1] = String::from("os"); labels[1] = String::from("os");
@ -798,7 +804,7 @@ fn build_left_items(app: &App) -> Action {
} }
} }
Mode::BootDiags => { Mode::BootDiags => {
select_num = 0; select_type = SelectionType::Loop;
let (new_title, _) = get_mode_strings(app.cur_mode); let (new_title, _) = get_mode_strings(app.cur_mode);
title = new_title; title = new_title;
app.diag_groups.get().iter().for_each(|group| { app.diag_groups.get().iter().for_each(|group| {
@ -810,19 +816,19 @@ fn build_left_items(app: &App) -> Action {
}); });
} }
Mode::BootSetup => { Mode::BootSetup => {
select_num = 0; select_type = SelectionType::Loop;
let (new_title, _) = get_mode_strings(app.cur_mode); let (new_title, _) = get_mode_strings(app.cur_mode);
title = new_title; title = new_title;
} }
Mode::SetBootMode => { Mode::SetBootMode => {
select_num = 1; select_type = SelectionType::One;
title = String::from("Set Boot Mode"); title = String::from("Set Boot Mode");
app.boot_modes.iter().for_each(|entry| { app.boot_modes.iter().for_each(|entry| {
items.push(format!("{:?} Safe Mode", entry)); items.push(format!("{:?} Safe Mode", entry));
}); });
} }
Mode::Done | Mode::Failed => { Mode::Done | Mode::Failed => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Done"); title = String::from("Done");
} }
// Invalid states // Invalid states
@ -835,7 +841,7 @@ fn build_left_items(app: &App) -> Action {
panic!("This shouldn't happen?") panic!("This shouldn't happen?")
} }
}; };
Action::UpdateLeft(title, labels, items, select_num) Action::UpdateLeft(title, labels, items, select_type)
} }
fn build_right_items(app: &App) -> Action { fn build_right_items(app: &App) -> Action {

View file

@ -16,7 +16,12 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
use crate::{components::popup::Type, line::DVLine, state::Mode, system::disk::Disk}; use crate::{
components::{left::SelectionType, popup::Type as PopupType},
line::DVLine,
state::Mode,
system::disk::Disk,
};
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum Action { pub enum Action {
@ -32,11 +37,7 @@ pub enum Action {
TasksComplete, TasksComplete,
UpdateDiskList(Vec<Disk>), UpdateDiskList(Vec<Disk>),
UpdateFooter(String), UpdateFooter(String),
UpdateLeft(String, Vec<String>, Vec<String>, usize), // (title, labels, items, select_num) UpdateLeft(String, Vec<String>, Vec<String>, SelectionType), // (title, labels, items, select_type)
// NOTE: select_num should be set to 0, 1, or 2.
// 0: For repeating selections
// 1: For a single choice
// 2: For two selections (obviously)
UpdateRight(Vec<Vec<DVLine>>, usize, Vec<Vec<DVLine>>), // (labels, start_index, items) - items before start are always shown UpdateRight(Vec<Vec<DVLine>>, usize, Vec<Vec<DVLine>>), // (labels, start_index, items) - items before start are always shown
// App (PE-Menu) // App (PE-Menu)
OpenTerminal, OpenTerminal,
@ -44,7 +45,7 @@ pub enum Action {
Shutdown, Shutdown,
// Screens // Screens
DismissPopup, DismissPopup,
DisplayPopup(Type, String), DisplayPopup(PopupType, String),
NextScreen, NextScreen,
PrevScreen, PrevScreen,
SetMode(Mode), SetMode(Mode),

View file

@ -30,6 +30,8 @@ use crate::action::Action;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct FpsCounter { pub struct FpsCounter {
mode: String,
last_tick_update: Instant, last_tick_update: Instant,
tick_count: u32, tick_count: u32,
ticks_per_second: f64, ticks_per_second: f64,
@ -49,6 +51,7 @@ impl FpsCounter {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
mode: String::from(""),
last_tick_update: Instant::now(), last_tick_update: Instant::now(),
tick_count: 0, tick_count: 0,
ticks_per_second: 0.0, ticks_per_second: 0.0,
@ -88,29 +91,44 @@ impl FpsCounter {
impl Component for FpsCounter { impl Component for FpsCounter {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::Tick => self.app_tick()?, Action::SetMode(mode) => {
self.mode = format!("{:?}", mode);
}
Action::Render => self.render_tick()?, Action::Render => self.render_tick()?,
Action::Tick => self.app_tick()?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)
} }
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [column, _] =
Layout::horizontal([Constraint::Min(1), Constraint::Length(2)]).areas(area);
let [_, row, _] = Layout::vertical([ let [_, row, _] = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
Constraint::Length(1), Constraint::Length(1),
Constraint::Min(0), Constraint::Min(0),
]) ])
.areas(column); .areas(area);
let [_, left, right, _] = Layout::horizontal([
Constraint::Length(2),
Constraint::Min(1),
Constraint::Min(1),
Constraint::Length(2),
])
.areas(row);
// Debug
let span = Span::styled(format!("Mode: {}", &self.mode), Style::new().dim());
let paragraph = Paragraph::new(span).left_aligned();
frame.render_widget(paragraph, left);
// FPS
let message = format!( let message = format!(
"{:.2} ticks/sec, {:.2} FPS", "{:.2} ticks/sec, {:.2} FPS",
self.ticks_per_second, self.frames_per_second self.ticks_per_second, self.frames_per_second
); );
let span = Span::styled(message, Style::new().dim()); let span = Span::styled(message, Style::new().dim());
let paragraph = Paragraph::new(span).right_aligned(); let paragraph = Paragraph::new(span).right_aligned();
frame.render_widget(paragraph, row); frame.render_widget(paragraph, right);
Ok(()) Ok(())
} }
} }

View file

@ -19,18 +19,27 @@ use ratatui::{
prelude::*, prelude::*,
widgets::{Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph}, widgets::{Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph},
}; };
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use super::{Component, state::StatefulList}; use super::{Component, state::StatefulList};
use crate::{action::Action, config::Config}; use crate::{action::Action, config::Config};
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum SelectionType {
#[default]
Loop,
One,
Two,
}
#[derive(Default)] #[derive(Default)]
pub struct Left { pub struct Left {
command_tx: Option<UnboundedSender<Action>>, command_tx: Option<UnboundedSender<Action>>,
config: Config, config: Config,
labels: Vec<String>, labels: Vec<String>,
list: StatefulList<String>, list: StatefulList<String>,
select_num: usize, select_type: SelectionType,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>, selections_saved: Vec<Option<usize>>,
title_text: String, title_text: String,
@ -40,8 +49,8 @@ impl Left {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
select_num: 0,
labels: vec![String::from("one"), String::from("two")], labels: vec![String::from("one"), String::from("two")],
select_type: SelectionType::Loop,
selections: vec![None, None], selections: vec![None, None],
selections_saved: vec![None, None], selections_saved: vec![None, None],
title_text: String::from("Home"), title_text: String::from("Home"),
@ -76,14 +85,14 @@ impl Component for Left {
Action::KeyUp => self.list.previous(), Action::KeyUp => self.list.previous(),
Action::KeyDown => self.list.next(), Action::KeyDown => self.list.next(),
Action::Process => { Action::Process => {
if self.select_num == 0 { if self.select_type == SelectionType::Loop {
// Selections aren't being used so this is a no-op // Selections aren't being used so this is a no-op
} else if let Some(command_tx) = self.command_tx.clone() { } else if let Some(command_tx) = self.command_tx.clone() {
match (self.selections[0], self.selections[1]) { match (self.selections[0], self.selections[1]) {
(None, None) => { (None, None) => {
// Making first selection // Making first selection
command_tx.send(Action::Select(self.list.selected(), None))?; command_tx.send(Action::Select(self.list.selected(), None))?;
if self.select_num == 1 { if self.select_type == SelectionType::One {
// Confirm selection // Confirm selection
command_tx.send(Action::NextScreen)?; command_tx.send(Action::NextScreen)?;
} }
@ -118,14 +127,14 @@ impl Component for Left {
self.selections[0] = None; self.selections[0] = None;
self.selections[1] = None; self.selections[1] = None;
} }
Action::UpdateLeft(title, labels, items, select_num) => { Action::UpdateLeft(title, labels, items, select_type) => {
self.title_text = title; self.title_text = title;
self.labels = labels self.labels = labels
.iter() .iter()
.map(|label| format!(" ~{}~", label.to_lowercase())) .map(|label| format!(" ~{}~", label.to_lowercase()))
.collect(); .collect();
self.list.set_items(items); self.list.set_items(items);
self.select_num = select_num; self.select_type = select_type;
} }
_ => {} _ => {}
} }

View file

@ -16,8 +16,14 @@
use core::{ use core::{
action::Action, action::Action,
components::{ components::{
Component, footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, Component,
state::StatefulList, title::Title, footer::Footer,
fps::FpsCounter,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
}, },
config::Config, config::Config,
line::{DVLine, get_disk_description_right, get_part_description}, line::{DVLine, get_disk_description_right, get_part_description},
@ -627,17 +633,17 @@ fn build_footer_string(cur_mode: Mode) -> String {
} }
fn build_left_items(app: &App, cur_mode: Mode) -> Action { fn build_left_items(app: &App, cur_mode: Mode) -> Action {
let select_num: usize; let select_type: SelectionType;
let title: String; let title: String;
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels: Vec<String> = Vec::new(); let mut labels: Vec<String> = Vec::new();
match cur_mode { match cur_mode {
Mode::Home => { Mode::Home => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Home"); title = String::from("Home");
} }
Mode::InstallDrivers => { Mode::InstallDrivers => {
select_num = 1; select_type = SelectionType::One;
title = String::from("Install Drivers"); title = String::from("Install Drivers");
app.clone app.clone
.driver_list .driver_list
@ -645,7 +651,7 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
.for_each(|driver| items.push(driver.to_string())); .for_each(|driver| items.push(driver.to_string()));
} }
Mode::SelectDisks => { Mode::SelectDisks => {
select_num = 2; select_type = SelectionType::Two;
title = String::from("Select Source and Destination Disks"); title = String::from("Select Source and Destination Disks");
labels.push(String::from("source")); labels.push(String::from("source"));
labels.push(String::from("dest")); labels.push(String::from("dest"));
@ -655,21 +661,21 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
.for_each(|disk| items.push(disk.description.to_string())); .for_each(|disk| items.push(disk.description.to_string()));
} }
Mode::SelectTableType => { Mode::SelectTableType => {
select_num = 1; select_type = SelectionType::One;
title = String::from("Select Partition Table Type"); title = String::from("Select Partition Table Type");
items.push(format!("{}", PartitionTableType::Guid)); items.push(format!("{}", PartitionTableType::Guid));
items.push(format!("{}", PartitionTableType::Legacy)); items.push(format!("{}", PartitionTableType::Legacy));
} }
Mode::Confirm => { Mode::Confirm => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Confirm Selections"); title = String::from("Confirm Selections");
} }
Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => { Mode::ScanDisks | Mode::PreClone | Mode::Clone | Mode::PostClone => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Processing"); title = String::from("Processing");
} }
Mode::SelectParts => { Mode::SelectParts => {
select_num = 2; select_type = SelectionType::Two;
title = String::from("Select Boot and OS Partitions"); title = String::from("Select Boot and OS Partitions");
labels.push(String::from("boot")); labels.push(String::from("boot"));
labels.push(String::from("os")); labels.push(String::from("os"));
@ -683,7 +689,7 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
} }
} }
Mode::Done | Mode::Failed => { Mode::Done | Mode::Failed => {
select_num = 0; select_type = SelectionType::Loop;
title = String::from("Done"); title = String::from("Done");
} }
// Invalid states // Invalid states
@ -695,7 +701,7 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
| Mode::PEMenu | Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
}; };
Action::UpdateLeft(title, labels, items, select_num) Action::UpdateLeft(title, labels, items, select_type)
} }
fn build_right_items(app: &App, cur_mode: Mode) -> Action { fn build_right_items(app: &App, cur_mode: Mode) -> Action {

View file

@ -16,8 +16,14 @@
use core::{ use core::{
action::Action, action::Action,
components::{ components::{
Component, footer::Footer, fps::FpsCounter, left::Left, popup, right::Right, Component,
state::StatefulList, title::Title, footer::Footer,
fps::FpsCounter,
left::{Left, SelectionType},
popup,
right::Right,
state::StatefulList,
title::Title,
}, },
config::Config, config::Config,
line::DVLine, line::DVLine,
@ -76,7 +82,7 @@ impl App {
let disk_list_arc = Arc::new(Mutex::new(Vec::new())); let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
let mut list = StatefulList::default(); let mut list = StatefulList::default();
list.set_items(load_tools()); list.set_items(load_tools(action_tx.clone()));
Ok(Self { Ok(Self {
// TUI // TUI
action_rx, action_rx,
@ -411,7 +417,7 @@ fn build_left_items(app: &App) -> Action {
}) })
// ─ // ─
.collect(); .collect();
Action::UpdateLeft(title, labels, items, 0) Action::UpdateLeft(title, labels, items, SelectionType::Loop)
} }
fn build_right_items(app: &App) -> Action { fn build_right_items(app: &App) -> Action {
@ -441,21 +447,45 @@ fn build_right_items(app: &App) -> Action {
Action::UpdateRight(labels, start_index, items) Action::UpdateRight(labels, start_index, items)
} }
pub fn load_tools() -> Vec<Tool> { pub fn load_tools(action_tx: mpsc::UnboundedSender<Action>) -> Vec<Tool> {
let mut entries: Vec<PathBuf>;
let mut tools: Vec<Tool> = Vec::new();
let exe_path = env::current_exe().expect("Failed to find main executable"); let exe_path = env::current_exe().expect("Failed to find main executable");
let tool_config_path = exe_path.parent().unwrap().join("menu_entries"); let tool_config_path = exe_path.parent().unwrap().join("menu_entries");
let mut entries: Vec<PathBuf> = std::fs::read_dir(tool_config_path) if let Ok(read_dir) = std::fs::read_dir(tool_config_path) {
.expect("Failed to find any tool configs") entries = read_dir
.map(|res| res.map(|e| e.path())) .map(|res| res.map(|e| e.path()))
.filter_map(Result::ok) .filter_map(Result::ok)
.collect(); .collect();
entries.sort(); entries.sort();
entries } else {
.iter() action_tx
.map(|entry| { .send(Action::Error(String::from(
let contents = fs::read_to_string(entry).expect("Failed to read tool config file"); "Failed to find any tool configs",
let tool: Tool = toml::from_str(&contents).expect("Failed to parse tool config file"); )))
tool .unwrap();
}) entries = Vec::new();
.collect() }
entries.iter().for_each(|entry| {
if let Ok(toml_str) = fs::read_to_string(entry) {
if let Ok(tool) = toml::from_str(&toml_str) {
tools.push(tool);
} else {
action_tx
.send(Action::Error(format!(
"Failed to parse tool config file: {:?}",
&entry,
)))
.unwrap();
}
} else {
action_tx
.send(Action::Error(format!(
"Failed to read tool config file: {:?}",
&entry,
)))
.unwrap();
}
});
tools
} }