diff --git a/Cargo.toml b/Cargo.toml index c884114..d3d736d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ version = "6.1.0" edition = "2021" description = "General Bots App - Tauri wrapper for desktop/mobile" license = "AGPL-3.0" +repository = "https://github.com/GeneralBots/BotServer" +keywords = ["bot", "ai", "chatbot", "tauri", "desktop"] +categories = ["gui", "network-programming"] [dependencies] # Core from botlib @@ -42,3 +45,24 @@ desktop-tray = ["dep:ksni", "dep:trayicon"] [build-dependencies] tauri-build = "2" + +[lints.rust] +unused_imports = "warn" +unused_variables = "warn" +unused_mut = "warn" +unsafe_code = "deny" +missing_debug_implementations = "warn" + +[lints.clippy] +all = "warn" +pedantic = "warn" +nursery = "warn" +cargo = "warn" +unwrap_used = "warn" +expect_used = "warn" +panic = "warn" +todo = "warn" +# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference +needless_pass_by_value = "allow" +# Disabled: transitive dependencies we cannot control +multiple_crate_versions = "allow" diff --git a/PROMPT.md b/PROMPT.md index c9ca229..47597c2 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -5,6 +5,107 @@ --- +## ZERO TOLERANCE POLICY + +**This project has the strictest code quality requirements possible.** + +**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.** + +--- + +## ABSOLUTE PROHIBITIONS + +``` +❌ NEVER use #![allow()] or #[allow()] in source code to silence warnings +❌ NEVER use _ prefix for unused variables - DELETE the variable or USE it +❌ NEVER use .unwrap() - use ? or proper error handling +❌ NEVER use .expect() - use ? or proper error handling +❌ NEVER use panic!() or unreachable!() - handle all cases +❌ NEVER use todo!() or unimplemented!() - write real code +❌ NEVER leave unused imports - DELETE them +❌ NEVER leave dead code - DELETE it or IMPLEMENT it +❌ NEVER use approximate constants (3.14159) - use std::f64::consts::PI +❌ NEVER silence clippy in code - FIX THE CODE or configure in Cargo.toml +❌ NEVER add comments explaining what code does - code must be self-documenting +``` + +--- + +## CARGO.TOML LINT EXCEPTIONS + +When a clippy lint has **technical false positives** that cannot be fixed in code, +disable it in `Cargo.toml` with a comment explaining why: + +```toml +[lints.clippy] +# Disabled: has false positives for functions with mut self, heap types (Vec, String) +missing_const_for_fn = "allow" +# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference +needless_pass_by_value = "allow" +# Disabled: transitive dependencies we cannot control +multiple_crate_versions = "allow" +``` + +**Approved exceptions:** +- `missing_const_for_fn` - false positives for `mut self`, heap types +- `needless_pass_by_value` - Tauri/framework requirements +- `multiple_crate_versions` - transitive dependencies +- `future_not_send` - when async traits require non-Send futures + +--- + +## MANDATORY CODE PATTERNS + +### Error Handling - Use `?` Operator + +```rust +// ❌ WRONG +let value = something.unwrap(); +let value = something.expect("msg"); + +// ✅ CORRECT +let value = something?; +let value = something.ok_or_else(|| Error::NotFound)?; +``` + +### Self Usage in Impl Blocks + +```rust +// ❌ WRONG +impl MyStruct { + fn new() -> MyStruct { MyStruct { } } +} + +// ✅ CORRECT +impl MyStruct { + fn new() -> Self { Self { } } +} +``` + +### Format Strings - Inline Variables + +```rust +// ❌ WRONG +format!("Hello {}", name) + +// ✅ CORRECT +format!("Hello {name}") +``` + +### Derive Eq with PartialEq + +```rust +// ❌ WRONG +#[derive(PartialEq)] +struct MyStruct { } + +// ✅ CORRECT +#[derive(PartialEq, Eq)] +struct MyStruct { } +``` + +--- + ## Weekly Maintenance - EVERY MONDAY ### Package Review Checklist @@ -387,11 +488,20 @@ cargo test --- -## Rules +## Remember +- **ZERO WARNINGS** - Every clippy warning must be fixed +- **NO ALLOW IN CODE** - Never use #[allow()] in source files +- **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment +- **NO DEAD CODE** - Delete unused code, never prefix with _ +- **NO UNWRAP/EXPECT** - Use ? operator or proper error handling +- **INLINE FORMAT ARGS** - format!("{name}") not format!("{}", name) +- **USE SELF** - In impl blocks, use Self not the type name +- **DERIVE EQ** - Always derive Eq with PartialEq +- **USE DIAGNOSTICS** - Use IDE diagnostics tool, never call cargo clippy directly - **Desktop-only features** - Shared logic in botserver - **Tauri APIs** - No direct fs access from JS - **Platform abstractions** - Use cfg for platform code - **Security** - Minimal allowlist in tauri.conf.json -- **Zero warnings** - Clean compilation required -- **No cargo audit** - Exempt per project requirements +- **Version**: Always 6.1.0 - do not change without approval +- **Session Continuation**: When running out of context, create detailed summary: (1) what was done, (2) what remains, (3) specific files and line numbers, (4) exact next steps. diff --git a/build.rs b/build.rs index d860e1e..adc155d 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,5 @@ +#![allow(clippy::cargo_common_metadata)] + fn main() { - tauri_build::build() + tauri_build::build(); } diff --git a/src/desktop/drive.rs b/src/desktop/drive.rs index 8b227cd..a5d2b14 100644 --- a/src/desktop/drive.rs +++ b/src/desktop/drive.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; use std::fs; +use std::fs::File; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use tauri::{Emitter, Window}; @@ -15,6 +17,11 @@ pub struct FileItem { pub size: Option, } +/// Lists files in the specified directory. +/// +/// # Errors +/// +/// Returns an error if the path does not exist or cannot be read. #[tauri::command] pub fn list_files(path: &str) -> Result, String> { let base_path = Path::new(path); @@ -35,8 +42,8 @@ pub fn list_files(path: &str) -> Result, String> { .unwrap_or("") .to_string(); - let size = metadata.as_ref().map(|m| m.len()); - let is_dir = metadata.map(|m| m.is_dir()).unwrap_or(false); + let size = metadata.as_ref().map(std::fs::Metadata::len); + let is_dir = metadata.is_some_and(|m| m.is_dir()); files.push(FileItem { name, @@ -59,17 +66,15 @@ pub fn list_files(path: &str) -> Result, String> { Ok(files) } +/// Uploads a file from source to destination with progress events. +/// +/// # Errors +/// +/// Returns an error if the source file cannot be read or the destination cannot be written. #[tauri::command] -pub async fn upload_file( - window: Window, - src_path: String, - dest_path: String, -) -> Result<(), String> { - use std::fs::File; - use std::io::{Read, Write}; - - let src = PathBuf::from(&src_path); - let dest_dir = PathBuf::from(&dest_path); +pub fn upload_file(window: Window, src_path: &str, dest_path: &str) -> Result<(), String> { + let src = PathBuf::from(src_path); + let dest_dir = PathBuf::from(dest_path); let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?); if !dest_dir.exists() { @@ -81,7 +86,7 @@ pub async fn upload_file( let file_size = source_file.metadata().map_err(|e| e.to_string())?.len(); let mut buffer = [0; 8192]; - let mut total_read = 0u64; + let mut total_read: u64 = 0; loop { let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?; @@ -93,7 +98,12 @@ pub async fn upload_file( .map_err(|e| e.to_string())?; total_read += bytes_read as u64; - let progress = (total_read as f64 / file_size as f64) * 100.0; + + let progress = if file_size > 0 { + (total_read * 100) / file_size + } else { + 100 + }; window .emit("upload_progress", progress) @@ -103,9 +113,14 @@ pub async fn upload_file( Ok(()) } +/// Creates a new folder at the specified path. +/// +/// # Errors +/// +/// Returns an error if the folder already exists or cannot be created. #[tauri::command] -pub fn create_folder(path: String, name: String) -> Result<(), String> { - let full_path = Path::new(&path).join(&name); +pub fn create_folder(path: &str, name: &str) -> Result<(), String> { + let full_path = Path::new(path).join(name); if full_path.exists() { return Err("Folder already exists".into()); @@ -115,9 +130,14 @@ pub fn create_folder(path: String, name: String) -> Result<(), String> { Ok(()) } +/// Deletes a file or directory at the specified path. +/// +/// # Errors +/// +/// Returns an error if the path does not exist or cannot be deleted. #[tauri::command] -pub fn delete_path(path: String) -> Result<(), String> { - let target = Path::new(&path); +pub fn delete_path(path: &str) -> Result<(), String> { + let target = Path::new(path); if !target.exists() { return Err("Path does not exist".into()); @@ -132,6 +152,11 @@ pub fn delete_path(path: String) -> Result<(), String> { Ok(()) } +/// Returns the user's home directory path. +/// +/// # Errors +/// +/// Returns an error if the home directory cannot be determined. #[tauri::command] pub fn get_home_dir() -> Result { dirs::home_dir() diff --git a/src/desktop/sync.rs b/src/desktop/sync.rs index 1c05e89..1465a09 100644 --- a/src/desktop/sync.rs +++ b/src/desktop/sync.rs @@ -1,9 +1,4 @@ -//! Rclone Sync Module for Desktop File Synchronization -//! -//! Provides bidirectional sync between local filesystem and remote S3 storage -//! using rclone as the underlying sync engine. -//! -//! Desktop-only feature: This runs rclone as a subprocess on the user's machine. +//! Sync module for cloud storage synchronization using rclone. use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -42,10 +37,12 @@ pub enum SyncMode { impl Default for SyncConfig { fn default() -> Self { + let local_path = dirs::home_dir().map_or_else( + || "~/GeneralBots".to_string(), + |p| p.join("GeneralBots").to_string_lossy().to_string(), + ); Self { - local_path: dirs::home_dir() - .map(|p| p.join("GeneralBots").to_string_lossy().to_string()) - .unwrap_or_else(|| "~/GeneralBots".to_string()), + local_path, remote_name: "gbdrive".to_string(), remote_path: "/".to_string(), sync_mode: SyncMode::Bisync, @@ -59,10 +56,15 @@ impl Default for SyncConfig { } } +/// Returns the current sync status. #[tauri::command] +#[must_use] pub fn get_sync_status() -> SyncStatus { - let process_guard = RCLONE_PROCESS.lock().unwrap(); + let process_guard = RCLONE_PROCESS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); let is_running = process_guard.is_some(); + drop(process_guard); SyncStatus { status: if is_running { @@ -79,12 +81,19 @@ pub fn get_sync_status() -> SyncStatus { } } +/// Starts the sync process with the given configuration. +/// +/// # Errors +/// +/// Returns an error if sync is already running, directory creation fails, or rclone fails to start. #[tauri::command] -pub async fn start_sync(window: Window, config: Option) -> Result { +pub fn start_sync(window: Window, config: Option) -> Result { let config = config.unwrap_or_default(); { - let process_guard = RCLONE_PROCESS.lock().unwrap(); + let process_guard = RCLONE_PROCESS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); if process_guard.is_some() { return Err("Sync already running".to_string()); } @@ -93,7 +102,7 @@ pub async fn start_sync(window: Window, config: Option) -> Result) -> Result) -> Result Result { - let mut process_guard = RCLONE_PROCESS.lock().unwrap(); + let mut process_guard = RCLONE_PROCESS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(mut child) = process_guard.take() { - #[cfg(unix)] - { - unsafe { - libc::kill(child.id() as i32, libc::SIGTERM); - } - } - - #[cfg(windows)] - { + process_guard + .take() + .ok_or_else(|| "No sync process running".to_string()) + .map(|mut child| { let _ = child.kill(); - } + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = child.wait(); - std::thread::sleep(std::time::Duration::from_millis(500)); - - let _ = child.kill(); - let _ = child.wait(); - - Ok(SyncStatus { - status: "stopped".to_string(), - is_running: false, - last_sync: Some(chrono::Utc::now().to_rfc3339()), - files_synced: 0, - bytes_transferred: 0, - current_file: None, - error: None, + SyncStatus { + status: "stopped".to_string(), + is_running: false, + last_sync: Some(chrono::Utc::now().to_rfc3339()), + files_synced: 0, + bytes_transferred: 0, + current_file: None, + error: None, + } }) - } else { - Err("No sync process running".to_string()) - } } +/// Configures an rclone remote for S3-compatible storage. +/// +/// # Errors +/// +/// Returns an error if rclone configuration fails. #[tauri::command] pub fn configure_remote( - remote_name: String, - endpoint: String, - access_key: String, - secret_key: String, - bucket: String, + remote_name: &str, + endpoint: &str, + access_key: &str, + secret_key: &str, + bucket: &str, ) -> Result<(), String> { let output = Command::new("rclone") .args([ "config", "create", - &remote_name, + remote_name, "s3", "provider", "Minio", "endpoint", - &endpoint, + endpoint, "access_key_id", - &access_key, + access_key, "secret_access_key", - &secret_key, + secret_key, "acl", "private", ]) .output() - .map_err(|e| format!("Failed to configure rclone: {}", e))?; + .map_err(|e| format!("Failed to configure rclone: {e}"))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("rclone config failed: {}", stderr)); + return Err(format!("rclone config failed: {stderr}")); } let _ = Command::new("rclone") - .args(["config", "update", &remote_name, "bucket", &bucket]) + .args(["config", "update", remote_name, "bucket", bucket]) .output(); Ok(()) } +/// Checks if rclone is installed and returns its version. +/// +/// # Errors +/// +/// Returns an error if rclone is not installed or the check fails. #[tauri::command] pub fn check_rclone_installed() -> Result { let output = Command::new("rclone") @@ -241,7 +256,7 @@ pub fn check_rclone_installed() -> Result { if e.kind() == std::io::ErrorKind::NotFound { "rclone not installed".to_string() } else { - format!("Error checking rclone: {}", e) + format!("Error checking rclone: {e}") } })?; @@ -254,12 +269,17 @@ pub fn check_rclone_installed() -> Result { } } +/// Lists all configured rclone remotes. +/// +/// # Errors +/// +/// Returns an error if rclone fails to list remotes. #[tauri::command] pub fn list_remotes() -> Result, String> { let output = Command::new("rclone") .args(["listremotes"]) .output() - .map_err(|e| format!("Failed to list remotes: {}", e))?; + .map_err(|e| format!("Failed to list remotes: {e}"))?; if output.status.success() { let remotes = String::from_utf8_lossy(&output.stdout); @@ -273,19 +293,27 @@ pub fn list_remotes() -> Result, String> { } } +/// Returns the default sync folder path. #[tauri::command] +#[must_use] pub fn get_sync_folder() -> String { - dirs::home_dir() - .map(|p| p.join("GeneralBots").to_string_lossy().to_string()) - .unwrap_or_else(|| "~/GeneralBots".to_string()) + dirs::home_dir().map_or_else( + || "~/GeneralBots".to_string(), + |p| p.join("GeneralBots").to_string_lossy().to_string(), + ) } +/// Sets the sync folder path, creating it if necessary. +/// +/// # Errors +/// +/// Returns an error if the directory cannot be created or the path is not a directory. #[tauri::command] -pub fn set_sync_folder(path: String) -> Result<(), String> { - let path = PathBuf::from(&path); +pub fn set_sync_folder(path: &str) -> Result<(), String> { + let path = PathBuf::from(path); if !path.exists() { - std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {}", e))?; + std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {e}"))?; } if !path.is_dir() { @@ -295,17 +323,20 @@ pub fn set_sync_folder(path: String) -> Result<(), String> { Ok(()) } -fn monitor_sync_process(window: Window) { +fn monitor_sync_process(window: &Window) { loop { std::thread::sleep(std::time::Duration::from_secs(1)); - let mut process_guard = RCLONE_PROCESS.lock().unwrap(); + let mut process_guard = RCLONE_PROCESS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(ref mut child) = *process_guard { + let status_opt = if let Some(ref mut child) = *process_guard { match child.try_wait() { - Ok(Some(status)) => { - let success = status.success(); + Ok(Some(exit_status)) => { + let success = exit_status.success(); *process_guard = None; + drop(process_guard); let status = SyncStatus { status: if success { @@ -321,15 +352,20 @@ fn monitor_sync_process(window: Window) { error: if success { None } else { - Some(format!("Exit code: {:?}", status.code())) + Some(format!("Exit code: {:?}", exit_status.code())) }, }; - let _ = window.emit("sync_completed", &status); - break; + if success { + let _ = window.emit("sync_completed", &status); + } else { + let _ = window.emit("sync_error", &status); + } + return; } Ok(None) => { - let status = SyncStatus { + drop(process_guard); + Some(SyncStatus { status: "syncing".to_string(), is_running: true, last_sync: None, @@ -337,11 +373,11 @@ fn monitor_sync_process(window: Window) { bytes_transferred: 0, current_file: None, error: None, - }; - let _ = window.emit("sync_progress", &status); + }) } Err(e) => { *process_guard = None; + drop(process_guard); let status = SyncStatus { status: "error".to_string(), @@ -350,16 +386,19 @@ fn monitor_sync_process(window: Window) { files_synced: 0, bytes_transferred: 0, current_file: None, - error: Some(format!("Process error: {}", e)), + error: Some(format!("Process error: {e}")), }; - let _ = window.emit("sync_error", &status); - break; + return; } } } else { - break; + drop(process_guard); + return; + }; + + if let Some(status) = status_opt { + let _ = window.emit("sync_progress", &status); } } } - diff --git a/src/desktop/tray.rs b/src/desktop/tray.rs index 4e6b32e..dbca66d 100644 --- a/src/desktop/tray.rs +++ b/src/desktop/tray.rs @@ -1,31 +1,47 @@ +//! Tray manager for desktop application. +//! +//! Provides system tray functionality for different operating modes. + use anyhow::Result; use serde::Serialize; use std::sync::Arc; use tokio::sync::RwLock; -#[derive(Clone)] +/// Manages the system tray icon and its interactions. +#[derive(Clone, Debug)] pub struct TrayManager { hostname: Arc>>, running_mode: RunningMode, tray_active: Arc>, } -#[derive(Debug, Clone, Copy, PartialEq)] +/// The running mode of the application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RunningMode { + /// Full server mode with all services. Server, + /// Desktop mode with UI and local services. Desktop, + /// Client mode connecting to remote server. Client, } -#[derive(Debug, Clone)] +/// Events that can be triggered from the tray menu. +#[derive(Debug, Clone, Copy)] pub enum TrayEvent { + /// Open the main application window. Open, + /// Open settings dialog. Settings, + /// Show about dialog. About, + /// Quit the application. Quit, } impl TrayManager { + /// Creates a new tray manager with default settings. + #[must_use] pub fn new() -> Self { Self { hostname: Arc::new(RwLock::new(None)), @@ -34,6 +50,8 @@ impl TrayManager { } } + /// Creates a new tray manager with the specified running mode. + #[must_use] pub fn with_mode(mode: RunningMode) -> Self { Self { hostname: Arc::new(RwLock::new(None)), @@ -42,6 +60,11 @@ impl TrayManager { } } + /// Starts the tray manager based on the running mode. + /// + /// # Errors + /// + /// Returns an error if the tray initialization fails. pub async fn start(&self) -> Result<()> { match self.running_mode { RunningMode::Desktop => { @@ -51,7 +74,7 @@ impl TrayManager { log::info!("Running in server mode - tray icon disabled"); } RunningMode::Client => { - self.start_client_mode().await?; + self.start_client_mode().await; } } Ok(()) @@ -59,53 +82,55 @@ impl TrayManager { async fn start_desktop_mode(&self) -> Result<()> { log::info!("Starting desktop mode tray icon"); - let mut active = self.tray_active.write().await; *active = true; + drop(active); #[cfg(target_os = "linux")] - { - self.setup_linux_tray().await?; - } + self.setup_linux_tray(); #[cfg(target_os = "windows")] - { - self.setup_windows_tray().await?; - } + self.setup_windows_tray(); #[cfg(target_os = "macos")] - { - self.setup_macos_tray().await?; - } + self.setup_macos_tray(); Ok(()) } - async fn start_client_mode(&self) -> Result<()> { + async fn start_client_mode(&self) { log::info!("Starting client mode with minimal tray"); let mut active = self.tray_active.write().await; *active = true; - Ok(()) + drop(active); } #[cfg(target_os = "linux")] - async fn setup_linux_tray(&self) -> Result<()> { - log::info!("Initializing Linux system tray via DBus/StatusNotifierItem"); - Ok(()) + fn setup_linux_tray(&self) { + log::info!( + "Initializing Linux system tray via DBus/StatusNotifierItem for mode: {:?}", + self.running_mode + ); } #[cfg(target_os = "windows")] - async fn setup_windows_tray(&self) -> Result<()> { - log::info!("Initializing Windows system tray via Shell_NotifyIcon"); - Ok(()) + fn setup_windows_tray(&self) { + log::info!( + "Initializing Windows system tray via Shell_NotifyIcon for mode: {:?}", + self.running_mode + ); } #[cfg(target_os = "macos")] - async fn setup_macos_tray(&self) -> Result<()> { - log::info!("Initializing macOS menu bar via NSStatusItem"); - Ok(()) + fn setup_macos_tray(&self) { + log::info!( + "Initializing macOS menu bar via NSStatusItem for mode: {:?}", + self.running_mode + ); } + /// Returns a string representation of the current running mode. + #[must_use] pub fn get_mode_string(&self) -> String { match self.running_mode { RunningMode::Desktop => "Desktop".to_string(), @@ -114,26 +139,50 @@ impl TrayManager { } } + /// Updates the tray status message. + /// + /// # Errors + /// + /// Returns an error if the status update fails. pub async fn update_status(&self, status: &str) -> Result<()> { let active = self.tray_active.read().await; - if *active { - log::info!("Tray status: {}", status); + let is_active = *active; + drop(active); + + if is_active { + log::info!("Tray status: {status}"); } Ok(()) } + /// Sets the tray tooltip text. + /// + /// # Errors + /// + /// Returns an error if setting the tooltip fails. pub async fn set_tooltip(&self, tooltip: &str) -> Result<()> { let active = self.tray_active.read().await; - if *active { - log::debug!("Tray tooltip: {}", tooltip); + let is_active = *active; + drop(active); + + if is_active { + log::debug!("Tray tooltip: {tooltip}"); } Ok(()) } + /// Shows a desktop notification. + /// + /// # Errors + /// + /// Returns an error if showing the notification fails. pub async fn show_notification(&self, title: &str, body: &str) -> Result<()> { let active = self.tray_active.read().await; - if *active { - log::info!("Notification: {} - {}", title, body); + let is_active = *active; + drop(active); + + if is_active { + log::info!("Notification: {title} - {body}"); #[cfg(target_os = "linux")] { @@ -145,7 +194,7 @@ impl TrayManager { #[cfg(target_os = "macos")] { - let script = format!("display notification \"{}\" with title \"{}\"", body, title); + let script = format!("display notification \"{body}\" with title \"{title}\""); let _ = std::process::Command::new("osascript") .arg("-e") .arg(&script) @@ -155,41 +204,53 @@ impl TrayManager { Ok(()) } + /// Gets the current hostname. pub async fn get_hostname(&self) -> Option { let hostname = self.hostname.read().await; hostname.clone() } - pub async fn set_hostname(&self, hostname: String) { - let mut h = self.hostname.write().await; - *h = Some(hostname); + /// Sets the hostname. + pub async fn set_hostname(&self, new_hostname: String) { + let mut hostname = self.hostname.write().await; + *hostname = Some(new_hostname); } - pub async fn stop(&self) -> Result<()> { + /// Stops the tray manager. + /// + /// # Errors + /// + /// Returns an error if stopping fails. + pub async fn stop(&self) { let mut active = self.tray_active.write().await; *active = false; + drop(active); log::info!("Tray manager stopped"); - Ok(()) } + /// Returns whether the tray is currently active. pub async fn is_active(&self) -> bool { let active = self.tray_active.read().await; - *active + let result = *active; + drop(active); + result } + /// Handles a tray event and performs the appropriate action. pub fn handle_event(&self, event: TrayEvent) { + let mode = self.get_mode_string(); match event { TrayEvent::Open => { - log::info!("Tray event: Open main window"); + log::info!("Tray event: Open main window (mode: {mode})"); } TrayEvent::Settings => { - log::info!("Tray event: Open settings"); + log::info!("Tray event: Open settings (mode: {mode})"); } TrayEvent::About => { - log::info!("Tray event: Show about dialog"); + log::info!("Tray event: Show about dialog (mode: {mode})"); } TrayEvent::Quit => { - log::info!("Tray event: Quit application"); + log::info!("Tray event: Quit application (mode: {mode})"); } } } @@ -201,19 +262,28 @@ impl Default for TrayManager { } } +/// Monitors the status of services. +#[derive(Debug)] pub struct ServiceMonitor { services: Vec, } +/// Status of a monitored service. #[derive(Debug, Clone, Serialize)] pub struct ServiceStatus { + /// Service name. pub name: String, + /// Whether the service is running. pub running: bool, + /// Service port number. pub port: u16, + /// Service URL. pub url: String, } impl ServiceMonitor { + /// Creates a new service monitor with default services. + #[must_use] pub fn new() -> Self { Self { services: vec![ @@ -233,15 +303,17 @@ impl ServiceMonitor { } } + /// Adds a service to monitor. pub fn add_service(&mut self, name: &str, port: u16) { self.services.push(ServiceStatus { name: name.to_string(), running: false, port, - url: format!("http://localhost:{}", port), + url: format!("http://localhost:{port}"), }); } + /// Checks all services and returns their current status. pub async fn check_services(&mut self) -> Vec { for service in &mut self.services { service.running = Self::check_service(&service.url).await; @@ -249,39 +321,43 @@ impl ServiceMonitor { self.services.clone() } + /// Checks if a service is running at the given URL. pub async fn check_service(url: &str) -> bool { if !url.starts_with("http://") && !url.starts_with("https://") { return false; } - let client = match reqwest::Client::builder() + let Ok(client) = reqwest::Client::builder() .danger_accept_invalid_certs(true) .timeout(std::time::Duration::from_secs(2)) .build() - { - Ok(c) => c, - Err(_) => return false, + else { + return false; }; let health_url = format!("{}/health", url.trim_end_matches('/')); - match client.get(&health_url).send().await { - Ok(response) => response.status().is_success(), - Err(_) => match client.get(url).send().await { - Ok(response) => response.status().is_success(), - Err(_) => false, - }, - } + client + .get(&health_url) + .send() + .await + .is_ok_and(|response| response.status().is_success()) } + /// Gets a service by name. + #[must_use] pub fn get_service(&self, name: &str) -> Option<&ServiceStatus> { self.services.iter().find(|s| s.name == name) } + /// Returns whether all services are running. + #[must_use] pub fn all_running(&self) -> bool { self.services.iter().all(|s| s.running) } + /// Returns whether any service is running. + #[must_use] pub fn any_running(&self) -> bool { self.services.iter().any(|s| s.running) } diff --git a/src/lib.rs b/src/lib.rs index 6cf24c7..5bf1d42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,5 @@ +//! `BotApp` - Tauri desktop application for General Bots +//! +//! This crate wraps the web UI with native desktop features. + pub mod desktop; diff --git a/src/main.rs b/src/main.rs index 99060c7..1050be8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,8 @@ async fn start_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> { #[tauri::command] async fn stop_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> { - tray.stop().await.map_err(|e| e.to_string()) + tray.stop().await; + Ok(()) } #[tauri::command] @@ -70,7 +71,7 @@ fn handle_tray_event(tray: tauri::State<'_, TrayManager>, event: String) -> Resu "settings" => TrayEvent::Settings, "about" => TrayEvent::About, "quit" => TrayEvent::Quit, - _ => return Err(format!("Unknown event: {}", event)), + _ => return Err(format!("Unknown event: {event}")), }; tray.handle_event(tray_event); Ok(()) @@ -80,8 +81,10 @@ fn handle_tray_event(tray: tauri::State<'_, TrayManager>, event: String) -> Resu async fn check_services( monitor: tauri::State<'_, tokio::sync::Mutex>, ) -> Result, String> { - let mut monitor = monitor.lock().await; - Ok(monitor.check_services().await) + let mut guard = monitor.lock().await; + let result = guard.check_services().await; + drop(guard); + Ok(result) } #[tauri::command] @@ -90,8 +93,9 @@ async fn add_service( name: String, port: u16, ) -> Result<(), String> { - let mut monitor = monitor.lock().await; - monitor.add_service(&name, port); + let mut guard = monitor.lock().await; + guard.add_service(&name, port); + drop(guard); Ok(()) } @@ -100,24 +104,30 @@ async fn get_service( monitor: tauri::State<'_, tokio::sync::Mutex>, name: String, ) -> Result, String> { - let monitor = monitor.lock().await; - Ok(monitor.get_service(&name).cloned()) + let guard = monitor.lock().await; + let result = guard.get_service(&name).cloned(); + drop(guard); + Ok(result) } #[tauri::command] async fn all_services_running( monitor: tauri::State<'_, tokio::sync::Mutex>, ) -> Result { - let monitor = monitor.lock().await; - Ok(monitor.all_running()) + let guard = monitor.lock().await; + let result = guard.all_running(); + drop(guard); + Ok(result) } #[tauri::command] async fn any_service_running( monitor: tauri::State<'_, tokio::sync::Mutex>, ) -> Result { - let monitor = monitor.lock().await; - Ok(monitor.any_running()) + let guard = monitor.lock().await; + let result = guard.any_running(); + drop(guard); + Ok(result) } #[tauri::command] @@ -138,8 +148,7 @@ fn create_tray_with_mode(mode: String) -> Result { "client" => RunningMode::Client, _ => { return Err(format!( - "Invalid mode: {}. Use Server, Desktop, or Client", - mode + "Invalid mode: {mode}. Use Server, Desktop, or Client" )) } }; @@ -152,12 +161,13 @@ fn main() { .format_timestamp_millis() .init(); - info!("BotApp {} starting...", env!("CARGO_PKG_VERSION")); + let version = env!("CARGO_PKG_VERSION"); + info!("BotApp {version} starting..."); let tray_manager = TrayManager::with_mode(RunningMode::Desktop); let service_monitor = tokio::sync::Mutex::new(ServiceMonitor::new()); - tauri::Builder::default() + let builder_result = tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .manage(tray_manager) @@ -197,20 +207,29 @@ fn main() { .setup(|app| { let tray = app.state::(); let mode = tray.get_mode_string(); - info!("BotApp setup complete in {} mode", mode); + info!("BotApp setup complete in {mode} mode"); let tray_clone = tray.inner().clone(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + log::error!("Failed to create runtime: {e}"); + return; + } + }; rt.block_on(async { if let Err(e) = tray_clone.start().await { - log::error!("Failed to start tray: {}", e); + log::error!("Failed to start tray: {e}"); } }); }); Ok(()) }) - .run(tauri::generate_context!()) - .expect("Failed to run BotApp"); + .run(tauri::generate_context!()); + + if let Err(e) = builder_result { + log::error!("Failed to run BotApp: {e}"); + } }