Compare commits

...

2 commits

Author SHA1 Message Date
92f7874584
Replace Results component with LogView
Required ignoring KeyUp/KeyDown handling in some cases so a new Mode was
added to accomodate
2025-06-01 17:25:47 -07:00
a9e929585f
Refactor diagnostic groups (yet again) 2025-06-01 16:07:51 -07:00
11 changed files with 430 additions and 362 deletions

View file

@ -13,9 +13,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
// //
use crate::diags;
use core::{ use core::{
action::{Action, DiagResult}, action::Action,
components::{ components::{
Component, Component,
footer::Footer, footer::Footer,
@ -39,8 +38,6 @@ use core::{
tui::{Event, Tui}, tui::{Event, Tui},
}; };
use std::{ use std::{
array,
collections::HashMap,
env, env,
iter::zip, iter::zip,
path::PathBuf, path::PathBuf,
@ -57,13 +54,14 @@ use ratatui::{
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::diags::{DiagGroup, Type as DiagType, get_diag_type, parse_chkdsk};
pub struct App { pub struct App {
// TUI // TUI
action_rx: mpsc::UnboundedReceiver<Action>, action_rx: mpsc::UnboundedReceiver<Action>,
action_tx: mpsc::UnboundedSender<Action>, action_tx: mpsc::UnboundedSender<Action>,
components: Vec<Box<dyn Component>>, components: Vec<Box<dyn Component>>,
config: Config, config: Config,
diag_groups: diags::Groups,
frame_rate: f64, frame_rate: f64,
last_tick_key_events: Vec<KeyEvent>, last_tick_key_events: Vec<KeyEvent>,
should_quit: bool, should_quit: bool,
@ -72,28 +70,19 @@ pub struct App {
// App // App
clone: CloneSettings, clone: CloneSettings,
cur_mode: Mode, cur_mode: Mode,
diag_groups: Arc<Mutex<Vec<DiagGroup>>>,
list: StatefulList<Mode>, list: StatefulList<Mode>,
boot_modes: Vec<SafeMode>, boot_modes: Vec<SafeMode>,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
system32: String, system32: String,
results: Arc<Mutex<HashMap<String, String>>>,
tasks: Tasks, tasks: Tasks,
} }
impl App { impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> { pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel(); let (action_tx, action_rx) = mpsc::unbounded_channel();
let diag_groups_arc = Arc::new(Mutex::new(Vec::new()));
let disk_list_arc = Arc::new(Mutex::new(Vec::new())); let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let mut results_fake = HashMap::new();
results_fake.insert(String::from("1. One"), String::from("Line one,\nline two"));
results_fake.insert(
String::from("2. Two"),
String::from("Another example?\n¯\\_(ツ)_/¯"),
);
let too_many_lines: [usize; 75] = array::from_fn(|i| i + 1);
let too_many_lines: Vec<String> = too_many_lines.iter().map(|x| format!("{x}")).collect();
results_fake.insert(String::from("3. Three"), too_many_lines.join("\n"));
let results_arc = Arc::new(Mutex::new(results_fake));
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(vec![ list.set_items(vec![
@ -114,12 +103,11 @@ impl App {
Box::new(Footer::new()), Box::new(Footer::new()),
Box::new(popup::Popup::new()), Box::new(popup::Popup::new()),
Box::new(crate::components::progress::Progress::new()), Box::new(crate::components::progress::Progress::new()),
Box::new(crate::components::results::Results::new( Box::new(crate::components::logview::LogView::new(
results_arc.clone(), diag_groups_arc.clone(),
)), )),
], ],
config: Config::new()?, config: Config::new()?,
diag_groups: diags::Groups::new(),
frame_rate, frame_rate,
last_tick_key_events: Vec::new(), last_tick_key_events: Vec::new(),
should_quit: false, should_quit: false,
@ -128,11 +116,11 @@ impl App {
// App // App
clone: CloneSettings::new(disk_list_arc), clone: CloneSettings::new(disk_list_arc),
cur_mode: Mode::Home, cur_mode: Mode::Home,
diag_groups: diag_groups_arc,
list, list,
boot_modes: vec![SafeMode::Enable, SafeMode::Disable], boot_modes: vec![SafeMode::Enable, SafeMode::Disable],
system32: String::new(), system32: String::new(),
selections: vec![None, None], selections: vec![None, None],
results: results_arc,
tasks, tasks,
}) })
} }
@ -357,6 +345,9 @@ impl App {
self.action_tx.send(Action::SetMode(next_mode))?; self.action_tx.send(Action::SetMode(next_mode))?;
} }
Action::Process => match self.cur_mode { Action::Process => match self.cur_mode {
Mode::BootDiags => {
self.action_tx.send(Action::SetMode(Mode::LogView))?;
}
Mode::DiagMenu => { Mode::DiagMenu => {
// Use highlighted entry // Use highlighted entry
if let Some(new_mode) = self.list.get_selected() { if let Some(new_mode) = self.list.get_selected() {
@ -366,9 +357,9 @@ impl App {
Mode::Done => { Mode::Done => {
self.action_tx.send(Action::NextScreen)?; self.action_tx.send(Action::NextScreen)?;
} }
Mode::BootDiags | Mode::BootSetup => { Mode::BootSetup => {
let new_mode = self.next_mode(); //let new_mode = self.next_mode();
self.action_tx.send(Action::SetMode(new_mode))?; //self.action_tx.send(Action::SetMode(new_mode))?;
} }
_ => {} _ => {}
}, },
@ -419,7 +410,9 @@ impl App {
self.action_tx.send(Action::DiagLineStart { self.action_tx.send(Action::DiagLineStart {
text: title.clone(), text: title.clone(),
})?; })?;
self.diag_groups.start(title.to_owned()); if let Ok(mut diag_groups) = self.diag_groups.lock() {
diag_groups.push(DiagGroup::new(get_diag_type(&title)));
}
} }
} }
Action::TasksComplete => { Action::TasksComplete => {
@ -448,90 +441,60 @@ impl App {
fn handle_task(&mut self, task: &Task) -> Result<()> { fn handle_task(&mut self, task: &Task) -> Result<()> {
info!("Handling Task: {task:?}"); info!("Handling Task: {task:?}");
match self.cur_mode { if self.cur_mode == Mode::BootScan {
Mode::BootScan => { if let Ok(mut diag_groups) = self.diag_groups.lock() {
if let Some(current_group) = diag_groups.last_mut() {
match current_group.diag_type {
DiagType::CheckDisk => {
if let Some(task_result) = &task.result {
//
parse_chkdsk(current_group, task_result.clone());
}
}
_ => (),
}
}
}
return Ok(());
}
match task.task_type {
TaskType::CommandNoWait(_, _) | TaskType::CommandWait(_, _) | TaskType::Diskpart(_) => {
// Check result
if let Some(result) = &task.result { if let Some(result) = &task.result {
let title = self.diag_groups.current_group();
let passed: bool;
let info: String;
match result { match result {
TaskResult::Error(msg) => { TaskResult::Error(msg) => {
passed = false; self.action_tx
info = msg.to_owned(); .send(Action::Error(format!("{task:?} Failed: {msg}")))?;
} }
TaskResult::Output(stdout, stderr, success) => { TaskResult::Output(stdout, stderr, success) => {
passed = *success; if !success {
if title == "Filesystem" { let msg = if !stdout.is_empty() {
info = parse_chkdsk(stdout); stdout.clone()
} else { } else if !stderr.is_empty() {
let div = if !(stdout.is_empty() || stderr.is_empty()) { stderr.clone()
"\n\n-----------\n\n"
} else { } else {
"" String::from("Unknown Error")
}; };
info = format!("{stdout}{div}{stderr}"); self.action_tx
.send(Action::Error(format!("{task:?} Failed: {msg}")))?;
} }
} }
} }
self.diag_groups.update(title, passed, info);
if passed {
self.action_tx.send(Action::DiagLineUpdate {
result: DiagResult::Pass,
text: String::from("Pass?"),
})?;
} else {
self.action_tx.send(Action::DiagLineUpdate {
result: DiagResult::Fail,
text: String::from("Fail?"),
})?;
};
} }
} }
_ => { TaskType::GroupEnd { ref label } => {
match task.task_type { self.action_tx.send(Action::DiagLineEnd {
TaskType::CommandNoWait(_, _) text: label.clone(),
| TaskType::CommandWait(_, _) })?;
| TaskType::Diskpart(_) => {
// Check result
if let Some(result) = &task.result {
match result {
TaskResult::Error(msg) => {
self.action_tx
.send(Action::Error(format!("{task:?} Failed: {msg}")))?;
}
TaskResult::Output(stdout, stderr, success) => {
if !success {
let msg = if !stdout.is_empty() {
stdout.clone()
} else if !stderr.is_empty() {
stderr.clone()
} else {
String::from("Unknown Error")
};
self.action_tx.send(Action::Error(format!(
"{task:?} Failed: {msg}"
)))?;
}
}
}
}
}
TaskType::GroupEnd { ref label } => {
self.action_tx.send(Action::DiagLineEnd {
text: label.clone(),
})?;
}
_ => {}
}
} }
_ => {}
} }
Ok(()) Ok(())
} }
fn queue_boot_scan_tasks(&mut self) -> Result<()> { fn queue_boot_scan_tasks(&mut self) -> Result<()> {
self.diag_groups.reset(); if let Ok(mut diag_groups) = self.diag_groups.lock() {
if let Ok(mut results) = self.results.lock() { diag_groups.clear();
results.clear();
} }
let disk_list = self.clone.disk_list.lock().unwrap(); let disk_list = self.clone.disk_list.lock().unwrap();
if let Some(disk_index) = self.clone.disk_index_dest { if let Some(disk_index) = self.clone.disk_index_dest {
@ -557,7 +520,7 @@ impl App {
// BCD // BCD
if !letter_boot.is_empty() { if !letter_boot.is_empty() {
self.tasks.add_group( self.tasks.add_group(
"Boot Files", DiagType::BootConfigData.to_string().as_str(),
vec![TaskType::CommandWait( vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\bcdedit.exe", &self.system32)), PathBuf::from(format!("{}\\bcdedit.exe", &self.system32)),
vec![ vec![
@ -578,7 +541,7 @@ impl App {
// Bitlocker // Bitlocker
self.tasks.add_group( self.tasks.add_group(
"Bitlocker", DiagType::Bitlocker.to_string().as_str(),
vec![ vec![
TaskType::CommandWait( TaskType::CommandWait(
PathBuf::from(format!("{}\\manage-bde.exe", &self.system32)), PathBuf::from(format!("{}\\manage-bde.exe", &self.system32)),
@ -596,6 +559,24 @@ impl App {
); );
// Filesystem Health // Filesystem Health
self.tasks.add_group(
DiagType::CheckDisk.to_string().as_str(),
vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\chkdsk.exe", &self.system32)),
vec![format!("{letter_os}:")],
)],
);
// DISM Health
self.tasks.add_group(
DiagType::ComponentStore.to_string().as_str(),
vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\dism.exe", &self.system32)),
vec![format!("{letter_os}:")],
)],
);
// Critical Files/Folders
let paths: Vec<PathBuf> = [ let paths: Vec<PathBuf> = [
// Files/Folders // Files/Folders
"Users", "Users",
@ -609,28 +590,13 @@ impl App {
.map(|s| PathBuf::from(format!("{letter_os}:\\{s}"))) .map(|s| PathBuf::from(format!("{letter_os}:\\{s}")))
.collect(); .collect();
self.tasks.add_group( self.tasks.add_group(
"Filesystem", DiagType::SystemFiles.to_string().as_str(),
vec![ vec![TaskType::TestPaths(paths)],
TaskType::CommandWait(
PathBuf::from(format!("{}\\chkdsk.exe", &self.system32)),
vec![format!("{letter_os}:")],
),
TaskType::TestPaths(paths),
],
);
// DISM Health
self.tasks.add_group(
"System Files",
vec![TaskType::CommandWait(
PathBuf::from(format!("{}\\dism.exe", &self.system32)),
vec![format!("{letter_os}:")],
)],
); );
// Registry // Registry
self.tasks.add_group( self.tasks.add_group(
"Registry", DiagType::Registry.to_string().as_str(),
vec![ vec![
TaskType::CommandWait( TaskType::CommandWait(
PathBuf::from(format!("{}\\reg.exe", &self.system32)), PathBuf::from(format!("{}\\reg.exe", &self.system32)),
@ -762,13 +728,16 @@ fn get_chunks(r: Rect) -> Vec<Rect> {
fn build_footer_string(cur_mode: Mode) -> String { fn build_footer_string(cur_mode: Mode) -> String {
match cur_mode { match cur_mode {
Mode::BootDiags | Mode::LogView => {
String::from("(Enter) to select / (m) for menu / (s) to start over / (q) to quit")
}
Mode::BootScan | Mode::BootSetup | Mode::Home | Mode::ScanDisks => { Mode::BootScan | Mode::BootSetup | Mode::Home | Mode::ScanDisks => {
String::from("(q) to quit") String::from("(q) to quit")
} }
Mode::InstallDrivers | Mode::InjectDrivers | Mode::SetBootMode => { Mode::InstallDrivers | Mode::InjectDrivers | Mode::SetBootMode => {
String::from("(Enter) to select / (q) to quit") String::from("(Enter) to select / (q) to quit")
} }
Mode::BootDiags | Mode::DiagMenu | Mode::SelectParts => { Mode::DiagMenu | Mode::SelectParts => {
String::from("(Enter) to select / (s) to start over / (q) to quit") String::from("(Enter) to select / (s) to start over / (q) to quit")
} }
Mode::Done => String::from("(Enter) to continue / (q) to quit"), Mode::Done => String::from("(Enter) to continue / (q) to quit"),
@ -848,18 +817,25 @@ fn build_left_items(app: &App) -> Action {
} }
} }
} }
Mode::BootDiags => { Mode::BootDiags | Mode::LogView => {
select_type = SelectionType::Loop; 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| { if let Ok(diag_groups) = app.diag_groups.lock() {
info!("BootDiags Group: {:?}", group); let labels: Vec<String> = diag_groups
items.push(if group.passed { .iter()
group.title.clone() .map(|group| {
} else { let label = group.diag_type.to_string();
format!("{} - Issues detected!", group.title) let status = if group.passed {
}); "" // Leave blank if OK
}); } else {
" -- Issue(s) detected"
};
format!("{label}{status}")
})
.collect();
items.extend(labels.into_iter());
}
} }
Mode::BootSetup => { Mode::BootSetup => {
select_type = SelectionType::Loop; select_type = SelectionType::Loop;
@ -970,18 +946,13 @@ fn build_right_items(app: &App) -> Action {
}); });
} }
Mode::BootDiags => { Mode::BootDiags => {
app.diag_groups.get().iter().for_each(|group| { if let Ok(diag_groups) = app.diag_groups.lock() {
let mut lines = Vec::new(); let mut summary: Vec<DVLine> = Vec::new();
group.info.iter().for_each(|text| { diag_groups
text.lines().for_each(|line| { .iter()
lines.push(DVLine { .for_each(|group| summary.extend(group.get_logs_summary().into_iter()));
line_parts: vec![String::from(line)], items.push(summary);
line_colors: vec![Color::Reset], }
});
});
});
items.push(lines);
});
} }
Mode::InjectDrivers | Mode::InstallDrivers => { Mode::InjectDrivers | Mode::InstallDrivers => {
items.push(vec![DVLine { items.push(vec![DVLine {
@ -1051,7 +1022,7 @@ fn build_right_items(app: &App) -> Action {
fn get_mode_strings(mode: Mode) -> (String, String) { fn get_mode_strings(mode: Mode) -> (String, String) {
match mode { match mode {
Mode::BootScan | Mode::BootDiags => ( Mode::BootScan | Mode::BootDiags | Mode::LogView => (
String::from("Boot Diagnostics"), String::from("Boot Diagnostics"),
String::from("Check for common Windows boot issues"), String::from("Check for common Windows boot issues"),
), ),
@ -1070,22 +1041,3 @@ fn get_mode_strings(mode: Mode) -> (String, String) {
_ => panic!("This shouldn't happen"), _ => panic!("This shouldn't happen"),
} }
} }
fn parse_chkdsk(output: &str) -> String {
// Split lines
let lines: Vec<_> = output.split("\r\n").collect();
// Omit progress lines and unhelpful messages
lines
.into_iter()
.filter(|line| {
!(line.contains("\r")
|| line.contains("Class not registered")
|| line.contains("/F parameter")
|| line.contains("Running CHKDSK")
|| line.contains("Total duration:")
|| line.contains("Failed to transfer logged messages"))
})
.collect::<Vec<_>>()
.join("\n")
}

View file

@ -1,2 +1,2 @@
pub mod logview;
pub mod progress; pub mod progress;
pub mod results;

View file

@ -0,0 +1,125 @@
// 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 ratatui::{
Frame,
layout::Rect,
widgets::{Block, Clear, Padding, Paragraph, Wrap},
};
use tokio::sync::mpsc::UnboundedSender;
use core::{
action::Action,
components::{Component, state::StatefulList},
config::Config,
state::Mode,
};
use std::sync::{Arc, Mutex};
use crate::diags::DiagGroup;
#[derive(Default, Debug, Clone)]
pub struct LogView {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
line_index: u16,
list: StatefulList<String>,
mode: Mode,
diag_groups: Arc<Mutex<Vec<DiagGroup>>>,
}
impl LogView {
#[must_use]
pub fn new(diag_groups: Arc<Mutex<Vec<DiagGroup>>>) -> Self {
LogView {
diag_groups,
..Default::default()
}
}
}
impl Component for LogView {
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 => {
if self.mode == Mode::LogView {
if self.line_index > 0 {
self.line_index = self.line_index - 1;
}
} else {
self.list.previous();
}
}
Action::KeyDown => {
if self.mode == Mode::LogView {
self.line_index = self.line_index + 1;
} else {
self.list.next();
}
}
Action::Process => {
if self.mode == Mode::LogView {
if let Some(command_tx) = self.command_tx.clone() {
command_tx.send(Action::SetMode(Mode::BootDiags))?;
}
}
}
Action::SetMode(new_mode) => {
self.line_index = 0;
self.mode = new_mode;
if self.mode == Mode::BootDiags {
self.list.clear_items();
if let Ok(diag_groups) = self.diag_groups.lock() {
let raw_logs = diag_groups
.iter()
.map(|group| group.get_logs_raw())
.collect();
self.list.set_items(raw_logs);
}
}
}
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, rect: Rect) -> Result<()> {
if self.mode != Mode::LogView {
return Ok(());
}
if let Some(log_text) = self.list.get_selected() {
let paragraph = Paragraph::new(log_text)
.wrap(Wrap { trim: true })
.scroll((self.line_index, 0))
.block(Block::bordered().padding(Padding::horizontal(1)));
frame.render_widget(Clear, rect);
frame.render_widget(paragraph, rect);
}
Ok(())
}
}

View file

@ -123,7 +123,7 @@ impl Component for Progress {
DiagResult::Warn => Color::Yellow, DiagResult::Warn => Color::Yellow,
}; };
let text = if line.running || line.text.is_empty() { let text = if line.running || line.text.is_empty() {
String::from("...") String::from("..")
} else { } else {
line.text.clone() line.text.clone()
}; };

View file

@ -1,136 +0,0 @@
// 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 ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Style, Stylize},
widgets::{Block, Clear, Padding, Paragraph, Tabs},
};
use core::{action::Action, components::Component, state::Mode};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
#[derive(Default, Debug, Clone)]
pub struct Results {
// command_tx: Option<UnboundedSender<Action>>,
// config: Config,
key_index: usize,
line_index: u16,
results: Arc<Mutex<HashMap<String, String>>>,
show: bool,
}
impl Results {
#[must_use]
pub fn new(results: Arc<Mutex<HashMap<String, String>>>) -> Self {
Results {
key_index: 0,
line_index: 0,
results,
show: false,
}
}
// 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(())
// }
}
impl Component for Results {
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::KeyUp => {
if self.line_index > 0 {
self.line_index = self.line_index - 1;
}
}
Action::KeyDown => {
self.line_index = self.line_index + 1;
}
Action::KeyLeft => {
if self.key_index > 0 {
self.key_index -= 1;
self.line_index = 0;
}
}
Action::KeyRight => {
if self.key_index < 2 {
self.key_index += 1;
self.line_index = 0;
}
}
Action::SetMode(mode) => {
self.show = mode == Mode::BootDiags;
self.key_index = 0;
self.line_index = 0;
}
_ => {}
};
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, rect: Rect) -> Result<()> {
return Ok(());
if !self.show {
return Ok(());
}
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(rect)
.to_vec();
if let Ok(results_hashmap) = self.results.lock() {
// Labels
let mut tab_labels: Vec<&str> = results_hashmap.keys().map(|k| k.as_str()).collect();
tab_labels.sort();
let hash_key = tab_labels.get(self.key_index).unwrap().to_string();
let tabs = Tabs::new(tab_labels)
.block(Block::bordered())
.style(Style::default().white())
.highlight_style(Style::default().green())
.select(self.key_index)
.divider("")
.padding(" [", "] ");
// Details
let details = if let Some(lines) = results_hashmap.get(&hash_key) {
lines.clone()
} else {
String::from("¯\\_(ツ)_/¯")
};
let paragraph = Paragraph::new(details)
.scroll((self.line_index, 0))
.block(Block::bordered().padding(Padding::horizontal(1)));
// Render
frame.render_widget(Clear, rect);
frame.render_widget(tabs, areas[0]);
frame.render_widget(paragraph, areas[1]);
}
Ok(())
}
}

View file

@ -13,78 +13,169 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
use std::collections::HashMap; use core::{line::DVLine, tasks::TaskResult};
use std::{fmt, thread::sleep, time::Duration};
use tracing::warn; use ratatui::style::Color;
use tracing::{info, warn};
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub struct Groups { pub enum Type {
items: HashMap<String, Line>, Bitlocker,
order: Vec<String>, BootConfigData,
CheckDisk,
ComponentStore,
Registry,
SystemFiles,
Unknown,
} }
impl Groups { impl fmt::Display for Type {
pub fn new() -> Self { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Groups { match self {
items: HashMap::new(), Type::Bitlocker => write!(f, "Bitlocker"),
order: Vec::new(), Type::BootConfigData => write!(f, "Boot Files"),
} Type::CheckDisk => write!(f, "CHKDSK"),
} Type::ComponentStore => write!(f, "DISM ScanHealth"),
Type::Registry => write!(f, "Registry"),
pub fn current_group(&self) -> String { Type::SystemFiles => write!(f, "System Files"),
match self.order.last() { Type::Unknown => write!(f, "Unknown Type"),
Some(label) => label.clone(),
None => String::from("No current group"),
}
}
pub fn get(&self) -> Vec<&Line> {
let mut lines = Vec::new();
self.order.iter().for_each(|key| {
if let Some(line) = self.items.get(key) {
lines.push(line);
}
});
lines
}
pub fn reset(&mut self) {
self.items.clear();
self.order.clear();
}
pub fn start(&mut self, title: String) {
self.order.push(title.clone());
self.items.insert(
title.clone(),
Line {
title,
passed: true,
info: Vec::new(),
},
);
}
pub fn update(&mut self, title: String, passed: bool, info: String) {
if let Some(line) = self.items.get_mut(&title) {
line.update(passed, info);
} else {
warn!("WARNING/DELETEME - This shouldn't happen?!");
self.start(title);
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Line { pub struct Log {
pub title: String, pub label: String,
pub passed: bool, pub summary: Vec<DVLine>,
pub info: Vec<String>, pub raw: String,
} }
impl Line { #[derive(Clone, Debug)]
pub fn update(&mut self, passed: bool, info: String) { pub struct DiagGroup {
self.passed &= passed; // We fail if any tests in this group fail pub complete: bool,
self.info.push(info); pub diag_type: Type,
pub passed: bool,
pub logs: Vec<Log>,
pub result: String,
}
impl DiagGroup {
pub fn new(diag_type: Type) -> Self {
DiagGroup {
complete: false,
diag_type,
passed: true,
logs: Vec::new(),
result: String::new(),
}
}
pub fn get_logs_raw(&self) -> String {
let raw_logs: Vec<String> = self
.logs
.iter()
.map(|log| format!("-- {} --\n{}", &log.label, &log.raw))
.collect();
raw_logs.join("\n\n\n")
}
pub fn get_logs_summary(&self) -> Vec<DVLine> {
let mut summaries: Vec<DVLine> = Vec::new();
self.logs
.iter()
.for_each(|log| summaries.extend(log.summary.clone().into_iter()));
summaries
} }
} }
pub fn get_diag_type(label: &str) -> Type {
info!("Getting Diag type for {label}");
match label {
"Bitlocker" => Type::Bitlocker,
"Boot Files" => Type::BootConfigData,
"DISM ScanHealth" => Type::ComponentStore,
"CHKDSK" => Type::CheckDisk,
"Registry" => Type::Registry,
"System Files" => Type::SystemFiles,
_ => {
warn!("Failed to determine type");
Type::Unknown
}
}
}
pub fn parse_chkdsk(diag_group: &mut DiagGroup, task_result: TaskResult) {
if !cfg!(windows) {
sleep(Duration::from_millis(500));
return ();
}
match task_result {
TaskResult::Error(err) => {
diag_group.passed = false;
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("CHKDSK"),
summary: vec![DVLine {
line_parts: vec![String::from("CHKDSK: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: err,
});
}
TaskResult::Output(stdout, stderr, _) => {
if stdout.contains("Windows has scanned the file system and found no problems.") {
diag_group.passed &= true;
diag_group.result = String::from("OK");
diag_group.logs.push(Log {
label: String::from("CHKDSK"),
summary: vec![DVLine {
line_parts: vec![String::from("CHKDSK: "), String::from("OK")],
line_colors: vec![Color::Reset, Color::Green],
}],
raw: format!("{stdout}\n\n-------\n\n{stderr}"),
});
} else {
diag_group.passed = false;
diag_group.result = String::from("Error");
diag_group.logs.push(Log {
label: String::from("CHKDSK"),
summary: vec![DVLine {
line_parts: vec![String::from("CHKDSK: "), String::from("Error")],
line_colors: vec![Color::Reset, Color::Red],
}],
raw: format!("{stdout}\n\n-------\n\n{stderr}"),
});
}
}
}
// let mut summary = Vec::new();
// let raw = if stderr.is_empty() {
// stdout.to_string()
// } else {
// format!("{stdout}\n\n --stderr-- \n\n{stderr}")
// };
// // TODO: Implement actual logic for result
// Log {
// label: String::from("CHKDSK"),
// raw,
// summary,
// }
// // Split lines
// let lines: Vec<_> = output.split("\r\n").collect();
//
// // Omit progress lines and unhelpful messages
// lines
// .into_iter()
// .filter(|line| {
// !(line.contains("\r")
// || line.contains("Class not registered")
// || line.contains("/F parameter")
// || line.contains("Running CHKDSK")
// || line.contains("Total duration:")
// || line.contains("Failed to transfer logged messages"))
// })
// .collect::<Vec<_>>()
// .join("\n")
}

View file

@ -109,8 +109,6 @@
"<Enter>": "Process", "<Enter>": "Process",
"<Up>": "KeyUp", "<Up>": "KeyUp",
"<Down>": "KeyDown", "<Down>": "KeyDown",
"<Left>": "KeyLeft",
"<Right>": "KeyRight",
"<r>": "BootScan", "<r>": "BootScan",
"<s>": "ScanDisks", "<s>": "ScanDisks",
"<q>": "Quit", "<q>": "Quit",
@ -133,6 +131,15 @@
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend"
}, },
"LogView": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"InjectDrivers": { "InjectDrivers": {
"<Enter>": "Process", "<Enter>": "Process",
"<Up>": "KeyUp", "<Up>": "KeyUp",

View file

@ -23,7 +23,7 @@ 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, state::Mode};
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum SelectionType { pub enum SelectionType {
@ -39,6 +39,7 @@ pub struct Left {
config: Config, config: Config,
labels: Vec<String>, labels: Vec<String>,
list: StatefulList<String>, list: StatefulList<String>,
mode: Mode,
select_type: SelectionType, select_type: SelectionType,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>, selections_saved: Vec<Option<usize>>,
@ -82,9 +83,21 @@ impl Component for Left {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::Highlight(index) => self.set_highlight(index), Action::Highlight(index) => self.set_highlight(index),
Action::KeyUp => self.list.previous(), Action::KeyUp => {
Action::KeyDown => self.list.next(), if self.mode != Mode::LogView {
self.list.previous();
}
}
Action::KeyDown => {
if self.mode != Mode::LogView {
self.list.next();
}
}
Action::Process => { Action::Process => {
if self.mode == Mode::LogView {
// Avoid updating selections/etc while log is open
return Ok(None);
}
if self.select_type == SelectionType::Loop { 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() {
@ -123,7 +136,8 @@ impl Component for Left {
self.selections_saved[0] = one; self.selections_saved[0] = one;
self.selections_saved[1] = two; self.selections_saved[1] = two;
} }
Action::SetMode(_) => { Action::SetMode(new_mode) => {
self.mode = new_mode;
self.selections[0] = None; self.selections[0] = None;
self.selections[1] = None; self.selections[1] = None;
} }

View file

@ -22,7 +22,7 @@ use ratatui::{
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, line::DVLine}; use crate::{action::Action, config::Config, line::DVLine, state::Mode};
#[derive(Default)] #[derive(Default)]
pub struct Right { pub struct Right {
@ -31,6 +31,7 @@ pub struct Right {
list_header: Vec<DVLine>, list_header: Vec<DVLine>,
list_labels: Vec<Vec<DVLine>>, list_labels: Vec<Vec<DVLine>>,
list: StatefulList<Vec<DVLine>>, list: StatefulList<Vec<DVLine>>,
mode: Mode,
selections: Vec<Option<usize>>, selections: Vec<Option<usize>>,
selections_saved: Vec<Option<usize>>, selections_saved: Vec<Option<usize>>,
title: String, title: String,
@ -93,8 +94,16 @@ impl Component for Right {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::Highlight(index) => self.set_highlight(index), Action::Highlight(index) => self.set_highlight(index),
Action::KeyUp => self.list.previous(), Action::KeyUp => {
Action::KeyDown => self.list.next(), if self.mode != Mode::LogView {
self.list.previous();
}
}
Action::KeyDown => {
if self.mode != Mode::LogView {
self.list.next();
}
}
Action::Select(one, two) => { Action::Select(one, two) => {
self.selections[0] = one; self.selections[0] = one;
self.selections[1] = two; self.selections[1] = two;
@ -109,7 +118,8 @@ impl Component for Right {
self.selections_saved[0] = one; self.selections_saved[0] = one;
self.selections_saved[1] = two; self.selections_saved[1] = two;
} }
Action::SetMode(_) => { Action::SetMode(new_mode) => {
self.mode = new_mode;
self.selections[0] = None; self.selections[0] = None;
self.selections[1] = None; self.selections[1] = None;
self.selections_saved[0] = None; self.selections_saved[0] = None;

View file

@ -35,6 +35,7 @@ pub enum Mode {
BootDiags, BootDiags,
BootScan, BootScan,
BootSetup, BootSetup,
LogView,
InjectDrivers, InjectDrivers,
SetBootMode, SetBootMode,
// Clone // Clone

View file

@ -126,6 +126,7 @@ impl App {
| Mode::BootSetup | Mode::BootSetup
| Mode::DiagMenu | Mode::DiagMenu
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
} }
@ -149,6 +150,7 @@ impl App {
| Mode::BootSetup | Mode::BootSetup
| Mode::DiagMenu | Mode::DiagMenu
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
}; };
@ -627,6 +629,7 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::BootSetup | Mode::BootSetup
| Mode::DiagMenu | Mode::DiagMenu
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
} }
@ -698,6 +701,7 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
| Mode::BootSetup | Mode::BootSetup
| Mode::DiagMenu | Mode::DiagMenu
| Mode::InjectDrivers | Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
}; };