Compare commits

..

No commits in common. "049b036e651870b764a0ff2ca276eded8a7d6106" and "6e3a0a582f2eadfd060514dca4942c7a735aacdf" have entirely different histories.

14 changed files with 738 additions and 2658 deletions

5
.gitignore vendored
View file

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

2821
Cargo.lock generated

File diff suppressed because it is too large Load diff

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

@ -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(),
} }

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

@ -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