Compare commits

..

No commits in common. "dev" and "win-installer" have entirely different histories.

18 changed files with 793 additions and 2759 deletions

5
.gitignore vendored
View file

@ -1,4 +1,3 @@
.data/*.log
/*/target
/pw
/target /target
/*/target
.data/*.log

2833
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -659,7 +659,6 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::PostClone | Mode::PostClone
| Mode::ScanWinSources | Mode::ScanWinSources
| Mode::SelectTableType | Mode::SelectTableType
| Mode::SelectTicket
| Mode::SelectWinSource | Mode::SelectWinSource
| Mode::SelectWinImage | Mode::SelectWinImage
| Mode::SetUserName => { | Mode::SetUserName => {
@ -777,7 +776,6 @@ fn build_left_items(app: &App) -> Action {
| Mode::Clone | Mode::Clone
| Mode::PostClone | Mode::PostClone
| Mode::ScanWinSources | Mode::ScanWinSources
| Mode::SelectTicket
| Mode::SelectWinSource | Mode::SelectWinSource
| Mode::SelectWinImage | Mode::SelectWinImage
| Mode::SetUserName => { | Mode::SetUserName => {

View file

@ -45,12 +45,6 @@
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend"
}, },
"SelectTicket": {
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"Confirm": { "Confirm": {
"<b>": "PrevScreen", "<b>": "PrevScreen",
"<Enter>": "Process", "<Enter>": "Process",
@ -233,6 +227,5 @@
"network_server": "SERVER", "network_server": "SERVER",
"network_share": "SHARE", "network_share": "SHARE",
"network_user": "USER", "network_user": "USER",
"network_pass": "PASS", "network_pass": "PASS"
"ost_server": "ost.1201.com"
} }

View file

@ -18,7 +18,7 @@ name = "core"
authors = ["2Shirt <2xShirt@gmail.com>"] authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024" edition = "2024"
license = "GPL" license = "GPL"
version = "0.3.0" version = "0.2.0"
[dependencies] [dependencies]
better-panic = "0.3.0" better-panic = "0.3.0"
@ -48,7 +48,6 @@ regex = "1.11.1"
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.125" serde_json = "1.0.125"
signal-hook = "0.3.17" signal-hook = "0.3.17"
sqlx = { version = "0.8", features = ["mysql", "runtime-tokio"] }
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
tempfile = "3.13.0" tempfile = "3.13.0"
@ -57,12 +56,8 @@ tokio-util = "0.7.11"
tracing = "0.1.41" tracing = "0.1.41"
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tui-input = "*"
walkdir = "2.5.0" walkdir = "2.5.0"
[dependencies.ost-db-connector]
git = "ssh://git@git.1201.com:2222/1201/ost-db-connector.git"
[build-dependencies] [build-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] } vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

View file

@ -13,7 +13,6 @@
// 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 ost_db_connector::ResponseColor;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
@ -60,12 +59,6 @@ pub enum Action {
FindWimBackups, FindWimBackups,
FindWimNetwork, FindWimNetwork,
SetUserName(String), SetUserName(String),
// osTicket
PostResponse { color: ResponseColor, text: String },
ResetTicket,
SelectTicket(u16),
UserNo,
UserYes,
// Screens // Screens
DismissPopup, DismissPopup,
DisplayPopup(PopupType, String), DisplayPopup(PopupType, String),

View file

@ -28,7 +28,6 @@ pub mod footer;
pub mod left; pub mod left;
pub mod popup; pub mod popup;
pub mod right; pub mod right;
pub mod select_ticket;
pub mod state; pub mod state;
pub mod title; pub mod title;

View file

@ -14,7 +14,6 @@
// 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 color_eyre::Result; use color_eyre::Result;
use crossterm::event::KeyCode;
use rand::random; use rand::random;
use ratatui::{ use ratatui::{
prelude::*, prelude::*,
@ -26,14 +25,13 @@ use tokio::sync::mpsc::UnboundedSender;
use tracing::info; use tracing::info;
use super::Component; use super::Component;
use crate::{action::Action, config::Config, tui::Event}; use crate::{action::Action, config::Config};
#[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum Type { pub enum Type {
#[default] #[default]
Info, Info,
Error, Error,
Question,
Success, Success,
} }
@ -41,7 +39,6 @@ pub enum Type {
pub struct Popup { pub struct Popup {
command_tx: Option<UnboundedSender<Action>>, command_tx: Option<UnboundedSender<Action>>,
config: Config, config: Config,
get_input: bool,
popup_type: Type, popup_type: Type,
popup_text: String, popup_text: String,
} }
@ -49,10 +46,7 @@ pub struct Popup {
impl Popup { impl Popup {
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self::default()
get_input: false,
..Default::default()
}
} }
} }
@ -67,34 +61,11 @@ impl Component for Popup {
Ok(()) Ok(())
} }
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
if !self.get_input {
return Ok(None);
}
let action = match event {
Some(Event::Key(key_event)) => match key_event.code {
KeyCode::Char(c) => {
let response = c.to_ascii_lowercase();
match response {
'n' => Some(Action::UserNo),
'y' => Some(Action::UserYes),
_ => None,
}
}
_ => None,
},
Some(Event::Mouse(_)) => None,
_ => None,
};
Ok(action)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::DismissPopup => self.popup_text.clear(), Action::DismissPopup => self.popup_text.clear(),
Action::DisplayPopup(new_type, new_text) => { Action::DisplayPopup(new_type, new_text) => {
info!("Show Popup ({new_type}): {new_text}"); info!("Show Popup ({new_type}): {new_text}");
self.get_input = new_type == Type::Question;
self.popup_type = new_type; self.popup_type = new_type;
self.popup_text = format!("\n{new_text}"); self.popup_text = format!("\n{new_text}");
} }
@ -114,7 +85,6 @@ impl Component for Popup {
match self.popup_type { match self.popup_type {
Type::Info => style = style.cyan(), Type::Info => style = style.cyan(),
Type::Error => style = style.red(), Type::Error => style = style.red(),
Type::Question => style = style.yellow(),
Type::Success => style = style.green(), Type::Success => style = style.green(),
} }
@ -143,7 +113,6 @@ pub fn fortune() -> String {
7 => "TIP OF THE DAY\n\n\nNever go full Snappy!", 7 => "TIP OF THE DAY\n\n\nNever go full Snappy!",
8 => "WORDS OF WISDOM\n\n\n\nIts not DNS,\n\nTheres no way its DNS,\n\nIt was DNS.", 8 => "WORDS OF WISDOM\n\n\n\nIts not DNS,\n\nTheres no way its DNS,\n\nIt was DNS.",
9 => "HAL 9000\n\n\n\"I'm sorry Dave, I'm afraid I can't do that.\"", 9 => "HAL 9000\n\n\n\"I'm sorry Dave, I'm afraid I can't do that.\"",
10 => "\n\n\nIt's now safe to turn off your computer.",
_ => "COMPLETE\n\n\nThank you for using this tool!", _ => "COMPLETE\n\n\nThank you for using this tool!",
}) })
} }

View file

@ -1,197 +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 crossterm::event::KeyCode;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph},
};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::UnboundedSender;
use tui_input::{Input, InputRequest};
use super::Component;
use crate::{
action::Action, components::popup::Type as PopupType, config::Config, osticket::OsTicket,
state::Mode, tui::Event,
};
#[derive(Default)]
pub struct TicketSelection {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
input: Input,
mode: Mode,
osticket_arc: Arc<Mutex<OsTicket>>,
}
impl TicketSelection {
#[must_use]
pub fn new(osticket_arc: Arc<Mutex<OsTicket>>) -> Self {
Self {
osticket_arc,
..Default::default()
}
}
}
impl Component for TicketSelection {
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 handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
if self.mode != Mode::SelectTicket {
return Ok(None);
}
let action = match event {
Some(Event::Key(key_event)) => match key_event.code {
KeyCode::Backspace => {
self.input.handle(InputRequest::DeletePrevChar);
if let Ok(id) = self.input.value().parse::<u16>() {
Some(Action::SelectTicket(id))
} else {
Some(Action::ResetTicket)
}
}
KeyCode::Char(c) => {
let mut result = None;
if c.is_ascii_digit() {
self.input.handle(InputRequest::InsertChar(c));
if let Ok(id) = self.input.value().parse::<u16>() {
result = Some(Action::SelectTicket(id));
} else {
result = Some(Action::ResetTicket);
}
}
result
}
KeyCode::Enter => Some(Action::SetMode(Mode::ScanDisks)),
KeyCode::Esc => Some(Action::DisplayPopup(
PopupType::Question,
String::from("Disable osTicket integration?\n\n\nY / N"),
)),
_ => None,
},
Some(Event::Mouse(_)) => None,
_ => None,
};
Ok(action)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
if let Action::SetMode(mode) = action {
self.mode = mode;
self.input.reset();
if self.mode == Mode::SelectTicket
&& let Ok(osticket) = self.osticket_arc.lock()
&& let Some(id) = &osticket.ticket.id
{
id.to_string().chars().for_each(|c| {
let _ = self.input.handle(InputRequest::InsertChar(c));
});
}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::SelectTicket {
// Bail early
return Ok(());
}
let [_, center_area, _] = Layout::horizontal([
Constraint::Length(1),
Constraint::Min(1),
Constraint::Length(1),
])
.areas(area);
let [_, input_area, info_area, _] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(1),
])
.areas(center_area);
frame.render_widget(Clear, area);
let outer_block = Block::bordered().cyan().bold();
frame.render_widget(outer_block, area);
// Input Box
let width = input_area.width.max(3) - 3; // keep 2 for borders and 1 for cursor
let scroll = self.input.visual_scroll(width as usize);
let input = Paragraph::new(self.input.value())
.scroll((0, scroll as u16))
.white()
.block(Block::new().borders(Borders::BOTTOM).title("Enter ID"));
frame.render_widget(input, input_area);
// Info Box
let mut lines = Vec::with_capacity(4);
if let Ok(osticket) = self.osticket_arc.lock() {
if !osticket.connected {
lines.push(Line::from("Connecting...").style(Style::new().yellow()));
} else {
let ticket_id = if let Some(id) = &osticket.ticket.id {
id.to_string()
} else {
String::from("...")
};
let ticket_name = if let Some(name) = &osticket.ticket.name {
name.clone()
} else {
String::from("...")
};
let ticket_subject = if let Some(subject) = &osticket.ticket.subject {
subject.clone()
} else {
String::from("...")
};
lines.push(Line::from(vec![
Span::from("Ticket ID: ").bold(),
Span::from(ticket_id),
]));
lines.push(Line::from(vec![
Span::from("Customer Name: ").bold(),
Span::from(ticket_name),
]));
lines.push(Line::from(vec![
Span::from("Subject: ").bold(),
Span::from(ticket_subject),
]));
}
}
let info = Paragraph::new(lines)
.white()
.block(Block::new().borders(Borders::NONE).title("Ticket Info"));
frame.render_widget(info, info_area);
// Ratatui hides the cursor unless it's explicitly set. Position the cursor past the
// end of the input text and one line down from the border to the input line
let x = self.input.visual_cursor().max(scroll) - scroll;
frame.set_cursor_position((input_area.x + x as u16, input_area.y + 1));
// Done
Ok(())
}
}

View file

@ -62,8 +62,6 @@ pub struct Config {
pub network_user: String, pub network_user: String,
#[serde(default)] #[serde(default)]
pub network_pass: String, pub network_pass: String,
#[serde(default)]
pub ost_server: String,
} }
pub static PROJECT_NAME: &str = "DEJA-VU"; pub static PROJECT_NAME: &str = "DEJA-VU";
@ -86,15 +84,14 @@ impl Config {
let config_dir = get_config_dir(); let config_dir = get_config_dir();
let mut builder = config::Config::builder() let mut builder = config::Config::builder()
.set_default("app_title", default_config.app_title.as_str())? .set_default("app_title", default_config.app_title.as_str())?
.set_default("clone_app_path", default_config.clone_app_path.to_str())? .set_default("clone_app_path", default_config.app_title.as_str())?
.set_default("conemu_path", default_config.conemu_path.to_str())? .set_default("conemu_path", default_config.app_title.as_str())?
.set_default("config_dir", config_dir.to_str().unwrap())? .set_default("config_dir", config_dir.to_str().unwrap())?
.set_default("data_dir", data_dir.to_str().unwrap())? .set_default("data_dir", data_dir.to_str().unwrap())?
.set_default("network_server", default_config.network_server.as_str())? .set_default("network_server", default_config.app_title.as_str())?
.set_default("network_share", default_config.network_share.as_str())? .set_default("network_share", default_config.app_title.as_str())?
.set_default("network_user", default_config.network_user.as_str())? .set_default("network_user", default_config.app_title.as_str())?
.set_default("network_pass", default_config.network_pass.as_str())? .set_default("network_pass", default_config.app_title.as_str())?;
.set_default("ost_server", default_config.ost_server.as_str())?;
let config_files = [ let config_files = [
("config.json5", config::FileFormat::Json5), ("config.json5", config::FileFormat::Json5),

View file

@ -20,7 +20,6 @@ pub mod config;
pub mod errors; pub mod errors;
pub mod line; pub mod line;
pub mod logging; pub mod logging;
pub mod osticket;
pub mod state; pub mod state;
pub mod system; pub mod system;
pub mod tasks; pub mod tasks;

View file

@ -1,127 +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 ost_db_connector::Ticket;
use sqlx::{MySql, Pool};
use crate::system::disk::Disk;
pub enum ReportType {
Source,
Destination { label: String },
}
#[derive(Debug, Default)]
pub struct OsTicket {
pub connected: bool,
pub disabled: bool,
pub error: String,
pub server_url: String,
pub report: Vec<String>,
pub tech_note: String,
pub ticket: Ticket,
}
impl OsTicket {
pub fn new(ost_server: &str) -> Self {
Self {
connected: false,
disabled: false,
server_url: ost_server.to_string(),
..Default::default()
}
}
pub fn add_error(&mut self, text: &str) {
let lines: Vec<&str> = text
.lines()
.filter(|line| {
let line = line.trim();
!line.is_empty() && !line.eq_ignore_ascii_case("ERROR")
})
.collect();
self.error = lines.join("\n");
}
pub fn disable(&mut self) {
self.connected = false;
self.disabled = true;
}
pub fn get_report(&mut self) -> String {
if !self.error.is_empty() {
self.report.push(format!("\n\nERROR: {}", self.error));
}
self.report.join("\n")
}
pub fn set_db_pool(&mut self, pool: Option<Pool<MySql>>) {
if self.disabled {
return;
}
match pool {
Some(pool) => {
self.connected = true;
self.ticket.pool = Some(pool);
}
None => {
self.disable();
}
}
}
pub fn update_report(&mut self, disk: &Disk, report_type: ReportType) {
// Header
if self.report.is_empty() {
self.report.push(String::from("[Deja-Vu Report]"));
if !self.tech_note.is_empty() {
self.report.push(format!("Tech note: {}", self.tech_note));
}
}
// Disk Label
match report_type {
ReportType::Source => {
self.report.push(String::new());
self.report.push(String::from("Source:"));
}
ReportType::Destination { label } => {
self.report.push(String::new());
self.report.push(String::from("---"));
self.report.push(String::new());
self.report.push(format!("Destination ({label}):"));
}
}
// Disk Details
self.report.push(format!(
"... {:<8} {:>11} {:<4} {:<4} {}",
"Disk ID", "Size", "Conn", "Type", "Model (Serial)"
));
self.report.push(format!("... {}", disk.description));
self.report.push(String::new());
self.report.push(format!(
"... {:<8} {:>11} {:<7} {}",
"Part ID", "Size", "(FS)", "\"Label\""
));
disk.parts_description.iter().for_each(|desc| {
self.report.push(format!("... {desc}"));
});
if disk.parts_description.is_empty() {
self.report.push(String::from("... -None-"));
};
}
}

View file

@ -42,8 +42,6 @@ pub enum Mode {
Clone, Clone,
SelectParts, SelectParts,
PostClone, PostClone,
// osTicket
SelectTicket,
// Windows Installer // Windows Installer
ScanWinSources, ScanWinSources,
SelectWinSource, SelectWinSource,

View file

@ -18,7 +18,7 @@ name = "deja-vu"
authors = ["2Shirt <2xShirt@gmail.com>"] authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024" edition = "2024"
license = "GPL" license = "GPL"
version = "0.3.0" version = "0.2.0"
[dependencies] [dependencies]
core = { path = "../core" } core = { path = "../core" }
@ -41,9 +41,6 @@ tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
check_elevation = "0.2.4" check_elevation = "0.2.4"
[dependencies.ost-db-connector]
git = "ssh://git@git.1201.com:2222/1201/ost-db-connector.git"
[build-dependencies] [build-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] } vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

View file

@ -21,13 +21,11 @@ use core::{
left::{Left, SelectionType}, left::{Left, SelectionType},
popup, popup,
right::Right, right::Right,
select_ticket::TicketSelection,
state::StatefulList, state::StatefulList,
title::Title, title::Title,
}, },
config::Config, config::Config,
line::{DVLine, get_disk_description_right, get_part_description}, line::{DVLine, get_disk_description_right, get_part_description},
osticket::{OsTicket, ReportType},
state::Mode, state::Mode,
system::{ system::{
boot, boot,
@ -47,7 +45,6 @@ use std::{
}; };
use color_eyre::Result; use color_eyre::Result;
use ost_db_connector::ResponseColor;
use ratatui::{ use ratatui::{
crossterm::event::KeyEvent, crossterm::event::KeyEvent,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
@ -55,7 +52,7 @@ use ratatui::{
style::Color, style::Color,
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, info, warn}; use tracing::{debug, info};
use crate::state::State; use crate::state::State;
@ -70,8 +67,6 @@ pub struct App {
should_quit: bool, should_quit: bool,
should_suspend: bool, should_suspend: bool,
tick_rate: f64, tick_rate: f64,
// osTicket
osticket_arc: Arc<Mutex<OsTicket>>,
// App // App
cur_mode: Mode, cur_mode: Mode,
list: StatefulList<usize>, list: StatefulList<usize>,
@ -84,10 +79,7 @@ pub struct App {
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 config = Config::new()?;
let disk_list_arc = Arc::new(Mutex::new(Vec::new())); let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let osticket = OsTicket::new(&config.ost_server);
let osticket_arc = Arc::new(Mutex::new(osticket));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone()); let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
Ok(Self { Ok(Self {
// TUI // TUI
@ -98,7 +90,6 @@ impl App {
Box::new(Left::new()), Box::new(Left::new()),
Box::new(Right::new()), Box::new(Right::new()),
Box::new(Footer::new()), Box::new(Footer::new()),
Box::new(TicketSelection::new(osticket_arc.clone())),
Box::new(popup::Popup::new()), Box::new(popup::Popup::new()),
], ],
config: Config::new()?, config: Config::new()?,
@ -107,8 +98,6 @@ impl App {
should_quit: false, should_quit: false,
should_suspend: false, should_suspend: false,
tick_rate, tick_rate,
// osTicket
osticket_arc,
// App // App
state: State::new(disk_list_arc), state: State::new(disk_list_arc),
cur_mode: Mode::default(), cur_mode: Mode::default(),
@ -121,7 +110,7 @@ impl App {
pub fn prev_mode(&mut self) -> Option<Mode> { pub fn prev_mode(&mut self) -> Option<Mode> {
match self.cur_mode { match self.cur_mode {
Mode::Home | Mode::SelectTicket => Some(Mode::Home), Mode::Home => Some(Mode::Home),
Mode::Failed => Some(Mode::Failed), Mode::Failed => Some(Mode::Failed),
Mode::Done => Some(Mode::Done), Mode::Done => Some(Mode::Done),
Mode::SelectParts => Some(Mode::SelectParts), Mode::SelectParts => Some(Mode::SelectParts),
@ -153,8 +142,7 @@ impl App {
pub fn next_mode(&mut self) -> Option<Mode> { pub fn next_mode(&mut self) -> Option<Mode> {
let new_mode = match self.cur_mode { let new_mode = match self.cur_mode {
Mode::Home => Mode::SelectTicket, Mode::Home | Mode::InstallDrivers => Mode::ScanDisks,
Mode::SelectTicket | Mode::InstallDrivers => Mode::ScanDisks,
Mode::ScanDisks => Mode::SelectDisks, Mode::ScanDisks => Mode::SelectDisks,
Mode::SelectDisks => Mode::SelectTableType, Mode::SelectDisks => Mode::SelectTableType,
Mode::SelectTableType => Mode::Confirm, Mode::SelectTableType => Mode::Confirm,
@ -318,21 +306,11 @@ impl App {
component.init(tui.size()?)?; component.init(tui.size()?)?;
} }
// Start background task to connect to osTicket
let osticket_arc = self.osticket_arc.clone();
let ost_server = self.config.ost_server.clone();
tokio::spawn(async move {
let pool = ost_db_connector::connect(Some(ost_server), true).await;
if let Ok(mut osticket) = osticket_arc.lock() {
osticket.set_db_pool(pool);
}
});
let action_tx = self.action_tx.clone(); let action_tx = self.action_tx.clone();
action_tx.send(Action::SetMode(Mode::SelectTicket))?; action_tx.send(Action::SetMode(Mode::ScanDisks))?;
loop { loop {
self.handle_events(&mut tui).await?; self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui).await?; self.handle_actions(&mut tui)?;
if self.should_suspend { if self.should_suspend {
tui.suspend()?; tui.suspend()?;
action_tx.send(Action::Resume)?; action_tx.send(Action::Resume)?;
@ -391,7 +369,7 @@ impl App {
Ok(()) Ok(())
} }
async fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> { fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> {
while let Ok(action) = self.action_rx.try_recv() { while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render { if action != Action::Tick && action != Action::Render {
debug!("{action:?}"); debug!("{action:?}");
@ -411,9 +389,6 @@ impl App {
Action::KeyUp => self.list.previous(), Action::KeyUp => self.list.previous(),
Action::KeyDown => self.list.next(), Action::KeyDown => self.list.next(),
Action::Error(ref msg) => { Action::Error(ref msg) => {
if let Ok(mut osticket) = self.osticket_arc.lock() {
osticket.add_error(msg);
}
self.action_tx self.action_tx
.send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?; .send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?;
self.action_tx.send(Action::SetMode(Mode::Failed))?; self.action_tx.send(Action::SetMode(Mode::Failed))?;
@ -432,22 +407,6 @@ impl App {
}, },
Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?, Action::Render => self.render(tui)?,
Action::PostResponse { color, ref text } => {
if let Ok(osticket) = self.osticket_arc.lock()
&& osticket.connected
&& !osticket.disabled
{
let result = osticket.ticket.post_response(color, &text).await;
match result {
Ok(_) => {
info!("Posted results to osTicket!");
}
Err(err) => {
warn!("Failed to post results to osTicket: {err}");
}
};
}
}
Action::PrevScreen => { Action::PrevScreen => {
if let Some(new_mode) = self.prev_mode() { if let Some(new_mode) = self.prev_mode() {
self.prev_mode = new_mode; self.prev_mode = new_mode;
@ -503,10 +462,6 @@ impl App {
self.selections[0] = one; self.selections[0] = one;
self.selections[1] = two; self.selections[1] = two;
} }
Action::SelectTicket(id) => {
let mut osticket = self.osticket_arc.lock().unwrap();
let _ = osticket.ticket.select_id(id).await;
}
Action::SetMode(new_mode) => { Action::SetMode(new_mode) => {
// Clear TableType selection // Clear TableType selection
match new_mode { match new_mode {
@ -516,15 +471,10 @@ impl App {
_ => {} _ => {}
} }
self.set_mode(new_mode)?; self.set_mode(new_mode)?;
// Update components
self.action_tx self.action_tx
.send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?; .send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?;
self.action_tx.send(build_left_items(self))?; self.action_tx.send(build_left_items(self))?;
self.action_tx.send(build_right_items(self))?; self.action_tx.send(build_right_items(self))?;
// Update state
// TODO: Verify this shouldn't be before update components?
match new_mode { match new_mode {
Mode::SelectTableType | Mode::Confirm => { Mode::SelectTableType | Mode::Confirm => {
// Select source/dest disks // Select source/dest disks
@ -550,74 +500,8 @@ impl App {
} }
_ => {} _ => {}
}; };
// Update osTicket
match new_mode {
Mode::Confirm => {
if let Ok(mut osticket) = self.osticket_arc.lock()
&& osticket.connected
&& !osticket.disabled
{
info!("Updating OST Report (Before)...");
let disk_list = self.state.disk_list.lock().unwrap();
if let Some(source_index) = self.state.disk_index_source
&& let Some(source_disk) = disk_list.get(source_index)
&& let Some(dest_index) = self.state.disk_index_dest
&& let Some(dest_disk) = disk_list.get(dest_index)
{
osticket.update_report(source_disk, ReportType::Source);
osticket.update_report(
dest_disk,
ReportType::Destination {
label: String::from("Before"),
},
);
}
}
}
Mode::Done | Mode::Failed => {
if let Ok(mut osticket) = self.osticket_arc.lock()
&& osticket.connected
&& !osticket.disabled
{
info!("Updating OST Report (After)...");
let disk_list = self.state.disk_list.lock().unwrap();
if let Some(dest_index) = self.state.disk_index_dest
&& let Some(dest_disk) = disk_list.get(dest_index)
{
osticket.update_report(
dest_disk,
ReportType::Destination {
label: String::from("After"),
},
);
// Set color
let color = if new_mode == Mode::Failed {
ResponseColor::DiagFail
} else {
ResponseColor::Normal
};
// Upload osTicket report
self.action_tx.send(Action::PostResponse {
color,
text: osticket.get_report(),
})?;
}
}
}
_ => {}
}
} }
Action::TasksComplete => self.action_tx.send(Action::NextScreen)?, Action::TasksComplete => self.action_tx.send(Action::NextScreen)?,
Action::UserNo => self.action_tx.send(Action::DismissPopup)?,
Action::UserYes => {
if let Ok(mut osticket) = self.osticket_arc.lock() {
osticket.disable();
}
self.action_tx.send(Action::NextScreen)?;
}
_ => {} _ => {}
} }
for component in &mut self.components { for component in &mut self.components {
@ -670,10 +554,8 @@ impl App {
fn render(&mut self, tui: &mut Tui) -> Result<()> { fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| { tui.draw(|frame| {
if let [header, _body, footer, left, right, select_ticket, popup] = if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] {
get_chunks(frame.area())[..] let component_areas = vec![header, left, right, footer, popup];
{
let component_areas = vec![header, left, right, footer, select_ticket, popup];
for (component, area) in zip(self.components.iter_mut(), component_areas) { for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) { if let Err(err) = component.draw(frame, area) {
let _ = self let _ = self
@ -712,7 +594,7 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
fn get_chunks(r: Rect) -> Vec<Rect> { fn get_chunks(r: Rect) -> Vec<Rect> {
let mut chunks: Vec<Rect> = Vec::with_capacity(6); let mut chunks: Vec<Rect> = Vec::with_capacity(6);
// Main sections (Title, body, footer) // Main sections
chunks.extend( chunks.extend(
Layout::default() Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@ -734,9 +616,6 @@ fn get_chunks(r: Rect) -> Vec<Rect> {
.to_vec(), .to_vec(),
); );
// Ticket Selection
chunks.push(centered_rect(60, 40, r));
// Popup // Popup
chunks.push(centered_rect(60, 25, r)); chunks.push(centered_rect(60, 25, r));
@ -756,7 +635,7 @@ fn build_footer_string(cur_mode: Mode) -> String {
), ),
Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"), Mode::SelectTableType => String::from("(Enter) to select / (b) to go back / (q) to quit"),
Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"), Mode::Confirm => String::from("(Enter) to confirm / (b) to go back / (q) to quit"),
Mode::Done | Mode::Failed | Mode::SelectTicket => String::from("(Enter) or (q) to quit"), Mode::Done | Mode::Failed => String::from("(Enter) or (q) to quit"),
// Invalid states // Invalid states
Mode::BootDiags Mode::BootDiags
| Mode::BootScan | Mode::BootScan
@ -830,10 +709,6 @@ fn build_left_items(app: &App) -> Action {
}); });
} }
} }
Mode::SelectTicket => {
title = String::new();
select_type = SelectionType::Loop;
}
Mode::Done | Mode::Failed => { Mode::Done | Mode::Failed => {
select_type = SelectionType::Loop; select_type = SelectionType::Loop;
title = String::from("Done"); title = String::from("Done");
@ -860,21 +735,6 @@ fn build_right_items(app: &App) -> Action {
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new(); let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0; let mut start_index = 0;
if let Ok(osticket) = app.osticket_arc.lock()
&& !osticket.disabled
&& let Some(name) = &osticket.ticket.name
{
items.push(vec![DVLine {
line_parts: vec![
format!("osTicket: #{} ", osticket.ticket.id.unwrap()),
name.clone(),
String::from(" // "),
osticket.ticket.subject.clone().unwrap(),
],
line_colors: vec![Color::Reset, Color::Cyan, Color::Reset, Color::Reset],
}]);
start_index += 1;
}
match app.cur_mode { match app.cur_mode {
Mode::InstallDrivers => { Mode::InstallDrivers => {
items.push(vec![DVLine { items.push(vec![DVLine {
@ -885,7 +745,7 @@ fn build_right_items(app: &App) -> Action {
line_parts: vec![get_cpu_name()], line_parts: vec![get_cpu_name()],
line_colors: vec![Color::Reset], line_colors: vec![Color::Reset],
}]); }]);
start_index += 2; start_index = 2;
} }
Mode::SelectDisks | Mode::SelectTableType | Mode::Confirm => { Mode::SelectDisks | Mode::SelectTableType | Mode::Confirm => {
// Labels // Labels
@ -928,13 +788,8 @@ fn build_right_items(app: &App) -> Action {
line_colors: vec![Color::Cyan], line_colors: vec![Color::Cyan],
}]); }]);
} }
start_index += 1;
items.push(vec![DVLine {
line_parts: vec![String::new()],
line_colors: vec![Color::Reset],
}]);
if let Some(index) = app.state.disk_index_dest { if let Some(index) = app.state.disk_index_dest {
start_index += 1; start_index = 1;
let disk_list = app.state.disk_list.lock().unwrap(); let disk_list = app.state.disk_list.lock().unwrap();
if let Some(disk) = disk_list.get(index) { if let Some(disk) = disk_list.get(index) {
// Disk Details // Disk Details

View file

@ -137,7 +137,6 @@ impl App {
| Mode::PEMenu | Mode::PEMenu
| Mode::Process | Mode::Process
| Mode::SelectParts | Mode::SelectParts
| Mode::SelectTicket
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
} }
} }
@ -167,12 +166,11 @@ impl App {
)); ));
// Build Diskpart script to format destination disk // Build Diskpart script to format destination disk
if let Some(disk) = self.state.get_dest_disk() { let disk_list = self.state.disk_list.lock().unwrap();
let table_type = self if let Some(disk_index) = self.state.disk_index_dest
.state && let Some(disk) = disk_list.get(disk_index)
.table_type {
.clone() let table_type = self.state.table_type.clone().unwrap();
.expect("Failed to get table type.");
let diskpart_script = build_dest_format_script( let diskpart_script = build_dest_format_script(
disk.id, disk.id,
&table_type, &table_type,
@ -198,24 +196,15 @@ impl App {
let wimlib_imagex = program_files.join("wimlib\\wimlib-imagex.exe"); let wimlib_imagex = program_files.join("wimlib\\wimlib-imagex.exe");
// Get image info // Get image info
let wim_file_index = self let wim_sources = self.state.wim_sources.lock().unwrap();
.state let wim_file = wim_sources.get_file(self.state.wim_file_index.unwrap());
.wim_file_index let wim_index = self.state.wim_image_index.unwrap() + 1; // wimapply uses 1-based index
.expect("Failed to get source WIM file index");
let mut wim_image_index = self
.state
.wim_image_index
.expect("Failed to get source WIM image index");
let wim_sources = self
.state
.wim_sources
.lock()
.expect("Failed to get WIM Sources");
let wim_file = wim_sources.get_file(wim_file_index);
wim_image_index += 1; // wimapply uses 1-based index
// Add actions // Add actions
if let Some(disk) = self.state.get_dest_disk() { let disk_list = self.state.disk_list.lock().unwrap();
if let Some(disk_index) = self.state.disk_index_dest
&& let Some(disk) = disk_list.get(disk_index)
{
let num_parts = disk.parts.len(); let num_parts = disk.parts.len();
let dest_path = format!("{}:\\", disk.get_part_letter(num_parts - 1)); let dest_path = format!("{}:\\", disk.get_part_letter(num_parts - 1));
self.tasks.add(TaskType::CommandWait( self.tasks.add(TaskType::CommandWait(
@ -223,7 +212,7 @@ impl App {
vec![ vec![
String::from("apply"), String::from("apply"),
wim_file.path, wim_file.path,
format!("{wim_image_index}"), format!("{wim_index}"),
dest_path, dest_path,
], ],
)); ));
@ -234,29 +223,21 @@ impl App {
popup::Type::Info, popup::Type::Info,
String::from("Updating boot configuration"), String::from("Updating boot configuration"),
))?; ))?;
let wim_index = self let wim_sources = self.state.wim_sources.lock().unwrap();
.state let wim_file = wim_sources.get_file(self.state.wim_file_index.unwrap());
.wim_image_index
.expect("Failed to get source WIM index");
let wim_sources = self
.state
.wim_sources
.lock()
.expect("Failed to get WIM Sources");
let wim_file = wim_sources.get_file(wim_index);
// Get System32 path // Get System32 path
let system32 = get_system32_path(&self.action_tx); let system32 = get_system32_path(&self.action_tx);
// Add actions // Add actions
if let Some(disk) = self.state.get_dest_disk() { let disk_list = self.state.disk_list.lock().unwrap();
let table_type = self if let Some(disk_index) = self.state.disk_index_dest
.state && let Some(disk) = disk_list.get(disk_index)
.table_type {
.clone() let table_type = self.state.table_type.clone().unwrap();
.expect("Failed to get table type.");
let letter_boot = disk.get_part_letter(0); let letter_boot = disk.get_part_letter(0);
let letter_os = disk.get_part_letter(match table_type { let letter_os =
disk.get_part_letter(match self.state.table_type.clone().unwrap() {
PartitionTableType::Guid => 2, PartitionTableType::Guid => 2,
PartitionTableType::Legacy => 1, PartitionTableType::Legacy => 1,
}); });
@ -340,7 +321,7 @@ impl App {
} }
Mode::SetUserName => { Mode::SetUserName => {
if let Ok(wim_sources) = self.state.wim_sources.lock() if let Ok(wim_sources) = self.state.wim_sources.lock()
&& let Some(index) = self.state.wim_file_index && let Some(index) = self.state.wim_image_index
{ {
let image = wim_sources.get_file(index); let image = wim_sources.get_file(index);
if image.is_backup { if image.is_backup {
@ -560,11 +541,9 @@ impl App {
} }
} }
Mode::SelectWinSource => { Mode::SelectWinSource => {
info!("Set WIM file to: {one:?}");
self.state.wim_file_index = one; self.state.wim_file_index = one;
} }
Mode::SelectWinImage => { Mode::SelectWinImage => {
info!("Set WIM index to: {one:?}");
self.state.wim_image_index = one; self.state.wim_image_index = one;
} }
_ => {} _ => {}
@ -680,7 +659,6 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::PEMenu | Mode::PEMenu
| Mode::Process | Mode::Process
| Mode::SelectParts | Mode::SelectParts
| Mode::SelectTicket
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
} }
} }
@ -733,7 +711,7 @@ fn build_left_items(app: &App) -> Action {
Mode::SelectDisks => { Mode::SelectDisks => {
select_type = SelectionType::One; select_type = SelectionType::One;
title = String::from("Select Destination Disk"); title = String::from("Select Destination Disk");
let disk_list = app.state.disk_list.lock().expect("Failed to get disk list"); let disk_list = app.state.disk_list.lock().unwrap();
disk_list disk_list
.iter() .iter()
.for_each(|disk| items.push(disk.description.to_string())); .for_each(|disk| items.push(disk.description.to_string()));
@ -766,7 +744,6 @@ fn build_left_items(app: &App) -> Action {
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::SelectParts | Mode::SelectParts
| Mode::SelectTicket
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::SetBootMode => panic!("This shouldn't happen?"),
}; };
Action::UpdateLeft(title, labels, items, select_type) Action::UpdateLeft(title, labels, items, select_type)
@ -813,19 +790,14 @@ fn build_right_items(app: &App) -> Action {
} else { } else {
labels.push(vec![dest_dv_line]); labels.push(vec![dest_dv_line]);
} }
let disk_list = app.state.disk_list.lock().expect("Failed to get disk list"); let disk_list = app.state.disk_list.lock().unwrap();
disk_list disk_list
.iter() .iter()
.for_each(|disk| items.push(get_disk_description_right(disk, &None))); .for_each(|disk| items.push(get_disk_description_right(disk, &None)));
} }
Mode::SelectWinSource => { Mode::SelectWinSource => {
// Disk Info // Disk Info
let type_str = match app let type_str = match app.state.table_type.clone().unwrap() {
.state
.table_type
.clone()
.expect("Failed to get table type")
{
PartitionTableType::Guid => "GPT", PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR", PartitionTableType::Legacy => "MBR",
}; };
@ -843,8 +815,11 @@ fn build_right_items(app: &App) -> Action {
}, },
DVLine::blank(), DVLine::blank(),
]; ];
if let Some(disk) = app.state.get_dest_disk() { let disk_list = app.state.disk_list.lock().unwrap();
get_disk_description_right(&disk, &None) if let Some(index) = app.state.disk_index_dest
&& let Some(disk) = disk_list.get(index)
{
get_disk_description_right(disk, &None)
.into_iter() .into_iter()
.for_each(|dv_line| label_dv_lines.push(dv_line)); .for_each(|dv_line| label_dv_lines.push(dv_line));
} }
@ -895,12 +870,7 @@ fn build_right_items(app: &App) -> Action {
panic!("Failed to get source WIM file"); panic!("Failed to get source WIM file");
} }
// Disk Info // Disk Info
let type_str = match app let type_str = match app.state.table_type.clone().unwrap() {
.state
.table_type
.clone()
.expect("Failed to get table type")
{
PartitionTableType::Guid => "GPT", PartitionTableType::Guid => "GPT",
PartitionTableType::Legacy => "MBR", PartitionTableType::Legacy => "MBR",
}; };
@ -918,8 +888,11 @@ fn build_right_items(app: &App) -> Action {
}, },
DVLine::blank(), DVLine::blank(),
]; ];
if let Some(disk) = app.state.get_dest_disk() { let disk_list = app.state.disk_list.lock().unwrap();
get_disk_description_right(&disk, &None) if let Some(index) = app.state.disk_index_dest
&& let Some(disk) = disk_list.get(index)
{
get_disk_description_right(disk, &None)
.into_iter() .into_iter()
.for_each(|dv_line| label_dv_lines.push(dv_line)); .for_each(|dv_line| label_dv_lines.push(dv_line));
} }
@ -1003,8 +976,11 @@ fn build_right_items(app: &App) -> Action {
} else { } else {
labels.push(vec![dest_dv_line]); labels.push(vec![dest_dv_line]);
} }
if let Some(disk) = app.state.get_dest_disk() { let disk_list = app.state.disk_list.lock().unwrap();
items.push(get_disk_description_right(&disk, &None)); if let Some(index) = app.state.disk_index_dest
&& let Some(disk) = disk_list.get(index)
{
items.push(get_disk_description_right(disk, &None));
} }
} }
_ => {} _ => {}

View file

@ -62,17 +62,6 @@ impl State {
} }
} }
pub fn get_dest_disk(&self) -> Option<Disk> {
let mut dest_disk: Option<Disk> = None;
if let Ok(disk_list) = self.disk_list.lock()
&& let Some(disk_index) = self.disk_index_dest
&& let Some(disk) = disk_list.get(disk_index)
{
dest_disk = Some(disk.clone());
}
dest_disk
}
pub fn reset_all(&mut self) { pub fn reset_all(&mut self) {
self.wim_file_index = None; self.wim_file_index = None;
self.wim_image_index = None; self.wim_image_index = None;

View file

@ -26,7 +26,6 @@ use std::{
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::info;
use xml::reader::{EventReader, XmlEvent}; use xml::reader::{EventReader, XmlEvent};
use core::system::disk::bytes_to_string; use core::system::disk::bytes_to_string;
@ -188,17 +187,10 @@ impl WimSources {
rel_index = index - num_local; rel_index = index - num_local;
use_local = false; use_local = false;
}; };
info!("Converted Index: {index} -> {rel_index}");
if use_local { if use_local {
self.local self.local.get(rel_index).unwrap().clone()
.get(rel_index)
.expect("Failed to get local file by rel_index")
.to_owned()
} else { } else {
self.network self.network.get(rel_index).unwrap().clone()
.get(rel_index)
.expect("Failed to get network file by rel_index")
.to_owned()
} }
} }