233 lines
7.8 KiB
Rust
233 lines
7.8 KiB
Rust
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
|
|
|
// Declare modules
|
|
mod about;
|
|
mod config;
|
|
mod device;
|
|
mod hid_worker;
|
|
mod state;
|
|
mod ui;
|
|
mod util;
|
|
|
|
use std::process::exit;
|
|
// External Crate Imports (only those needed directly in main.rs)
|
|
use eframe::{egui, glow};
|
|
use fast_config::Config;
|
|
use std::sync::{Arc, Condvar, Mutex};
|
|
use std::time::Duration;
|
|
use clap::Parser;
|
|
|
|
// Internal Module Imports
|
|
use config::{ConfigData}; // Import specific items
|
|
use device::{VpcDevice};
|
|
use state::State; // Import the State enum
|
|
|
|
// Constants
|
|
const PROGRAM_TITLE: &str = "OpenVPC - Shift Tool";
|
|
const INITIAL_WIDTH: f32 = 740.0;
|
|
const INITIAL_HEIGHT: f32 = 260.0;
|
|
|
|
// Type aliases for shared state can make signatures cleaner
|
|
pub type SharedStateFlag = Arc<(Mutex<bool>, Condvar)>;
|
|
pub type SharedDeviceState = Arc<Mutex<u16>>; // Assuming Condvar isn't strictly needed here
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, about, long_about = None)]
|
|
struct Args {
|
|
#[arg(short, long, default_value_t = false)]
|
|
skip_firmware: bool,
|
|
}
|
|
|
|
// The main application struct
|
|
pub struct ShiftTool {
|
|
// State
|
|
state: State,
|
|
thread_state: SharedStateFlag, // Is the worker thread running?
|
|
|
|
// Device Data
|
|
device_list: Vec<VpcDevice>, // List of discovered compatible devices
|
|
|
|
// Shared state between UI and Worker Thread
|
|
source_states: Vec<SharedDeviceState>, // Current reported state per source
|
|
receiver_states: Vec<SharedDeviceState>, // Current reported state per receiver
|
|
shift_state: SharedDeviceState, // Combined/calculated shift state
|
|
|
|
// Configuration
|
|
config: Config<ConfigData>,
|
|
}
|
|
|
|
impl Default for ShiftTool {
|
|
fn default() -> Self {
|
|
// Determine config path safely
|
|
let config_dir = dirs::config_dir()
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| ".".to_string()); // Fallback to current dir
|
|
let config_path = format!("{}/shift_tool.json", config_dir);
|
|
|
|
// Handle potential config creation error
|
|
let config = match Config::new(&config_path, ConfigData::default()) {
|
|
Ok(cfg) => cfg,
|
|
Err(e) => {
|
|
// Log the error appropriately
|
|
eprintln!("Error creating config file at {}: {}", config_path, e);
|
|
// Fallback to default in-memory config? Or panic?
|
|
// Using default() here might lead to data loss if file exists but is broken.
|
|
// For simplicity here, we proceed with default, but real app might need better handling.
|
|
exit(1)
|
|
}
|
|
};
|
|
|
|
Self {
|
|
state: State::Initialising,
|
|
device_list: vec![],
|
|
source_states: vec![],
|
|
receiver_states: vec![],
|
|
shift_state: Arc::new(Mutex::new(0)), // Keep Condvar if needed for shift_state?
|
|
thread_state: Arc::new((Mutex::new(false), Condvar::new())),
|
|
config,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implementations specific to App lifecycle and top-level control
|
|
impl ShiftTool {
|
|
// Initialization logic called once at the start
|
|
fn init(&mut self) {
|
|
// Load config and populate initial sources/receivers based on config
|
|
// The config is already loaded in Default::default()
|
|
let num_sources = self.config.data.sources.len();
|
|
let num_receivers = self.config.data.receivers.len();
|
|
|
|
for _ in 0..num_sources {
|
|
self.add_source_state(); // Add state tracking
|
|
}
|
|
for _ in 0..num_receivers {
|
|
self.add_receiver_state(); // Add state tracking
|
|
}
|
|
|
|
// Initial device scan
|
|
self.refresh_devices(); // Now calls the method defined in device.rs
|
|
|
|
self.state = State::Running;
|
|
log::info!("Initialization complete. State set to Running.");
|
|
}
|
|
|
|
// Helper to add state tracking for a new source
|
|
fn add_source_state(&mut self) {
|
|
self.source_states
|
|
.push(Arc::new(Mutex::new(0)));
|
|
}
|
|
|
|
// Helper to add state tracking for a new receiver
|
|
fn add_receiver_state(&mut self) {
|
|
self.receiver_states
|
|
.push(Arc::new(Mutex::new(0)));
|
|
}
|
|
|
|
// Helper to get thread status (could be in ui.rs or main.rs)
|
|
fn get_thread_status(&self) -> bool {
|
|
match self.thread_state.0.lock() {
|
|
Ok(guard) => *guard,
|
|
Err(poisoned) => {
|
|
log::error!("Thread state mutex poisoned!");
|
|
**poisoned.get_ref() // Still try to get the value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Graceful shutdown logic
|
|
fn shutdown_app(&mut self) {
|
|
log::info!("Shutdown requested.");
|
|
// Signal the worker thread to stop
|
|
{
|
|
let &(ref lock, ref cvar) = &*self.thread_state;
|
|
match lock.lock() {
|
|
Ok(mut started) => {
|
|
*started = false;
|
|
log::info!("Signaling worker thread to stop.");
|
|
}
|
|
Err(_) => {
|
|
log::error!("Thread state mutex poisoned during shutdown!");
|
|
}
|
|
}
|
|
cvar.notify_all(); // Wake up thread if it's waiting
|
|
}
|
|
|
|
// Save configuration
|
|
if let Err(e) = self.config.save() {
|
|
log::error!("Failed to save configuration on exit: {}", e);
|
|
} else {
|
|
log::info!("Configuration saved.");
|
|
}
|
|
|
|
// Give the thread a moment to process the stop signal (optional)
|
|
// Note: Joining the thread handle would be more robust if we kept it.
|
|
std::thread::sleep(Duration::from_millis(250));
|
|
log::info!("Shutdown complete.");
|
|
}
|
|
}
|
|
|
|
// Main eframe application loop
|
|
impl eframe::App for ShiftTool {
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
log::debug!("Update Called.");
|
|
// Request repaint ensures GUI updates even if worker is slow
|
|
ctx.request_repaint_after(Duration::from_millis(50));
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
egui::Resize::default()
|
|
.default_width(INITIAL_WIDTH)
|
|
.default_height(INITIAL_HEIGHT)
|
|
.auto_sized()
|
|
.show(ui, |ui| match self.state {
|
|
State::Initialising => {
|
|
// Show a simple "Loading..." message while init runs
|
|
ui.centered_and_justified(|ui| {
|
|
ui.label("Initialising...");
|
|
});
|
|
// Actual init logic runs once after this frame
|
|
self.init();
|
|
}
|
|
State::About => {
|
|
// Call the UI drawing function from the ui module
|
|
ui::draw_about_screen(self, ui);
|
|
}
|
|
State::Running => {
|
|
// Call the UI drawing function from the ui module
|
|
ui::draw_running_state(self, ui, ctx);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Called when the application is about to close
|
|
fn on_exit(&mut self, _gl: Option<&glow::Context>) {
|
|
self.shutdown_app();
|
|
}
|
|
}
|
|
|
|
// Application Entry Point
|
|
fn main() -> eframe::Result<()> {
|
|
// Initialize logging
|
|
env_logger::init();
|
|
|
|
// --- Command Line Argument Parsing ---
|
|
// let _args = Args::parse();
|
|
// --- End Argument Parsing ---
|
|
|
|
log::info!("Starting {}", PROGRAM_TITLE);
|
|
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default()
|
|
.with_inner_size([INITIAL_WIDTH, INITIAL_HEIGHT])
|
|
.with_title(PROGRAM_TITLE), // Set window title here
|
|
..Default::default()
|
|
};
|
|
|
|
eframe::run_native(
|
|
PROGRAM_TITLE, // Used for window title if not set in viewport
|
|
options,
|
|
Box::new(|_cc| Ok(Box::new(ShiftTool::default()))), // Create the app instance
|
|
)
|
|
}
|