diff --git a/tauri/src-tauri/build.rs b/tauri/src-tauri/build.rs index 82d481cc..d860e1e6 100644 --- a/tauri/src-tauri/build.rs +++ b/tauri/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { tauri_build::build() -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/api_testing.rs b/tauri/src-tauri/src/api_testing.rs index d221e997..281ef764 100644 --- a/tauri/src-tauri/src/api_testing.rs +++ b/tauri/src-tauri/src/api_testing.rs @@ -1,10 +1,10 @@ -use serde::{Serialize, Deserialize}; -use std::sync::Arc; -use tokio::sync::RwLock; -use std::collections::HashMap; use chrono::{DateTime, Utc}; use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::RwLock; /// API test method #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] @@ -36,11 +36,22 @@ impl HttpMethod { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AssertionType { StatusCode(u16), - StatusRange { min: u16, max: u16 }, - ResponseTime { max_ms: u64 }, + StatusRange { + min: u16, + max: u16, + }, + ResponseTime { + max_ms: u64, + }, HeaderExists(String), - HeaderEquals { key: String, value: String }, - JsonPath { path: String, expected: serde_json::Value }, + HeaderEquals { + key: String, + value: String, + }, + JsonPath { + path: String, + expected: serde_json::Value, + }, BodyContains(String), BodyMatches(String), // Regex ContentType(String), @@ -78,9 +89,16 @@ pub enum APITestBody { /// API test authentication #[derive(Debug, Clone, Serialize, Deserialize)] pub enum APITestAuth { - Basic { username: String, password: String }, + Basic { + username: String, + password: String, + }, Bearer(String), - ApiKey { key: String, value: String, in_header: bool }, + ApiKey { + key: String, + value: String, + in_header: bool, + }, Custom(HashMap), } @@ -205,7 +223,10 @@ impl APITestingManager { } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } @@ -221,7 +242,10 @@ impl APITestingManager { /// Add test suite pub async fn add_test_suite(&self, suite: APITestSuite) { - self.test_suites.write().await.insert(suite.id.clone(), suite); + self.test_suites + .write() + .await + .insert(suite.id.clone(), suite); } /// Get test suite @@ -235,7 +259,11 @@ impl APITestingManager { } /// Run single test - pub async fn run_test(&self, test: &APITest, variables: &HashMap) -> APITestResult { + pub async fn run_test( + &self, + test: &APITest, + variables: &HashMap, + ) -> APITestResult { let start_time = std::time::Instant::now(); let mut result = APITestResult { test_id: test.id.clone(), @@ -274,9 +302,11 @@ impl APITestingManager { result.retries_used = retry; // Run assertions - result.assertion_results = self.run_assertions(&test.assertions, status, &result.response_headers, &body).await; + result.assertion_results = self + .run_assertions(&test.assertions, status, &result.response_headers, &body) + .await; result.success = result.assertion_results.iter().all(|a| a.passed); - + break; } Err(e) => { @@ -298,7 +328,7 @@ impl APITestingManager { let suite = self.get_test_suite(suite_id).await?; let run_id = uuid::Uuid::new_v4().to_string(); let start_time = std::time::Instant::now(); - + // Merge variables let mut variables = self.shared_variables.read().await.clone(); variables.extend(suite.variables.clone()); @@ -323,10 +353,10 @@ impl APITestingManager { let test = test.clone(); let vars = variables.clone(); let manager = self.clone_for_parallel(); - - tasks.push(tokio::spawn(async move { - manager.run_test(&test, &vars).await - })); + + tasks.push(tokio::spawn( + async move { manager.run_test(&test, &vars).await }, + )); } for task in tasks { @@ -371,11 +401,10 @@ impl APITestingManager { // Send notification if let Some(notification_manager) = &self.notification_manager { - let message = format!( - "Test suite completed: {} passed, {} failed", - passed, failed - ); - let _ = notification_manager.notify_success("API Tests", &message).await; + let message = format!("Test suite completed: {} passed, {} failed", passed, failed); + let _ = notification_manager + .notify_success("API Tests", &message) + .await; } Some(history_entry) @@ -403,9 +432,11 @@ impl APITestingManager { /// Export test suite pub async fn export_test_suite(&self, suite_id: &str) -> Result { - let suite = self.get_test_suite(suite_id).await + let suite = self + .get_test_suite(suite_id) + .await .ok_or_else(|| "Test suite not found".to_string())?; - + serde_json::to_string_pretty(&suite) .map_err(|e| format!("Failed to serialize test suite: {}", e)) } @@ -469,7 +500,7 @@ impl APITestingManager { // Execute request let response = request.send().await.map_err(|e| e.to_string())?; let status = response.status().as_u16(); - + let mut headers = HashMap::new(); for (key, value) in response.headers() { if let Ok(value_str) = value.to_str() { @@ -493,42 +524,39 @@ impl APITestingManager { for assertion in assertions { let result = match assertion { - AssertionType::StatusCode(expected) => { - AssertionResult { - assertion: assertion.clone(), - passed: status == *expected, - actual_value: Some(status.to_string()), - error_message: if status != *expected { - Some(format!("Expected status {}, got {}", expected, status)) - } else { - None - }, - } - } - AssertionType::StatusRange { min, max } => { - AssertionResult { - assertion: assertion.clone(), - passed: status >= *min && status <= *max, - actual_value: Some(status.to_string()), - error_message: if status < *min || status > *max { - Some(format!("Expected status between {} and {}, got {}", min, max, status)) - } else { - None - }, - } - } - AssertionType::HeaderExists(key) => { - AssertionResult { - assertion: assertion.clone(), - passed: headers.contains_key(key), - actual_value: None, - error_message: if !headers.contains_key(key) { - Some(format!("Header '{}' not found", key)) - } else { - None - }, - } - } + AssertionType::StatusCode(expected) => AssertionResult { + assertion: assertion.clone(), + passed: status == *expected, + actual_value: Some(status.to_string()), + error_message: if status != *expected { + Some(format!("Expected status {}, got {}", expected, status)) + } else { + None + }, + }, + AssertionType::StatusRange { min, max } => AssertionResult { + assertion: assertion.clone(), + passed: status >= *min && status <= *max, + actual_value: Some(status.to_string()), + error_message: if status < *min || status > *max { + Some(format!( + "Expected status between {} and {}, got {}", + min, max, status + )) + } else { + None + }, + }, + AssertionType::HeaderExists(key) => AssertionResult { + assertion: assertion.clone(), + passed: headers.contains_key(key), + actual_value: None, + error_message: if !headers.contains_key(key) { + Some(format!("Header '{}' not found", key)) + } else { + None + }, + }, AssertionType::HeaderEquals { key, value } => { let actual = headers.get(key); AssertionResult { @@ -536,25 +564,29 @@ impl APITestingManager { passed: actual == Some(value), actual_value: actual.cloned(), error_message: if actual != Some(value) { - Some(format!("Header '{}' expected '{}', got '{:?}'", key, value, actual)) + Some(format!( + "Header '{}' expected '{}', got '{:?}'", + key, value, actual + )) } else { None }, } } - AssertionType::BodyContains(text) => { - AssertionResult { - assertion: assertion.clone(), - passed: body.contains(text), - actual_value: None, - error_message: if !body.contains(text) { - Some(format!("Body does not contain '{}'", text)) - } else { - None - }, - } - } - AssertionType::JsonPath { path: _, expected: _ } => { + AssertionType::BodyContains(text) => AssertionResult { + assertion: assertion.clone(), + passed: body.contains(text), + actual_value: None, + error_message: if !body.contains(text) { + Some(format!("Body does not contain '{}'", text)) + } else { + None + }, + }, + AssertionType::JsonPath { + path: _, + expected: _, + } => { // TODO: Implement JSON path assertion AssertionResult { assertion: assertion.clone(), @@ -563,14 +595,12 @@ impl APITestingManager { error_message: Some("JSON path assertions not yet implemented".to_string()), } } - _ => { - AssertionResult { - assertion: assertion.clone(), - passed: false, - actual_value: None, - error_message: Some("Assertion type not implemented".to_string()), - } - } + _ => AssertionResult { + assertion: assertion.clone(), + passed: false, + actual_value: None, + error_message: Some("Assertion type not implemented".to_string()), + }, }; results.push(result); } @@ -602,7 +632,11 @@ impl APITestingManager { let token = self.replace_variables(token, variables); request.bearer_auth(token) } - APITestAuth::ApiKey { key, value, in_header } => { + APITestAuth::ApiKey { + key, + value, + in_header, + } => { let key = self.replace_variables(key, variables); let value = self.replace_variables(value, variables); if *in_header { @@ -645,4 +679,4 @@ pub struct APITestStatistics { pub average_duration_ms: f64, pub most_failed_tests: Vec<(String, usize)>, pub slowest_tests: Vec<(String, u64)>, -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/app_mover.rs b/tauri/src-tauri/src/app_mover.rs index e9747ece..e8bbcc35 100644 --- a/tauri/src-tauri/src/app_mover.rs +++ b/tauri/src-tauri/src/app_mover.rs @@ -1,19 +1,18 @@ -use tauri::AppHandle; use std::path::PathBuf; +use tauri::AppHandle; /// Check if the app should be moved to Applications folder /// This is a macOS-specific feature #[cfg(target_os = "macos")] -pub async fn check_and_prompt_move(app_handle: AppHandle) -> Result<(), String> { - +pub async fn check_and_prompt_move(_app_handle: AppHandle) -> Result<(), String> { // Get current app bundle path let bundle_path = get_app_bundle_path()?; - + // Check if already in Applications folder if is_in_applications_folder(&bundle_path) { return Ok(()); } - + // Check if we've already asked this question let settings = crate::settings::Settings::load().unwrap_or_default(); if let Some(asked) = settings.general.show_welcome_on_startup { @@ -22,23 +21,24 @@ pub async fn check_and_prompt_move(app_handle: AppHandle) -> Result<(), String> return Ok(()); } } - + // For now, just log and return // TODO: Implement dialog using tauri-plugin-dialog tracing::info!("App should be moved to Applications folder"); - - if false { // Temporarily disabled until dialog is implemented + + if false { + // Temporarily disabled until dialog is implemented move_to_applications_folder(bundle_path)?; - + // Restart the app from the new location restart_from_applications()?; } - + // Update settings to not ask again let mut settings = crate::settings::Settings::load().unwrap_or_default(); settings.general.show_welcome_on_startup = Some(false); settings.save().ok(); - + Ok(()) } @@ -51,27 +51,28 @@ pub async fn check_and_prompt_move(_app_handle: AppHandle) -> Result<(), String> #[cfg(target_os = "macos")] fn get_app_bundle_path() -> Result { use std::env; - + // Get the executable path - let exe_path = env::current_exe() - .map_err(|e| format!("Failed to get executable path: {}", e))?; - + let exe_path = + env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))?; + // Navigate up to the .app bundle // Typical structure: /path/to/VibeTunnel.app/Contents/MacOS/VibeTunnel let mut bundle_path = exe_path; - + // Go up three levels to reach the .app bundle for _ in 0..3 { - bundle_path = bundle_path.parent() + bundle_path = bundle_path + .parent() .ok_or("Failed to find app bundle")? .to_path_buf(); } - + // Verify this is an .app bundle if !bundle_path.to_string_lossy().ends_with(".app") { return Err("Not running from an app bundle".to_string()); } - + Ok(bundle_path) } @@ -83,25 +84,26 @@ fn is_in_applications_folder(bundle_path: &PathBuf) -> bool { #[cfg(target_os = "macos")] fn move_to_applications_folder(bundle_path: PathBuf) -> Result<(), String> { - use std::process::Command; use std::fs; - - let app_name = bundle_path.file_name() + use std::process::Command; + + let app_name = bundle_path + .file_name() .ok_or("Failed to get app name")? .to_string_lossy(); - + let dest_path = PathBuf::from("/Applications").join(app_name.as_ref()); - + // Check if destination already exists if dest_path.exists() { // For now, just remove the existing app // TODO: Implement dialog using tauri-plugin-dialog - + // Remove existing app fs::remove_dir_all(&dest_path) .map_err(|e| format!("Failed to remove existing app: {}", e))?; } - + // Use AppleScript to move the app with proper permissions let script = format!( r#"tell application "Finder" @@ -109,32 +111,32 @@ fn move_to_applications_folder(bundle_path: PathBuf) -> Result<(), String> { end tell"#, bundle_path.to_string_lossy() ); - + let output = Command::new("osascript") .arg("-e") .arg(script) .output() .map_err(|e| format!("Failed to execute move command: {}", e))?; - + if !output.status.success() { let error = String::from_utf8_lossy(&output.stderr); return Err(format!("Failed to move app: {}", error)); } - + Ok(()) } #[cfg(target_os = "macos")] fn restart_from_applications() -> Result<(), String> { use std::process::Command; - + // Launch the app from the Applications folder let _output = Command::new("open") .arg("-n") .arg("/Applications/VibeTunnel.app") .spawn() .map_err(|e| format!("Failed to restart app: {}", e))?; - + // Exit the current instance std::process::exit(0); } @@ -151,10 +153,10 @@ pub async fn is_in_applications_folder_command() -> Result { let bundle_path = get_app_bundle_path()?; Ok(is_in_applications_folder(&bundle_path)) } - + #[cfg(not(target_os = "macos"))] { // Always return true on non-macOS platforms Ok(true) } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/auth.rs b/tauri/src-tauri/src/auth.rs index d4d9c3e3..93aaf60c 100644 --- a/tauri/src-tauri/src/auth.rs +++ b/tauri/src-tauri/src/auth.rs @@ -5,7 +5,7 @@ use axum::{ response::Response, Json, }; -use base64::{Engine as _, engine::general_purpose}; +use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -108,4 +108,4 @@ pub async fn login( message: "No password configured".to_string(), })) } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/auth_cache.rs b/tauri/src-tauri/src/auth_cache.rs index 8f25be25..e403fe66 100644 --- a/tauri/src-tauri/src/auth_cache.rs +++ b/tauri/src-tauri/src/auth_cache.rs @@ -1,9 +1,9 @@ -use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -use std::collections::HashMap; -use chrono::{DateTime, Utc, Duration}; -use sha2::{Sha256, Digest}; /// Authentication token type #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -104,7 +104,7 @@ impl Default for AuthCacheConfig { Self { enabled: true, max_entries: 1000, - default_ttl_seconds: 3600, // 1 hour + default_ttl_seconds: 3600, // 1 hour refresh_threshold_seconds: 300, // 5 minutes persist_to_disk: false, encryption_enabled: true, @@ -126,7 +126,11 @@ pub struct AuthCacheStats { } /// Token refresh callback -pub type TokenRefreshCallback = Arc futures::future::BoxFuture<'static, Result> + Send + Sync>; +pub type TokenRefreshCallback = Arc< + dyn Fn(CachedToken) -> futures::future::BoxFuture<'static, Result> + + Send + + Sync, +>; /// Authentication cache manager pub struct AuthCacheManager { @@ -168,7 +172,10 @@ impl AuthCacheManager { } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } @@ -244,12 +251,13 @@ impl AuthCacheManager { // Check if needs refresh if token.needs_refresh(config.refresh_threshold_seconds) { // Trigger refresh in background - if let Some(refresh_callback) = self.refresh_callbacks.read().await.get(key) { + if let Some(refresh_callback) = self.refresh_callbacks.read().await.get(key) + { let token_clone = token.clone(); let callback = refresh_callback.clone(); let key_clone = key.to_string(); let manager = self.clone_for_refresh(); - + tokio::spawn(async move { if let Ok(refreshed_token) = callback(token_clone).await { let _ = manager.store_token(&key_clone, refreshed_token).await; @@ -269,7 +277,11 @@ impl AuthCacheManager { } /// Store credential in cache - pub async fn store_credential(&self, key: &str, credential: AuthCredential) -> Result<(), String> { + pub async fn store_credential( + &self, + key: &str, + credential: AuthCredential, + ) -> Result<(), String> { let config = self.config.read().await; if !config.enabled { return Ok(()); @@ -303,7 +315,7 @@ impl AuthCacheManager { } let mut cache = self.cache.write().await; - + if let Some(entry) = cache.get_mut(key) { entry.last_accessed = Utc::now(); entry.access_count += 1; @@ -315,7 +327,10 @@ impl AuthCacheManager { /// Register token refresh callback pub async fn register_refresh_callback(&self, key: &str, callback: TokenRefreshCallback) { - self.refresh_callbacks.write().await.insert(key.to_string(), callback); + self.refresh_callbacks + .write() + .await + .insert(key.to_string(), callback); } /// Clear specific cache entry @@ -330,7 +345,7 @@ impl AuthCacheManager { pub async fn clear_all(&self) { let mut cache = self.cache.write().await; cache.clear(); - + let mut stats = self.stats.write().await; stats.total_entries = 0; stats.total_tokens = 0; @@ -344,7 +359,9 @@ impl AuthCacheManager { /// List all cache entries pub async fn list_entries(&self) -> Vec<(String, DateTime, u64)> { - self.cache.read().await + self.cache + .read() + .await .values() .map(|entry| (entry.key.clone(), entry.last_accessed, entry.access_count)) .collect() @@ -354,7 +371,7 @@ impl AuthCacheManager { pub async fn export_cache(&self) -> Result { let cache = self.cache.read().await; let entries: Vec<_> = cache.values().cloned().collect(); - + serde_json::to_string_pretty(&entries) .map_err(|e| format!("Failed to serialize cache: {}", e)) } @@ -363,19 +380,17 @@ impl AuthCacheManager { pub async fn import_cache(&self, json_data: &str) -> Result<(), String> { let entries: Vec = serde_json::from_str(json_data) .map_err(|e| format!("Failed to deserialize cache: {}", e))?; - + let mut cache = self.cache.write().await; let mut stats = self.stats.write().await; - + for entry in entries { cache.insert(entry.key.clone(), entry); } - + stats.total_entries = cache.len(); - stats.total_tokens = cache.values() - .map(|e| e.tokens.len()) - .sum(); - + stats.total_tokens = cache.values().map(|e| e.tokens.len()).sum(); + Ok(()) } @@ -388,14 +403,20 @@ impl AuthCacheManager { // Helper methods fn token_matches_scope(&self, token: &CachedToken, scope: &AuthScope) -> bool { - token.scope.service == scope.service && - token.scope.resource == scope.resource && - scope.permissions.iter().all(|p| token.scope.permissions.contains(p)) + token.scope.service == scope.service + && token.scope.resource == scope.resource + && scope + .permissions + .iter() + .all(|p| token.scope.permissions.contains(p)) } - fn evict_oldest_entry(&self, cache: &mut HashMap, stats: &mut AuthCacheStats) { - if let Some((key, _)) = cache.iter() - .min_by_key(|(_, entry)| entry.last_accessed) { + fn evict_oldest_entry( + &self, + cache: &mut HashMap, + stats: &mut AuthCacheStats, + ) { + if let Some((key, _)) = cache.iter().min_by_key(|(_, entry)| entry.last_accessed) { let key = key.clone(); cache.remove(&key); stats.eviction_count += 1; @@ -410,7 +431,7 @@ impl AuthCacheManager { loop { tokio::time::sleep(cleanup_interval.to_std().unwrap()).await; - + let config = self.config.read().await; if !config.enabled { continue; @@ -429,9 +450,7 @@ impl AuthCacheManager { } stats.expired_tokens += total_expired; - stats.total_tokens = cache.values() - .map(|e| e.tokens.len()) - .sum(); + stats.total_tokens = cache.values().map(|e| e.tokens.len()).sum(); // Remove empty entries cache.retain(|_, entry| !entry.tokens.is_empty() || entry.credential.is_some()); @@ -480,4 +499,4 @@ pub struct AuthCacheError { pub code: String, pub message: String, pub details: Option>, -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/auto_launch.rs b/tauri/src-tauri/src/auto_launch.rs index 738c23e8..511f0dfc 100644 --- a/tauri/src-tauri/src/auto_launch.rs +++ b/tauri/src-tauri/src/auto_launch.rs @@ -1,10 +1,10 @@ +use crate::state::AppState; use auto_launch::AutoLaunchBuilder; use tauri::State; -use crate::state::AppState; fn get_app_path() -> String { let exe_path = std::env::current_exe().unwrap(); - + // On macOS, we need to use the .app bundle path, not the executable inside it #[cfg(target_os = "macos")] { @@ -20,7 +20,7 @@ fn get_app_path() -> String { } } } - + // For other platforms or if we couldn't find the .app bundle, use the executable path exe_path.to_string_lossy().to_string() } @@ -32,10 +32,10 @@ pub fn enable_auto_launch() -> Result<(), String> { .set_args(&["--auto-launch"]) .build() .map_err(|e| format!("Failed to build auto-launch: {}", e))?; - + auto.enable() .map_err(|e| format!("Failed to enable auto-launch: {}", e))?; - + Ok(()) } @@ -45,10 +45,10 @@ pub fn disable_auto_launch() -> Result<(), String> { .set_app_path(&get_app_path()) .build() .map_err(|e| format!("Failed to build auto-launch: {}", e))?; - + auto.disable() .map_err(|e| format!("Failed to disable auto-launch: {}", e))?; - + Ok(()) } @@ -58,16 +58,13 @@ pub fn is_auto_launch_enabled() -> Result { .set_app_path(&get_app_path()) .build() .map_err(|e| format!("Failed to build auto-launch: {}", e))?; - + auto.is_enabled() .map_err(|e| format!("Failed to check auto-launch status: {}", e)) } #[tauri::command] -pub async fn set_auto_launch( - enabled: bool, - _state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn set_auto_launch(enabled: bool, _state: State<'_, AppState>) -> Result<(), String> { if enabled { enable_auto_launch() } else { @@ -76,8 +73,6 @@ pub async fn set_auto_launch( } #[tauri::command] -pub async fn get_auto_launch( - _state: State<'_, AppState>, -) -> Result { +pub async fn get_auto_launch(_state: State<'_, AppState>) -> Result { is_auto_launch_enabled() -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/backend_manager.rs b/tauri/src-tauri/src/backend_manager.rs index d7ad669a..e8b22efd 100644 --- a/tauri/src-tauri/src/backend_manager.rs +++ b/tauri/src-tauri/src/backend_manager.rs @@ -1,10 +1,10 @@ -use serde::{Serialize, Deserialize}; -use std::sync::Arc; -use tokio::sync::RwLock; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; -use chrono::{DateTime, Utc}; +use std::sync::Arc; use tokio::process::Command; +use tokio::sync::RwLock; /// Backend type enumeration #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -26,7 +26,7 @@ impl BackendType { BackendType::Custom => "custom", } } - + pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "rust" => BackendType::Rust, @@ -141,7 +141,7 @@ impl BackendManager { active_backend: Arc::new(RwLock::new(Some(BackendType::Rust))), notification_manager: None, }; - + // Initialize default backend configurations tokio::spawn({ let configs = manager.configs.clone(); @@ -150,118 +150,130 @@ impl BackendManager { *configs.write().await = default_configs; } }); - + manager } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } /// Initialize default backend configurations fn initialize_default_configs() -> HashMap { let mut configs = HashMap::new(); - + // Rust backend (built-in) - configs.insert(BackendType::Rust, BackendConfig { - backend_type: BackendType::Rust, - name: "Rust (Built-in)".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - executable_path: None, - working_directory: None, - environment_variables: HashMap::new(), - arguments: vec![], - port: Some(4020), - features: BackendFeatures { - terminal_sessions: true, - file_browser: true, - port_forwarding: true, - authentication: true, - websocket_support: true, - rest_api: true, - graphql_api: false, - metrics: true, + configs.insert( + BackendType::Rust, + BackendConfig { + backend_type: BackendType::Rust, + name: "Rust (Built-in)".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + executable_path: None, + working_directory: None, + environment_variables: HashMap::new(), + arguments: vec![], + port: Some(4020), + features: BackendFeatures { + terminal_sessions: true, + file_browser: true, + port_forwarding: true, + authentication: true, + websocket_support: true, + rest_api: true, + graphql_api: false, + metrics: true, + }, + requirements: BackendRequirements { + runtime: None, + runtime_version: None, + dependencies: vec![], + system_packages: vec![], + min_memory_mb: Some(64), + min_disk_space_mb: Some(10), + }, }, - requirements: BackendRequirements { - runtime: None, - runtime_version: None, - dependencies: vec![], - system_packages: vec![], - min_memory_mb: Some(64), - min_disk_space_mb: Some(10), - }, - }); - + ); + // Node.js backend - configs.insert(BackendType::NodeJS, BackendConfig { - backend_type: BackendType::NodeJS, - name: "Node.js Server".to_string(), - version: "1.0.0".to_string(), - executable_path: Some(PathBuf::from("node")), - working_directory: None, - environment_variables: HashMap::new(), - arguments: vec!["server.js".to_string()], - port: Some(4021), - features: BackendFeatures { - terminal_sessions: true, - file_browser: true, - port_forwarding: false, - authentication: true, - websocket_support: true, - rest_api: true, - graphql_api: true, - metrics: false, + configs.insert( + BackendType::NodeJS, + BackendConfig { + backend_type: BackendType::NodeJS, + name: "Node.js Server".to_string(), + version: "1.0.0".to_string(), + executable_path: Some(PathBuf::from("node")), + working_directory: None, + environment_variables: HashMap::new(), + arguments: vec!["server.js".to_string()], + port: Some(4021), + features: BackendFeatures { + terminal_sessions: true, + file_browser: true, + port_forwarding: false, + authentication: true, + websocket_support: true, + rest_api: true, + graphql_api: true, + metrics: false, + }, + requirements: BackendRequirements { + runtime: Some("node".to_string()), + runtime_version: Some(">=16.0.0".to_string()), + dependencies: vec![ + "express".to_string(), + "socket.io".to_string(), + "node-pty".to_string(), + ], + system_packages: vec![], + min_memory_mb: Some(128), + min_disk_space_mb: Some(50), + }, }, - requirements: BackendRequirements { - runtime: Some("node".to_string()), - runtime_version: Some(">=16.0.0".to_string()), - dependencies: vec![ - "express".to_string(), - "socket.io".to_string(), - "node-pty".to_string(), - ], - system_packages: vec![], - min_memory_mb: Some(128), - min_disk_space_mb: Some(50), - }, - }); - + ); + // Python backend - configs.insert(BackendType::Python, BackendConfig { - backend_type: BackendType::Python, - name: "Python Server".to_string(), - version: "1.0.0".to_string(), - executable_path: Some(PathBuf::from("python3")), - working_directory: None, - environment_variables: HashMap::new(), - arguments: vec!["-m".to_string(), "vibetunnel_server".to_string()], - port: Some(4022), - features: BackendFeatures { - terminal_sessions: true, - file_browser: true, - port_forwarding: false, - authentication: true, - websocket_support: true, - rest_api: true, - graphql_api: false, - metrics: true, + configs.insert( + BackendType::Python, + BackendConfig { + backend_type: BackendType::Python, + name: "Python Server".to_string(), + version: "1.0.0".to_string(), + executable_path: Some(PathBuf::from("python3")), + working_directory: None, + environment_variables: HashMap::new(), + arguments: vec!["-m".to_string(), "vibetunnel_server".to_string()], + port: Some(4022), + features: BackendFeatures { + terminal_sessions: true, + file_browser: true, + port_forwarding: false, + authentication: true, + websocket_support: true, + rest_api: true, + graphql_api: false, + metrics: true, + }, + requirements: BackendRequirements { + runtime: Some("python3".to_string()), + runtime_version: Some(">=3.8".to_string()), + dependencies: vec![ + "fastapi".to_string(), + "uvicorn".to_string(), + "websockets".to_string(), + "ptyprocess".to_string(), + ], + system_packages: vec![], + min_memory_mb: Some(96), + min_disk_space_mb: Some(30), + }, }, - requirements: BackendRequirements { - runtime: Some("python3".to_string()), - runtime_version: Some(">=3.8".to_string()), - dependencies: vec![ - "fastapi".to_string(), - "uvicorn".to_string(), - "websockets".to_string(), - "ptyprocess".to_string(), - ], - system_packages: vec![], - min_memory_mb: Some(96), - min_disk_space_mb: Some(30), - }, - }); - + ); + configs } @@ -303,14 +315,16 @@ impl BackendManager { if !self.is_backend_installed(backend_type).await { return Err(format!("{:?} backend is not installed", backend_type)); } - + // Get backend configuration - let config = self.get_backend_config(backend_type).await + let config = self + .get_backend_config(backend_type) + .await .ok_or_else(|| "Backend configuration not found".to_string())?; - + // Generate instance ID let instance_id = uuid::Uuid::new_v4().to_string(); - + // Create backend instance let instance = BackendInstance { id: instance_id.clone(), @@ -330,15 +344,19 @@ impl BackendManager { active_connections: 0, }, }; - + // Store instance - self.instances.write().await.insert(instance_id.clone(), instance); - + self.instances + .write() + .await + .insert(instance_id.clone(), instance); + // Start backend process match backend_type { BackendType::Rust => { // Rust backend is handled internally - self.update_instance_status(&instance_id, BackendStatus::Running).await; + self.update_instance_status(&instance_id, BackendStatus::Running) + .await; *self.active_backend.write().await = Some(BackendType::Rust); Ok(instance_id) } @@ -351,15 +369,19 @@ impl BackendManager { /// Stop backend pub async fn stop_backend(&self, instance_id: &str) -> Result<(), String> { - let instance = self.instances.read().await + let instance = self + .instances + .read() + .await .get(instance_id) .cloned() .ok_or_else(|| "Backend instance not found".to_string())?; - + match instance.backend_type { BackendType::Rust => { // Rust backend is handled internally - self.update_instance_status(instance_id, BackendStatus::Stopped).await; + self.update_instance_status(instance_id, BackendStatus::Stopped) + .await; Ok(()) } _ => { @@ -378,8 +400,12 @@ impl BackendManager { // Find and stop current backend instances let instance_id = { let instances = self.instances.read().await; - instances.iter() - .find(|(_, instance)| instance.backend_type == current && instance.status == BackendStatus::Running) + instances + .iter() + .find(|(_, instance)| { + instance.backend_type == current + && instance.status == BackendStatus::Running + }) .map(|(id, _)| id.clone()) }; if let Some(id) = instance_id { @@ -387,21 +413,23 @@ impl BackendManager { } } } - + // Start new backend self.start_backend(backend_type).await?; - + // Update active backend *self.active_backend.write().await = Some(backend_type); - + // Notify about backend switch if let Some(notification_manager) = &self.notification_manager { - let _ = notification_manager.notify_success( - "Backend Switched", - &format!("Switched to {:?} backend", backend_type) - ).await; + let _ = notification_manager + .notify_success( + "Backend Switched", + &format!("Switched to {:?} backend", backend_type), + ) + .await; } - + Ok(()) } @@ -417,27 +445,30 @@ impl BackendManager { /// Get backend health pub async fn check_backend_health(&self, instance_id: &str) -> Result { - let instance = self.instances.read().await + let instance = self + .instances + .read() + .await .get(instance_id) .cloned() .ok_or_else(|| "Backend instance not found".to_string())?; - + if instance.status != BackendStatus::Running { return Ok(HealthStatus::Unknown); } - + // Perform health check based on backend type let health_status = match instance.backend_type { BackendType::Rust => HealthStatus::Healthy, // Always healthy for built-in _ => self.check_external_backend_health(&instance).await?, }; - + // Update instance health status if let Some(instance) = self.instances.write().await.get_mut(instance_id) { instance.health_status = health_status; instance.last_health_check = Some(Utc::now()); } - + Ok(health_status) } @@ -487,7 +518,11 @@ impl BackendManager { Err("Python backend installation not yet implemented".to_string()) } - async fn start_external_backend(&self, _instance_id: &str, _config: BackendConfig) -> Result { + async fn start_external_backend( + &self, + _instance_id: &str, + _config: BackendConfig, + ) -> Result { // TODO: Implement external backend startup Err("External backend startup not yet implemented".to_string()) } @@ -497,7 +532,10 @@ impl BackendManager { Err("External backend shutdown not yet implemented".to_string()) } - async fn check_external_backend_health(&self, _instance: &BackendInstance) -> Result { + async fn check_external_backend_health( + &self, + _instance: &BackendInstance, + ) -> Result { // TODO: Implement health check for external backends Ok(HealthStatus::Unknown) } @@ -520,4 +558,4 @@ pub struct BackendStats { pub running_instances: usize, pub active_backend: Option, pub health_summary: HashMap, -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/cast.rs b/tauri/src-tauri/src/cast.rs index c7a770f2..64e0fda7 100644 --- a/tauri/src-tauri/src/cast.rs +++ b/tauri/src-tauri/src/cast.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; @@ -5,7 +6,6 @@ use std::io::{BufWriter, Write}; use std::path::Path; use std::sync::Arc; use tokio::sync::Mutex; -use chrono::{DateTime, Utc}; /// Asciinema cast v2 format header #[derive(Debug, Clone, Serialize, Deserialize)] @@ -62,12 +62,7 @@ pub struct CastRecorder { impl CastRecorder { /// Create a new cast recorder - pub fn new( - width: u16, - height: u16, - title: Option, - command: Option, - ) -> Self { + pub fn new(width: u16, height: u16, title: Option, command: Option) -> Self { let now = Utc::now(); let header = CastHeader { version: 2, @@ -114,7 +109,8 @@ impl CastRecorder { self.write_event_to_file(&mut writer, event)?; } - writer.flush() + writer + .flush() .map_err(|e| format!("Failed to flush writer: {}", e))?; self.file_writer = Some(Arc::new(Mutex::new(writer))); @@ -131,7 +127,8 @@ impl CastRecorder { if let Some(writer_arc) = self.file_writer.take() { let mut writer = writer_arc.lock().await; - writer.flush() + writer + .flush() .map_err(|e| format!("Failed to flush final data: {}", e))?; } @@ -153,7 +150,8 @@ impl CastRecorder { async fn add_event(&self, event_type: EventType, data: &[u8]) -> Result<(), String> { let timestamp = Utc::now() .signed_duration_since(self.start_time) - .num_milliseconds() as f64 / 1000.0; + .num_milliseconds() as f64 + / 1000.0; // Convert data to string (handling potential UTF-8 errors) let data_string = String::from_utf8_lossy(data).to_string(); @@ -168,7 +166,8 @@ impl CastRecorder { if let Some(writer_arc) = &self.file_writer { let mut writer = writer_arc.lock().await; self.write_event_to_file(&mut writer, &event)?; - writer.flush() + writer + .flush() .map_err(|e| format!("Failed to flush event: {}", e))?; } @@ -186,14 +185,10 @@ impl CastRecorder { event: &CastEvent, ) -> Result<(), String> { // Format: [timestamp, event_type, data] - let event_array = serde_json::json!([ - event.timestamp, - event.event_type.as_str(), - event.data - ]); + let event_array = + serde_json::json!([event.timestamp, event.event_type.as_str(), event.data]); - writeln!(writer, "{}", event_array) - .map_err(|e| format!("Failed to write event: {}", e))?; + writeln!(writer, "{}", event_array).map_err(|e| format!("Failed to write event: {}", e))?; Ok(()) } @@ -207,7 +202,7 @@ impl CastRecorder { // Calculate duration let events = self.events.lock().await; let duration = events.last().map(|e| e.timestamp); - + // Update header with duration let mut header = self.header.clone(); header.duration = duration; @@ -223,7 +218,8 @@ impl CastRecorder { self.write_event_to_file(&mut writer, event)?; } - writer.flush() + writer + .flush() .map_err(|e| format!("Failed to flush file: {}", e))?; Ok(()) @@ -361,4 +357,4 @@ impl CastManager { false } } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/cli_installer.rs b/tauri/src-tauri/src/cli_installer.rs index 9a7f290e..4ada1f26 100644 --- a/tauri/src-tauri/src/cli_installer.rs +++ b/tauri/src-tauri/src/cli_installer.rs @@ -1,8 +1,8 @@ +use serde::Serialize; use std::fs; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; -use serde::Serialize; const CLI_SCRIPT: &str = r#"#!/bin/bash # VibeTunnel CLI wrapper @@ -21,6 +21,7 @@ fi "$APP_PATH/Contents/MacOS/VibeTunnel" --cli "$@" "#; +#[cfg(target_os = "windows")] const WINDOWS_CLI_SCRIPT: &str = r#"@echo off :: VibeTunnel CLI wrapper for Windows @@ -36,6 +37,7 @@ if not exist "%APP_PATH%" ( "%APP_PATH%" --cli %* "#; +#[cfg(target_os = "linux")] const LINUX_CLI_SCRIPT: &str = r#"#!/bin/bash # VibeTunnel CLI wrapper for Linux @@ -67,12 +69,12 @@ pub fn install_cli_tool() -> Result { { install_cli_macos() } - + #[cfg(target_os = "windows")] { install_cli_windows() } - + #[cfg(target_os = "linux")] { install_cli_linux() @@ -82,18 +84,22 @@ pub fn install_cli_tool() -> Result { #[cfg(target_os = "macos")] fn install_cli_macos() -> Result { let cli_path = PathBuf::from("/usr/local/bin/vt"); - + // Check if /usr/local/bin exists, create if not let bin_dir = cli_path.parent().unwrap(); if !bin_dir.exists() { - fs::create_dir_all(bin_dir) - .map_err(|e| format!("Failed to create /usr/local/bin: {}. Try running with sudo.", e))?; + fs::create_dir_all(bin_dir).map_err(|e| { + format!( + "Failed to create /usr/local/bin: {}. Try running with sudo.", + e + ) + })?; } - + // Write the CLI script fs::write(&cli_path, CLI_SCRIPT) .map_err(|e| format!("Failed to write CLI script: {}. Try running with sudo.", e))?; - + // Make it executable #[cfg(unix)] { @@ -104,7 +110,7 @@ fn install_cli_macos() -> Result { fs::set_permissions(&cli_path, perms) .map_err(|e| format!("Failed to set permissions: {}", e))?; } - + Ok(CliInstallResult { installed: true, path: cli_path.to_string_lossy().to_string(), @@ -114,50 +120,51 @@ fn install_cli_macos() -> Result { #[cfg(target_os = "windows")] fn install_cli_windows() -> Result { - let user_path = std::env::var("USERPROFILE") - .map_err(|_| "Failed to get user profile path")?; - + let user_path = std::env::var("USERPROFILE").map_err(|_| "Failed to get user profile path")?; + let cli_dir = PathBuf::from(&user_path).join(".vibetunnel"); let cli_path = cli_dir.join("vt.cmd"); - + // Create directory if it doesn't exist if !cli_dir.exists() { fs::create_dir_all(&cli_dir) .map_err(|e| format!("Failed to create CLI directory: {}", e))?; } - + // Write the CLI script fs::write(&cli_path, WINDOWS_CLI_SCRIPT) .map_err(|e| format!("Failed to write CLI script: {}", e))?; - + // Add to PATH if not already there add_to_windows_path(&cli_dir)?; - + Ok(CliInstallResult { installed: true, path: cli_path.to_string_lossy().to_string(), - message: format!("CLI tool installed successfully at {}. Restart your terminal to use 'vt' command.", cli_path.display()), + message: format!( + "CLI tool installed successfully at {}. Restart your terminal to use 'vt' command.", + cli_path.display() + ), }) } #[cfg(target_os = "linux")] fn install_cli_linux() -> Result { - let home_dir = std::env::var("HOME") - .map_err(|_| "Failed to get home directory")?; - + let home_dir = std::env::var("HOME").map_err(|_| "Failed to get home directory")?; + let local_bin = PathBuf::from(&home_dir).join(".local").join("bin"); let cli_path = local_bin.join("vt"); - + // Create ~/.local/bin if it doesn't exist if !local_bin.exists() { fs::create_dir_all(&local_bin) .map_err(|e| format!("Failed to create ~/.local/bin: {}", e))?; } - + // Write the CLI script fs::write(&cli_path, LINUX_CLI_SCRIPT) .map_err(|e| format!("Failed to write CLI script: {}", e))?; - + // Make it executable #[cfg(unix)] { @@ -168,11 +175,14 @@ fn install_cli_linux() -> Result { fs::set_permissions(&cli_path, perms) .map_err(|e| format!("Failed to set permissions: {}", e))?; } - + Ok(CliInstallResult { installed: true, path: cli_path.to_string_lossy().to_string(), - message: format!("CLI tool installed successfully at {}. Make sure ~/.local/bin is in your PATH.", cli_path.display()), + message: format!( + "CLI tool installed successfully at {}. Make sure ~/.local/bin is in your PATH.", + cli_path.display() + ), }) } @@ -183,26 +193,27 @@ fn add_to_windows_path(dir: &Path) -> Result<(), String> { use winreg::enums::*; use winreg::RegKey; let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE) + let env = hkcu + .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE) .map_err(|e| format!("Failed to open registry key: {}", e))?; - + let path: String = env.get_value("Path").unwrap_or_default(); let dir_str = dir.to_string_lossy(); - + if !path.contains(&*dir_str) { let new_path = if path.is_empty() { dir_str.to_string() } else { format!("{};{}", path, dir_str) }; - + env.set_value("Path", &new_path) .map_err(|e| format!("Failed to update PATH: {}", e))?; } - + Ok(()) } - + #[cfg(not(windows))] { Ok(()) @@ -217,43 +228,43 @@ pub fn uninstall_cli_tool() -> Result { fs::remove_file(&cli_path) .map_err(|e| format!("Failed to remove CLI tool: {}. Try running with sudo.", e))?; } - + Ok(CliInstallResult { installed: false, path: cli_path.to_string_lossy().to_string(), message: "CLI tool uninstalled successfully".to_string(), }) } - + #[cfg(target_os = "windows")] { - let user_path = std::env::var("USERPROFILE") - .map_err(|_| "Failed to get user profile path")?; + let user_path = + std::env::var("USERPROFILE").map_err(|_| "Failed to get user profile path")?; let cli_path = PathBuf::from(&user_path).join(".vibetunnel").join("vt.cmd"); - + if cli_path.exists() { - fs::remove_file(&cli_path) - .map_err(|e| format!("Failed to remove CLI tool: {}", e))?; + fs::remove_file(&cli_path).map_err(|e| format!("Failed to remove CLI tool: {}", e))?; } - + Ok(CliInstallResult { installed: false, path: cli_path.to_string_lossy().to_string(), message: "CLI tool uninstalled successfully".to_string(), }) } - + #[cfg(target_os = "linux")] { - let home_dir = std::env::var("HOME") - .map_err(|_| "Failed to get home directory")?; - let cli_path = PathBuf::from(&home_dir).join(".local").join("bin").join("vt"); - + let home_dir = std::env::var("HOME").map_err(|_| "Failed to get home directory")?; + let cli_path = PathBuf::from(&home_dir) + .join(".local") + .join("bin") + .join("vt"); + if cli_path.exists() { - fs::remove_file(&cli_path) - .map_err(|e| format!("Failed to remove CLI tool: {}", e))?; + fs::remove_file(&cli_path).map_err(|e| format!("Failed to remove CLI tool: {}", e))?; } - + Ok(CliInstallResult { installed: false, path: cli_path.to_string_lossy().to_string(), @@ -267,20 +278,27 @@ pub fn is_cli_installed() -> bool { { PathBuf::from("/usr/local/bin/vt").exists() } - + #[cfg(target_os = "windows")] { if let Ok(user_path) = std::env::var("USERPROFILE") { - PathBuf::from(&user_path).join(".vibetunnel").join("vt.cmd").exists() + PathBuf::from(&user_path) + .join(".vibetunnel") + .join("vt.cmd") + .exists() } else { false } } - + #[cfg(target_os = "linux")] { if let Ok(home_dir) = std::env::var("HOME") { - PathBuf::from(&home_dir).join(".local").join("bin").join("vt").exists() + PathBuf::from(&home_dir) + .join(".local") + .join("bin") + .join("vt") + .exists() } else { false } @@ -300,4 +318,4 @@ pub fn uninstall_cli() -> Result { #[tauri::command] pub fn check_cli_installed() -> Result { Ok(is_cli_installed()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/commands.rs b/tauri/src-tauri/src/commands.rs index d8b56125..b1abf977 100644 --- a/tauri/src-tauri/src/commands.rs +++ b/tauri/src-tauri/src/commands.rs @@ -1,8 +1,8 @@ +use crate::server::HttpServer; +use crate::state::AppState; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tauri::{Manager, State}; -use crate::server::HttpServer; -use crate::state::AppState; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Terminal { @@ -37,30 +37,27 @@ pub async fn create_terminal( state: State<'_, AppState>, ) -> Result { let terminal_manager = &state.terminal_manager; - - terminal_manager.create_session( - options.name.unwrap_or_else(|| "Terminal".to_string()), - options.rows.unwrap_or(24), - options.cols.unwrap_or(80), - options.cwd, - options.env, - options.shell, - ).await + + terminal_manager + .create_session( + options.name.unwrap_or_else(|| "Terminal".to_string()), + options.rows.unwrap_or(24), + options.cols.unwrap_or(80), + options.cwd, + options.env, + options.shell, + ) + .await } #[tauri::command] -pub async fn list_terminals( - state: State<'_, AppState>, -) -> Result, String> { +pub async fn list_terminals(state: State<'_, AppState>) -> Result, String> { let terminal_manager = &state.terminal_manager; Ok(terminal_manager.list_sessions().await) } #[tauri::command] -pub async fn close_terminal( - id: String, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn close_terminal(id: String, state: State<'_, AppState>) -> Result<(), String> { let terminal_manager = &state.terminal_manager; terminal_manager.close_session(&id).await } @@ -87,63 +84,70 @@ pub async fn write_to_terminal( } #[tauri::command] -pub async fn read_from_terminal( - id: String, - state: State<'_, AppState>, -) -> Result, String> { +pub async fn read_from_terminal(id: String, state: State<'_, AppState>) -> Result, String> { let terminal_manager = &state.terminal_manager; terminal_manager.read_from_session(&id).await } #[tauri::command] -pub async fn start_server( - state: State<'_, AppState>, -) -> Result { +pub async fn start_server(state: State<'_, AppState>) -> Result { let mut server = state.http_server.write().await; - + if let Some(http_server) = server.as_ref() { // Get actual port from running server let port = http_server.port(); - + // Check if ngrok is active let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() { ngrok_tunnel.url } else { format!("http://localhost:{}", port) }; - + return Ok(ServerStatus { running: true, port, url, }); } - + // Load settings to check if password is enabled let settings = crate::settings::Settings::load().unwrap_or_default(); - + // Start HTTP server with auth if configured - let mut http_server = if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() { - let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password)); - HttpServer::with_auth(state.terminal_manager.clone(), state.session_monitor.clone(), auth_config) - } else { - HttpServer::new(state.terminal_manager.clone(), state.session_monitor.clone()) - }; - + let mut http_server = + if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() { + let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password)); + HttpServer::with_auth( + state.terminal_manager.clone(), + state.session_monitor.clone(), + auth_config, + ) + } else { + HttpServer::new( + state.terminal_manager.clone(), + state.session_monitor.clone(), + ) + }; + // Start server with appropriate access mode let (port, url) = match settings.dashboard.access_mode.as_str() { "network" => { let port = http_server.start_with_mode("network").await?; (port, format!("http://0.0.0.0:{}", port)) - }, + } "ngrok" => { // For ngrok mode, start in localhost and let ngrok handle the tunneling let port = http_server.start_with_mode("localhost").await?; - + // Try to start ngrok tunnel if auth token is configured let url = if let Some(auth_token) = settings.advanced.ngrok_auth_token { if !auth_token.is_empty() { - match state.ngrok_manager.start_tunnel(port, Some(auth_token)).await { + match state + .ngrok_manager + .start_tunnel(port, Some(auth_token)) + .await + { Ok(tunnel) => tunnel.url, Err(e) => { tracing::error!("Failed to start ngrok tunnel: {}", e); @@ -156,17 +160,17 @@ pub async fn start_server( } else { return Err("Ngrok auth token is required for ngrok access mode".to_string()); }; - + (port, url) - }, + } _ => { let port = http_server.start_with_mode("localhost").await?; (port, format!("http://localhost:{}", port)) } }; - + *server = Some(http_server); - + Ok(ServerStatus { running: true, port, @@ -175,30 +179,26 @@ pub async fn start_server( } #[tauri::command] -pub async fn stop_server( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn stop_server(state: State<'_, AppState>) -> Result<(), String> { let mut server = state.http_server.write().await; - + if let Some(mut http_server) = server.take() { http_server.stop().await?; } - + // Also stop ngrok tunnel if active let _ = state.ngrok_manager.stop_tunnel().await; - + Ok(()) } #[tauri::command] -pub async fn get_server_status( - state: State<'_, AppState>, -) -> Result { +pub async fn get_server_status(state: State<'_, AppState>) -> Result { let server = state.http_server.read().await; - + if let Some(http_server) = server.as_ref() { let port = http_server.port(); - + // Check if ngrok is active and return its URL let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() { ngrok_tunnel.url @@ -210,7 +210,7 @@ pub async fn get_server_status( _ => format!("http://localhost:{}", port), } }; - + Ok(ServerStatus { running: true, port, @@ -231,39 +231,37 @@ pub fn get_app_version() -> String { } #[tauri::command] -pub async fn restart_server( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn restart_server(state: State<'_, AppState>) -> Result<(), String> { // Stop the current server let mut server = state.http_server.write().await; - + if let Some(mut http_server) = server.take() { http_server.stop().await?; } - + // Wait a moment tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - + // Start a new server let terminal_manager = state.terminal_manager.clone(); let session_monitor = state.session_monitor.clone(); let settings = crate::settings::Settings::load().unwrap_or_default(); - + let mut new_server = HttpServer::new(terminal_manager, session_monitor); - new_server.start_with_mode(match settings.dashboard.access_mode.as_str() { - "network" => "network", - _ => "localhost" - }).await?; - + new_server + .start_with_mode(match settings.dashboard.access_mode.as_str() { + "network" => "network", + _ => "localhost", + }) + .await?; + *server = Some(new_server); - + Ok(()) } #[tauri::command] -pub async fn show_server_console( - app_handle: tauri::AppHandle, -) -> Result<(), String> { +pub async fn show_server_console(app_handle: tauri::AppHandle) -> Result<(), String> { // Check if server console window already exists if let Some(window) = app_handle.get_webview_window("server-console") { window.show().map_err(|e| e.to_string())?; @@ -273,7 +271,7 @@ pub async fn show_server_console( tauri::WebviewWindowBuilder::new( &app_handle, "server-console", - tauri::WebviewUrl::App("server-console.html".into()) + tauri::WebviewUrl::App("server-console.html".into()), ) .title("Server Console - VibeTunnel") .inner_size(900.0, 600.0) @@ -283,14 +281,12 @@ pub async fn show_server_console( .build() .map_err(|e| e.to_string())?; } - + Ok(()) } #[tauri::command] -pub async fn show_welcome_screen( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn show_welcome_screen(state: State<'_, AppState>) -> Result<(), String> { let welcome_manager = &state.welcome_manager; welcome_manager.show_welcome_window().await } @@ -303,13 +299,13 @@ pub async fn purge_all_settings( // Create default settings and save to clear the file let default_settings = crate::settings::Settings::default(); default_settings.save().map_err(|e| e.to_string())?; - + // Quit the app after a short delay tokio::spawn(async move { tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; app_handle.exit(0); }); - + Ok(()) } @@ -318,8 +314,11 @@ pub async fn update_dock_icon_visibility(app_handle: tauri::AppHandle) -> Result #[cfg(target_os = "macos")] { let settings = crate::settings::Settings::load().unwrap_or_default(); - let has_visible_windows = app_handle.windows().values().any(|w| w.is_visible().unwrap_or(false)); - + let has_visible_windows = app_handle + .windows() + .values() + .any(|w| w.is_visible().unwrap_or(false)); + if has_visible_windows { // Always show dock icon when windows are visible let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular); @@ -354,32 +353,40 @@ pub async fn start_terminal_recording( state: State<'_, AppState>, ) -> Result<(), String> { let cast_manager = &state.cast_manager; - + // Get terminal info for metadata let terminal_manager = &state.terminal_manager; let sessions = terminal_manager.list_sessions().await; - let session = sessions.iter() + let session = sessions + .iter() .find(|s| s.id == options.session_id) .ok_or_else(|| "Session not found".to_string())?; - + // Create recorder if it doesn't exist - cast_manager.create_recorder( - options.session_id.clone(), - session.cols, - session.rows, - options.title.or(Some(session.name.clone())), - None, // command - ).await.ok(); // Ignore if already exists - + cast_manager + .create_recorder( + options.session_id.clone(), + session.cols, + session.rows, + options.title.or(Some(session.name.clone())), + None, // command + ) + .await + .ok(); // Ignore if already exists + // Start recording if let Some(path) = options.output_path { - cast_manager.start_recording(&options.session_id, path).await + cast_manager + .start_recording(&options.session_id, path) + .await } else { // Use default path with timestamp let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); let filename = format!("vibetunnel_recording_{}.cast", timestamp); let path = std::env::temp_dir().join(filename); - cast_manager.start_recording(&options.session_id, path).await + cast_manager + .start_recording(&options.session_id, path) + .await } } @@ -409,14 +416,14 @@ pub async fn get_recording_status( ) -> Result { let cast_manager = &state.cast_manager; let is_recording = cast_manager.is_recording(&session_id).await; - + let duration = if let Some(recorder) = cast_manager.get_recorder(&session_id).await { let rec = recorder.lock().await; rec.get_duration().await } else { 0.0 }; - + Ok(RecordingStatus { is_recording, duration, @@ -448,42 +455,39 @@ pub async fn start_tty_forward( state: State<'_, AppState>, ) -> Result { let tty_forward_manager = &state.tty_forward_manager; - - let remote_host = options.remote_host.unwrap_or_else(|| "localhost".to_string()); + + let remote_host = options + .remote_host + .unwrap_or_else(|| "localhost".to_string()); let remote_port = options.remote_port.unwrap_or(22); - - tty_forward_manager.start_forward( - options.local_port, - remote_host, - remote_port, - options.shell, - ).await + + tty_forward_manager + .start_forward(options.local_port, remote_host, remote_port, options.shell) + .await } #[tauri::command] -pub async fn stop_tty_forward( - id: String, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn stop_tty_forward(id: String, state: State<'_, AppState>) -> Result<(), String> { let tty_forward_manager = &state.tty_forward_manager; tty_forward_manager.stop_forward(&id).await } #[tauri::command] -pub async fn list_tty_forwards( - state: State<'_, AppState>, -) -> Result, String> { +pub async fn list_tty_forwards(state: State<'_, AppState>) -> Result, String> { let tty_forward_manager = &state.tty_forward_manager; let forwards = tty_forward_manager.list_forwards().await; - - Ok(forwards.into_iter().map(|f| TTYForwardInfo { - id: f.id, - local_port: f.local_port, - remote_host: f.remote_host, - remote_port: f.remote_port, - connected: f.connected, - client_count: f.client_count, - }).collect()) + + Ok(forwards + .into_iter() + .map(|f| TTYForwardInfo { + id: f.id, + local_port: f.local_port, + remote_host: f.remote_host, + remote_port: f.remote_port, + connected: f.connected, + client_count: f.client_count, + }) + .collect()) } #[tauri::command] @@ -492,15 +496,18 @@ pub async fn get_tty_forward( state: State<'_, AppState>, ) -> Result, String> { let tty_forward_manager = &state.tty_forward_manager; - - Ok(tty_forward_manager.get_forward(&id).await.map(|f| TTYForwardInfo { - id: f.id, - local_port: f.local_port, - remote_host: f.remote_host, - remote_port: f.remote_port, - connected: f.connected, - client_count: f.client_count, - })) + + Ok(tty_forward_manager + .get_forward(&id) + .await + .map(|f| TTYForwardInfo { + id: f.id, + local_port: f.local_port, + remote_host: f.remote_host, + remote_port: f.remote_port, + connected: f.connected, + client_count: f.client_count, + })) } // Session Monitoring Commands @@ -521,9 +528,7 @@ pub async fn get_monitored_sessions( } #[tauri::command] -pub async fn start_session_monitoring( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn start_session_monitoring(state: State<'_, AppState>) -> Result<(), String> { let session_monitor = &state.session_monitor; session_monitor.start_monitoring().await; Ok(()) @@ -531,9 +536,7 @@ pub async fn start_session_monitoring( // Port Conflict Resolution Commands #[tauri::command] -pub async fn check_port_availability( - port: u16, -) -> Result { +pub async fn check_port_availability(port: u16) -> Result { Ok(crate::port_conflict::PortConflictResolver::is_port_available(port).await) } @@ -559,23 +562,22 @@ pub async fn force_kill_process( } #[tauri::command] -pub async fn find_available_ports( - near_port: u16, - count: usize, -) -> Result, String> { +pub async fn find_available_ports(near_port: u16, count: usize) -> Result, String> { let mut available_ports = Vec::new(); let start = near_port.saturating_sub(10).max(1024); let end = near_port.saturating_add(100).min(65535); - + for port in start..=end { - if port != near_port && crate::port_conflict::PortConflictResolver::is_port_available(port).await { + if port != near_port + && crate::port_conflict::PortConflictResolver::is_port_available(port).await + { available_ports.push(port); if available_ports.len() >= count { break; } } } - + Ok(available_ports) } @@ -591,7 +593,8 @@ pub async fn get_all_ip_addresses() -> Result, String> { } #[tauri::command] -pub async fn get_network_interfaces() -> Result, String> { +pub async fn get_network_interfaces() -> Result, String> +{ Ok(crate::network_utils::NetworkUtils::get_all_interfaces()) } @@ -601,10 +604,7 @@ pub async fn get_hostname() -> Result, String> { } #[tauri::command] -pub async fn test_network_connectivity( - host: String, - port: u16, -) -> Result { +pub async fn test_network_connectivity(host: String, port: u16) -> Result { Ok(crate::network_utils::NetworkUtils::test_connectivity(&host, port).await) } @@ -631,14 +631,16 @@ pub async fn show_notification( state: State<'_, AppState>, ) -> Result { let notification_manager = &state.notification_manager; - notification_manager.show_notification( - options.notification_type, - options.priority, - options.title, - options.body, - options.actions, - options.metadata, - ).await + notification_manager + .show_notification( + options.notification_type, + options.priority, + options.title, + options.body, + options.actions, + options.metadata, + ) + .await } #[tauri::command] @@ -668,9 +670,7 @@ pub async fn mark_notification_as_read( } #[tauri::command] -pub async fn mark_all_notifications_as_read( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn mark_all_notifications_as_read(state: State<'_, AppState>) -> Result<(), String> { let notification_manager = &state.notification_manager; notification_manager.mark_all_as_read().await } @@ -681,21 +681,19 @@ pub async fn clear_notification( state: State<'_, AppState>, ) -> Result<(), String> { let notification_manager = &state.notification_manager; - notification_manager.clear_notification(¬ification_id).await + notification_manager + .clear_notification(¬ification_id) + .await } #[tauri::command] -pub async fn clear_all_notifications( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn clear_all_notifications(state: State<'_, AppState>) -> Result<(), String> { let notification_manager = &state.notification_manager; notification_manager.clear_all_notifications().await } #[tauri::command] -pub async fn get_unread_notification_count( - state: State<'_, AppState>, -) -> Result { +pub async fn get_unread_notification_count(state: State<'_, AppState>) -> Result { let notification_manager = &state.notification_manager; Ok(notification_manager.get_unread_count().await) } @@ -728,9 +726,7 @@ pub async fn get_welcome_state( } #[tauri::command] -pub async fn should_show_welcome( - state: State<'_, AppState>, -) -> Result { +pub async fn should_show_welcome(state: State<'_, AppState>) -> Result { let welcome_manager = &state.welcome_manager; Ok(welcome_manager.should_show_welcome().await) } @@ -762,17 +758,13 @@ pub async fn complete_tutorial_step( } #[tauri::command] -pub async fn skip_tutorial( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn skip_tutorial(state: State<'_, AppState>) -> Result<(), String> { let welcome_manager = &state.welcome_manager; welcome_manager.skip_tutorial().await } #[tauri::command] -pub async fn reset_tutorial( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn reset_tutorial(state: State<'_, AppState>) -> Result<(), String> { let welcome_manager = &state.welcome_manager; welcome_manager.reset_tutorial().await } @@ -786,9 +778,7 @@ pub async fn get_tutorial_progress( } #[tauri::command] -pub async fn show_welcome_window( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn show_welcome_window(state: State<'_, AppState>) -> Result<(), String> { let welcome_manager = &state.welcome_manager; welcome_manager.show_welcome_window().await } @@ -797,16 +787,18 @@ pub async fn show_welcome_window( #[tauri::command] pub async fn get_recording_settings() -> Result { let settings = crate::settings::Settings::load().unwrap_or_default(); - Ok(settings.recording.unwrap_or(crate::settings::RecordingSettings { - enabled: true, - output_directory: None, - format: "asciinema".to_string(), - include_timing: true, - compress_output: false, - max_file_size_mb: Some(100), - auto_save: false, - filename_template: Some("vibetunnel_%Y%m%d_%H%M%S".to_string()), - })) + Ok(settings + .recording + .unwrap_or(crate::settings::RecordingSettings { + enabled: true, + output_directory: None, + format: "asciinema".to_string(), + include_timing: true, + compress_output: false, + max_file_size_mb: Some(100), + auto_save: false, + filename_template: Some("vibetunnel_%Y%m%d_%H%M%S".to_string()), + })) } #[tauri::command] @@ -822,29 +814,49 @@ pub async fn save_recording_settings( pub async fn get_all_advanced_settings() -> Result, String> { let settings = crate::settings::Settings::load().unwrap_or_default(); let mut all_settings = HashMap::new(); - + // Convert all settings sections to JSON values - all_settings.insert("recording".to_string(), - serde_json::to_value(&settings.recording).unwrap_or(serde_json::Value::Null)); - all_settings.insert("tty_forward".to_string(), - serde_json::to_value(&settings.tty_forward).unwrap_or(serde_json::Value::Null)); - all_settings.insert("monitoring".to_string(), - serde_json::to_value(&settings.monitoring).unwrap_or(serde_json::Value::Null)); - all_settings.insert("network".to_string(), - serde_json::to_value(&settings.network).unwrap_or(serde_json::Value::Null)); - all_settings.insert("port".to_string(), - serde_json::to_value(&settings.port).unwrap_or(serde_json::Value::Null)); - all_settings.insert("notifications".to_string(), - serde_json::to_value(&settings.notifications).unwrap_or(serde_json::Value::Null)); - all_settings.insert("terminal_integrations".to_string(), - serde_json::to_value(&settings.terminal_integrations).unwrap_or(serde_json::Value::Null)); - all_settings.insert("updates".to_string(), - serde_json::to_value(&settings.updates).unwrap_or(serde_json::Value::Null)); - all_settings.insert("security".to_string(), - serde_json::to_value(&settings.security).unwrap_or(serde_json::Value::Null)); - all_settings.insert("debug".to_string(), - serde_json::to_value(&settings.debug).unwrap_or(serde_json::Value::Null)); - + all_settings.insert( + "recording".to_string(), + serde_json::to_value(&settings.recording).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "tty_forward".to_string(), + serde_json::to_value(&settings.tty_forward).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "monitoring".to_string(), + serde_json::to_value(&settings.monitoring).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "network".to_string(), + serde_json::to_value(&settings.network).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "port".to_string(), + serde_json::to_value(&settings.port).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "notifications".to_string(), + serde_json::to_value(&settings.notifications).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "terminal_integrations".to_string(), + serde_json::to_value(&settings.terminal_integrations).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "updates".to_string(), + serde_json::to_value(&settings.updates).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "security".to_string(), + serde_json::to_value(&settings.security).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "debug".to_string(), + serde_json::to_value(&settings.debug).unwrap_or(serde_json::Value::Null), + ); + Ok(all_settings) } @@ -854,7 +866,7 @@ pub async fn update_advanced_settings( value: serde_json::Value, ) -> Result<(), String> { let mut settings = crate::settings::Settings::load().unwrap_or_default(); - + match section.as_str() { "recording" => { settings.recording = serde_json::from_value(value) @@ -898,7 +910,7 @@ pub async fn update_advanced_settings( } _ => return Err(format!("Unknown settings section: {}", section)), } - + settings.save() } @@ -906,7 +918,7 @@ pub async fn update_advanced_settings( pub async fn reset_settings_section(section: String) -> Result<(), String> { let mut settings = crate::settings::Settings::load().unwrap_or_default(); let defaults = crate::settings::Settings::default(); - + match section.as_str() { "recording" => settings.recording = defaults.recording, "tty_forward" => settings.tty_forward = defaults.tty_forward, @@ -921,21 +933,20 @@ pub async fn reset_settings_section(section: String) -> Result<(), String> { "all" => settings = defaults, _ => return Err(format!("Unknown settings section: {}", section)), } - + settings.save() } #[tauri::command] pub async fn export_settings() -> Result { let settings = crate::settings::Settings::load().unwrap_or_default(); - toml::to_string_pretty(&settings) - .map_err(|e| format!("Failed to export settings: {}", e)) + toml::to_string_pretty(&settings).map_err(|e| format!("Failed to export settings: {}", e)) } #[tauri::command] pub async fn import_settings(toml_content: String) -> Result<(), String> { - let settings: crate::settings::Settings = toml::from_str(&toml_content) - .map_err(|e| format!("Failed to parse settings: {}", e))?; + let settings: crate::settings::Settings = + toml::from_str(&toml_content).map_err(|e| format!("Failed to parse settings: {}", e))?; settings.save() } @@ -963,7 +974,9 @@ pub async fn request_permission( state: State<'_, AppState>, ) -> Result { let permissions_manager = &state.permissions_manager; - permissions_manager.request_permission(permission_type).await + permissions_manager + .request_permission(permission_type) + .await } #[tauri::command] @@ -972,7 +985,9 @@ pub async fn get_permission_info( state: State<'_, AppState>, ) -> Result, String> { let permissions_manager = &state.permissions_manager; - Ok(permissions_manager.get_permission_info(permission_type).await) + Ok(permissions_manager + .get_permission_info(permission_type) + .await) } #[tauri::command] @@ -1000,9 +1015,7 @@ pub async fn get_missing_required_permissions( } #[tauri::command] -pub async fn all_required_permissions_granted( - state: State<'_, AppState>, -) -> Result { +pub async fn all_required_permissions_granted(state: State<'_, AppState>) -> Result { let permissions_manager = &state.permissions_manager; Ok(permissions_manager.all_required_permissions_granted().await) } @@ -1013,7 +1026,9 @@ pub async fn open_system_permission_settings( state: State<'_, AppState>, ) -> Result<(), String> { let permissions_manager = &state.permissions_manager; - permissions_manager.open_system_settings(permission_type).await + permissions_manager + .open_system_settings(permission_type) + .await } #[tauri::command] @@ -1022,16 +1037,25 @@ pub async fn get_permission_stats( ) -> Result { let permissions_manager = &state.permissions_manager; let all_permissions = permissions_manager.get_all_permissions().await; - + let stats = crate::permissions::PermissionStats { total_permissions: all_permissions.len(), - granted_permissions: all_permissions.iter().filter(|p| p.status == crate::permissions::PermissionStatus::Granted).count(), - denied_permissions: all_permissions.iter().filter(|p| p.status == crate::permissions::PermissionStatus::Denied).count(), + granted_permissions: all_permissions + .iter() + .filter(|p| p.status == crate::permissions::PermissionStatus::Granted) + .count(), + denied_permissions: all_permissions + .iter() + .filter(|p| p.status == crate::permissions::PermissionStatus::Denied) + .count(), required_permissions: all_permissions.iter().filter(|p| p.required).count(), - missing_required: all_permissions.iter().filter(|p| p.required && p.status != crate::permissions::PermissionStatus::Granted).count(), + missing_required: all_permissions + .iter() + .filter(|p| p.required && p.status != crate::permissions::PermissionStatus::Granted) + .count(), platform: std::env::consts::OS.to_string(), }; - + Ok(stats) } @@ -1045,25 +1069,19 @@ pub async fn check_for_updates( } #[tauri::command] -pub async fn download_update( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn download_update(state: State<'_, AppState>) -> Result<(), String> { let update_manager = &state.update_manager; update_manager.download_update().await } #[tauri::command] -pub async fn install_update( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn install_update(state: State<'_, AppState>) -> Result<(), String> { let update_manager = &state.update_manager; update_manager.install_update().await } #[tauri::command] -pub async fn cancel_update( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn cancel_update(state: State<'_, AppState>) -> Result<(), String> { let update_manager = &state.update_manager; update_manager.cancel_update().await } @@ -1157,10 +1175,7 @@ pub async fn start_backend( } #[tauri::command] -pub async fn stop_backend( - instance_id: String, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn stop_backend(instance_id: String, state: State<'_, AppState>) -> Result<(), String> { let backend_manager = &state.backend_manager; backend_manager.stop_backend(&instance_id).await } @@ -1204,27 +1219,33 @@ pub async fn get_backend_stats( state: State<'_, AppState>, ) -> Result { let backend_manager = &state.backend_manager; - + let backends = backend_manager.get_available_backends().await; let instances = backend_manager.get_backend_instances().await; let active_backend = backend_manager.get_active_backend().await; - + let mut health_summary = std::collections::HashMap::new(); for instance in &instances { *health_summary.entry(instance.health_status).or_insert(0) += 1; } - + let mut installed_count = 0; for backend in &backends { - if backend_manager.is_backend_installed(backend.backend_type).await { + if backend_manager + .is_backend_installed(backend.backend_type) + .await + { installed_count += 1; } } - + Ok(crate::backend_manager::BackendStats { total_backends: backends.len(), installed_backends: installed_count, - running_instances: instances.iter().filter(|i| i.status == crate::backend_manager::BackendStatus::Running).count(), + running_instances: instances + .iter() + .filter(|i| i.status == crate::backend_manager::BackendStatus::Running) + .count(), active_backend, health_summary, }) @@ -1263,12 +1284,14 @@ pub async fn log_debug_message( state: State<'_, AppState>, ) -> Result<(), String> { let debug_features_manager = &state.debug_features_manager; - debug_features_manager.log( - options.level, - &options.component, - &options.message, - options.metadata, - ).await; + debug_features_manager + .log( + options.level, + &options.component, + &options.message, + options.metadata, + ) + .await; Ok(()) } @@ -1281,7 +1304,9 @@ pub async fn record_performance_metric( state: State<'_, AppState>, ) -> Result<(), String> { let debug_features_manager = &state.debug_features_manager; - debug_features_manager.record_metric(&name, value, &unit, tags).await; + debug_features_manager + .record_metric(&name, value, &unit, tags) + .await; Ok(()) } @@ -1414,9 +1439,7 @@ pub async fn get_api_test_history( } #[tauri::command] -pub async fn clear_api_test_history( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn clear_api_test_history(state: State<'_, AppState>) -> Result<(), String> { let api_testing_manager = &state.api_testing_manager; api_testing_manager.clear_test_history().await; Ok(()) @@ -1428,7 +1451,9 @@ pub async fn import_postman_collection( state: State<'_, AppState>, ) -> Result { let api_testing_manager = &state.api_testing_manager; - api_testing_manager.import_postman_collection(&json_data).await + api_testing_manager + .import_postman_collection(&json_data) + .await } #[tauri::command] @@ -1458,19 +1483,14 @@ pub async fn generate_diagnostic_report( } #[tauri::command] -pub async fn clear_debug_data( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn clear_debug_data(state: State<'_, AppState>) -> Result<(), String> { let debug_features_manager = &state.debug_features_manager; debug_features_manager.clear_all_data().await; Ok(()) } #[tauri::command] -pub async fn set_debug_mode( - enabled: bool, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn set_debug_mode(enabled: bool, state: State<'_, AppState>) -> Result<(), String> { let debug_features_manager = &state.debug_features_manager; debug_features_manager.set_debug_mode(enabled).await; Ok(()) @@ -1481,18 +1501,18 @@ pub async fn get_debug_stats( state: State<'_, AppState>, ) -> Result { let debug_features_manager = &state.debug_features_manager; - + let logs = debug_features_manager.get_logs(None, None).await; let mut logs_by_level = HashMap::new(); for log in &logs { let level = format!("{:?}", log.level); *logs_by_level.entry(level).or_insert(0) += 1; } - + let metrics = debug_features_manager.get_performance_metrics(None).await; let snapshots = debug_features_manager.get_memory_snapshots(None).await; let requests = debug_features_manager.get_network_requests(None).await; - + Ok(crate::debug_features::DebugStats { total_logs: logs.len(), logs_by_level, @@ -1500,7 +1520,7 @@ pub async fn get_debug_stats( total_snapshots: snapshots.len(), total_requests: requests.len(), total_test_results: 0, // TODO: Track test results - total_benchmarks: 0, // TODO: Track benchmarks + total_benchmarks: 0, // TODO: Track benchmarks }) } @@ -1547,7 +1567,9 @@ pub async fn store_auth_token( state: State<'_, AppState>, ) -> Result<(), String> { let auth_cache_manager = &state.auth_cache_manager; - auth_cache_manager.store_token(&options.key, options.token).await + auth_cache_manager + .store_token(&options.key, options.token) + .await } #[tauri::command] @@ -1556,7 +1578,9 @@ pub async fn get_auth_token( state: State<'_, AppState>, ) -> Result, String> { let auth_cache_manager = &state.auth_cache_manager; - Ok(auth_cache_manager.get_token(&options.key, &options.scope).await) + Ok(auth_cache_manager + .get_token(&options.key, &options.scope) + .await) } #[tauri::command] @@ -1565,7 +1589,9 @@ pub async fn store_auth_credential( state: State<'_, AppState>, ) -> Result<(), String> { let auth_cache_manager = &state.auth_cache_manager; - auth_cache_manager.store_credential(&options.key, options.credential).await + auth_cache_manager + .store_credential(&options.key, options.credential) + .await } #[tauri::command] @@ -1578,19 +1604,14 @@ pub async fn get_auth_credential( } #[tauri::command] -pub async fn clear_auth_cache_entry( - key: String, - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn clear_auth_cache_entry(key: String, state: State<'_, AppState>) -> Result<(), String> { let auth_cache_manager = &state.auth_cache_manager; auth_cache_manager.clear_entry(&key).await; Ok(()) } #[tauri::command] -pub async fn clear_all_auth_cache( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn clear_all_auth_cache(state: State<'_, AppState>) -> Result<(), String> { let auth_cache_manager = &state.auth_cache_manager; auth_cache_manager.clear_all().await; Ok(()) @@ -1613,9 +1634,7 @@ pub async fn list_auth_cache_entries( } #[tauri::command] -pub async fn export_auth_cache( - state: State<'_, AppState>, -) -> Result { +pub async fn export_auth_cache(state: State<'_, AppState>) -> Result { let auth_cache_manager = &state.auth_cache_manager; auth_cache_manager.export_cache().await } @@ -1640,11 +1659,7 @@ pub fn create_auth_cache_key( username: Option, resource: Option, ) -> String { - crate::auth_cache::create_cache_key( - &service, - username.as_deref(), - resource.as_deref(), - ) + crate::auth_cache::create_cache_key(&service, username.as_deref(), resource.as_deref()) } // Terminal Integrations Commands @@ -1670,7 +1685,9 @@ pub async fn set_default_terminal( state: State<'_, AppState>, ) -> Result<(), String> { let terminal_integrations_manager = &state.terminal_integrations_manager; - terminal_integrations_manager.set_default_terminal(emulator).await + terminal_integrations_manager + .set_default_terminal(emulator) + .await } #[tauri::command] @@ -1680,7 +1697,9 @@ pub async fn launch_terminal_emulator( state: State<'_, AppState>, ) -> Result<(), String> { let terminal_integrations_manager = &state.terminal_integrations_manager; - terminal_integrations_manager.launch_terminal(emulator, options).await + terminal_integrations_manager + .launch_terminal(emulator, options) + .await } #[tauri::command] @@ -1689,7 +1708,9 @@ pub async fn get_terminal_config( state: State<'_, AppState>, ) -> Result, String> { let terminal_integrations_manager = &state.terminal_integrations_manager; - Ok(terminal_integrations_manager.get_terminal_config(emulator).await) + Ok(terminal_integrations_manager + .get_terminal_config(emulator) + .await) } #[tauri::command] @@ -1698,7 +1719,9 @@ pub async fn update_terminal_config( state: State<'_, AppState>, ) -> Result<(), String> { let terminal_integrations_manager = &state.terminal_integrations_manager; - terminal_integrations_manager.update_terminal_config(config).await; + terminal_integrations_manager + .update_terminal_config(config) + .await; Ok(()) } @@ -1707,7 +1730,9 @@ pub async fn list_detected_terminals( state: State<'_, AppState>, ) -> Result, String> { let terminal_integrations_manager = &state.terminal_integrations_manager; - Ok(terminal_integrations_manager.list_detected_terminals().await) + Ok(terminal_integrations_manager + .list_detected_terminals() + .await) } #[tauri::command] @@ -1719,7 +1744,9 @@ pub async fn create_terminal_ssh_url( state: State<'_, AppState>, ) -> Result, String> { let terminal_integrations_manager = &state.terminal_integrations_manager; - Ok(terminal_integrations_manager.create_ssh_url(emulator, &user, &host, port).await) + Ok(terminal_integrations_manager + .create_ssh_url(emulator, &user, &host, port) + .await) } #[tauri::command] @@ -1727,10 +1754,12 @@ pub async fn get_terminal_integration_stats( state: State<'_, AppState>, ) -> Result { let terminal_integrations_manager = &state.terminal_integrations_manager; - - let detected = terminal_integrations_manager.list_detected_terminals().await; + + let detected = terminal_integrations_manager + .list_detected_terminals() + .await; let default = terminal_integrations_manager.get_default_terminal().await; - + let mut terminals_by_platform = HashMap::new(); for info in &detected { if let Some(config) = &info.config { @@ -1742,7 +1771,7 @@ pub async fn get_terminal_integration_stats( } } } - + Ok(crate::terminal_integrations::TerminalIntegrationStats { total_terminals: detected.len(), installed_terminals: detected.iter().filter(|t| t.installed).count(), @@ -1756,85 +1785,146 @@ pub async fn get_terminal_integration_stats( pub async fn get_all_settings() -> Result, String> { let settings = crate::settings::Settings::load().unwrap_or_default(); let mut all_settings = HashMap::new(); - - all_settings.insert("general".to_string(), - serde_json::to_value(&settings.general).unwrap_or(serde_json::Value::Null)); - all_settings.insert("dashboard".to_string(), - serde_json::to_value(&settings.dashboard).unwrap_or(serde_json::Value::Null)); - all_settings.insert("advanced".to_string(), - serde_json::to_value(&settings.advanced).unwrap_or(serde_json::Value::Null)); - all_settings.insert("recording".to_string(), - serde_json::to_value(&settings.recording).unwrap_or(serde_json::Value::Null)); - all_settings.insert("tty_forward".to_string(), - serde_json::to_value(&settings.tty_forward).unwrap_or(serde_json::Value::Null)); - all_settings.insert("monitoring".to_string(), - serde_json::to_value(&settings.monitoring).unwrap_or(serde_json::Value::Null)); - all_settings.insert("network".to_string(), - serde_json::to_value(&settings.network).unwrap_or(serde_json::Value::Null)); - all_settings.insert("port".to_string(), - serde_json::to_value(&settings.port).unwrap_or(serde_json::Value::Null)); - all_settings.insert("notifications".to_string(), - serde_json::to_value(&settings.notifications).unwrap_or(serde_json::Value::Null)); - all_settings.insert("terminal_integrations".to_string(), - serde_json::to_value(&settings.terminal_integrations).unwrap_or(serde_json::Value::Null)); - all_settings.insert("updates".to_string(), - serde_json::to_value(&settings.updates).unwrap_or(serde_json::Value::Null)); - all_settings.insert("security".to_string(), - serde_json::to_value(&settings.security).unwrap_or(serde_json::Value::Null)); - all_settings.insert("debug".to_string(), - serde_json::to_value(&settings.debug).unwrap_or(serde_json::Value::Null)); - + + all_settings.insert( + "general".to_string(), + serde_json::to_value(&settings.general).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "dashboard".to_string(), + serde_json::to_value(&settings.dashboard).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "advanced".to_string(), + serde_json::to_value(&settings.advanced).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "recording".to_string(), + serde_json::to_value(&settings.recording).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "tty_forward".to_string(), + serde_json::to_value(&settings.tty_forward).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "monitoring".to_string(), + serde_json::to_value(&settings.monitoring).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "network".to_string(), + serde_json::to_value(&settings.network).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "port".to_string(), + serde_json::to_value(&settings.port).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "notifications".to_string(), + serde_json::to_value(&settings.notifications).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "terminal_integrations".to_string(), + serde_json::to_value(&settings.terminal_integrations).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "updates".to_string(), + serde_json::to_value(&settings.updates).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "security".to_string(), + serde_json::to_value(&settings.security).unwrap_or(serde_json::Value::Null), + ); + all_settings.insert( + "debug".to_string(), + serde_json::to_value(&settings.debug).unwrap_or(serde_json::Value::Null), + ); + Ok(all_settings) } #[tauri::command] pub async fn update_setting(section: String, key: String, value: String) -> Result<(), String> { let mut settings = crate::settings::Settings::load().unwrap_or_default(); - + // Parse the JSON value - let json_value: serde_json::Value = serde_json::from_str(&value) - .map_err(|e| format!("Invalid JSON value: {}", e))?; - + let json_value: serde_json::Value = + serde_json::from_str(&value).map_err(|e| format!("Invalid JSON value: {}", e))?; + match section.as_str() { - "general" => { - match key.as_str() { - "launch_at_login" => settings.general.launch_at_login = json_value.as_bool().unwrap_or(false), - "show_dock_icon" => settings.general.show_dock_icon = json_value.as_bool().unwrap_or(true), - "default_terminal" => settings.general.default_terminal = json_value.as_str().unwrap_or("system").to_string(), - "default_shell" => settings.general.default_shell = json_value.as_str().unwrap_or("default").to_string(), - "show_welcome_on_startup" => settings.general.show_welcome_on_startup = json_value.as_bool(), - "theme" => settings.general.theme = json_value.as_str().map(|s| s.to_string()), - "language" => settings.general.language = json_value.as_str().map(|s| s.to_string()), - "check_updates_automatically" => settings.general.check_updates_automatically = json_value.as_bool(), - _ => return Err(format!("Unknown general setting: {}", key)), + "general" => match key.as_str() { + "launch_at_login" => { + settings.general.launch_at_login = json_value.as_bool().unwrap_or(false) } - } - "dashboard" => { - match key.as_str() { - "server_port" => settings.dashboard.server_port = json_value.as_u64().unwrap_or(4020) as u16, - "enable_password" => settings.dashboard.enable_password = json_value.as_bool().unwrap_or(false), - "password" => settings.dashboard.password = json_value.as_str().unwrap_or("").to_string(), - "access_mode" => settings.dashboard.access_mode = json_value.as_str().unwrap_or("localhost").to_string(), - "auto_cleanup" => settings.dashboard.auto_cleanup = json_value.as_bool().unwrap_or(true), - "session_limit" => settings.dashboard.session_limit = json_value.as_u64().map(|v| v as u32), - "idle_timeout_minutes" => settings.dashboard.idle_timeout_minutes = json_value.as_u64().map(|v| v as u32), - "enable_cors" => settings.dashboard.enable_cors = json_value.as_bool(), - _ => return Err(format!("Unknown dashboard setting: {}", key)), + "show_dock_icon" => { + settings.general.show_dock_icon = json_value.as_bool().unwrap_or(true) } - } - "advanced" => { - match key.as_str() { - "debug_mode" => settings.advanced.debug_mode = json_value.as_bool().unwrap_or(false), - "log_level" => settings.advanced.log_level = json_value.as_str().unwrap_or("info").to_string(), - "session_timeout" => settings.advanced.session_timeout = json_value.as_u64().unwrap_or(0) as u32, - "ngrok_auth_token" => settings.advanced.ngrok_auth_token = json_value.as_str().map(|s| s.to_string()), - "ngrok_region" => settings.advanced.ngrok_region = json_value.as_str().map(|s| s.to_string()), - "ngrok_subdomain" => settings.advanced.ngrok_subdomain = json_value.as_str().map(|s| s.to_string()), - "enable_telemetry" => settings.advanced.enable_telemetry = json_value.as_bool(), - "experimental_features" => settings.advanced.experimental_features = json_value.as_bool(), - _ => return Err(format!("Unknown advanced setting: {}", key)), + "default_terminal" => { + settings.general.default_terminal = + json_value.as_str().unwrap_or("system").to_string() } - } + "default_shell" => { + settings.general.default_shell = + json_value.as_str().unwrap_or("default").to_string() + } + "show_welcome_on_startup" => { + settings.general.show_welcome_on_startup = json_value.as_bool() + } + "theme" => settings.general.theme = json_value.as_str().map(|s| s.to_string()), + "language" => settings.general.language = json_value.as_str().map(|s| s.to_string()), + "check_updates_automatically" => { + settings.general.check_updates_automatically = json_value.as_bool() + } + _ => return Err(format!("Unknown general setting: {}", key)), + }, + "dashboard" => match key.as_str() { + "server_port" => { + settings.dashboard.server_port = json_value.as_u64().unwrap_or(4020) as u16 + } + "enable_password" => { + settings.dashboard.enable_password = json_value.as_bool().unwrap_or(false) + } + "password" => { + settings.dashboard.password = json_value.as_str().unwrap_or("").to_string() + } + "access_mode" => { + settings.dashboard.access_mode = + json_value.as_str().unwrap_or("localhost").to_string() + } + "auto_cleanup" => { + settings.dashboard.auto_cleanup = json_value.as_bool().unwrap_or(true) + } + "session_limit" => { + settings.dashboard.session_limit = json_value.as_u64().map(|v| v as u32) + } + "idle_timeout_minutes" => { + settings.dashboard.idle_timeout_minutes = json_value.as_u64().map(|v| v as u32) + } + "enable_cors" => settings.dashboard.enable_cors = json_value.as_bool(), + _ => return Err(format!("Unknown dashboard setting: {}", key)), + }, + "advanced" => match key.as_str() { + "debug_mode" => settings.advanced.debug_mode = json_value.as_bool().unwrap_or(false), + "log_level" => { + settings.advanced.log_level = json_value.as_str().unwrap_or("info").to_string() + } + "session_timeout" => { + settings.advanced.session_timeout = json_value.as_u64().unwrap_or(0) as u32 + } + "ngrok_auth_token" => { + settings.advanced.ngrok_auth_token = json_value.as_str().map(|s| s.to_string()) + } + "ngrok_region" => { + settings.advanced.ngrok_region = json_value.as_str().map(|s| s.to_string()) + } + "ngrok_subdomain" => { + settings.advanced.ngrok_subdomain = json_value.as_str().map(|s| s.to_string()) + } + "enable_telemetry" => settings.advanced.enable_telemetry = json_value.as_bool(), + "experimental_features" => { + settings.advanced.experimental_features = json_value.as_bool() + } + _ => return Err(format!("Unknown advanced setting: {}", key)), + }, "debug" => { // Ensure debug settings exist if settings.debug.is_none() { @@ -1849,35 +1939,52 @@ pub async fn update_setting(section: String, key: String, value: String) -> Resu show_internal_errors: false, }); } - + if let Some(ref mut debug) = settings.debug { match key.as_str() { - "enable_debug_menu" => debug.enable_debug_menu = json_value.as_bool().unwrap_or(false), - "show_performance_stats" => debug.show_performance_stats = json_value.as_bool().unwrap_or(false), - "enable_verbose_logging" => debug.enable_verbose_logging = json_value.as_bool().unwrap_or(false), + "enable_debug_menu" => { + debug.enable_debug_menu = json_value.as_bool().unwrap_or(false) + } + "show_performance_stats" => { + debug.show_performance_stats = json_value.as_bool().unwrap_or(false) + } + "enable_verbose_logging" => { + debug.enable_verbose_logging = json_value.as_bool().unwrap_or(false) + } "log_to_file" => debug.log_to_file = json_value.as_bool().unwrap_or(false), - "log_file_path" => debug.log_file_path = json_value.as_str().map(|s| s.to_string()), - "max_log_file_size_mb" => debug.max_log_file_size_mb = json_value.as_u64().map(|v| v as u32), - "enable_dev_tools" => debug.enable_dev_tools = json_value.as_bool().unwrap_or(false), - "show_internal_errors" => debug.show_internal_errors = json_value.as_bool().unwrap_or(false), + "log_file_path" => { + debug.log_file_path = json_value.as_str().map(|s| s.to_string()) + } + "max_log_file_size_mb" => { + debug.max_log_file_size_mb = json_value.as_u64().map(|v| v as u32) + } + "enable_dev_tools" => { + debug.enable_dev_tools = json_value.as_bool().unwrap_or(false) + } + "show_internal_errors" => { + debug.show_internal_errors = json_value.as_bool().unwrap_or(false) + } _ => return Err(format!("Unknown debug setting: {}", key)), } } } _ => return Err(format!("Unknown settings section: {}", section)), } - + settings.save() } #[tauri::command] -pub async fn set_dashboard_password(password: String, state: State<'_, AppState>) -> Result<(), String> { +pub async fn set_dashboard_password( + password: String, + state: State<'_, AppState>, +) -> Result<(), String> { // Update settings let mut settings = crate::settings::Settings::load().unwrap_or_default(); settings.dashboard.password = password.clone(); settings.dashboard.enable_password = !password.is_empty(); settings.save()?; - + // Update the running server's auth configuration if it's running let server = state.http_server.read().await; if server.is_some() { @@ -1885,7 +1992,7 @@ pub async fn set_dashboard_password(password: String, state: State<'_, AppState> // Restart server to apply new auth settings restart_server(state).await?; } - + Ok(()) } @@ -1895,29 +2002,44 @@ pub async fn restart_server_with_port(port: u16, state: State<'_, AppState>) -> let mut settings = crate::settings::Settings::load().unwrap_or_default(); settings.dashboard.server_port = port; settings.save()?; - + // Restart the server restart_server(state).await } #[tauri::command] -pub async fn update_server_bind_address(address: String, state: State<'_, AppState>) -> Result<(), String> { +pub async fn update_server_bind_address( + address: String, + state: State<'_, AppState>, + app_handle: tauri::AppHandle, +) -> Result<(), String> { // Update settings let mut settings = crate::settings::Settings::load().unwrap_or_default(); - settings.dashboard.access_mode = if address == "127.0.0.1" { "localhost" } else { "network" }.to_string(); + let access_mode = if address == "127.0.0.1" { + "localhost" + } else { + "network" + }; + settings.dashboard.access_mode = access_mode.to_string(); settings.save()?; - + + // Update tray menu to reflect new access mode + crate::tray_menu::TrayMenuManager::update_access_mode(&app_handle, access_mode).await; + // Restart server to apply new bind address restart_server(state).await } #[tauri::command] -pub async fn set_dock_icon_visibility(visible: bool, app_handle: tauri::AppHandle) -> Result<(), String> { +pub async fn set_dock_icon_visibility( + visible: bool, + app_handle: tauri::AppHandle, +) -> Result<(), String> { // Update settings let mut settings = crate::settings::Settings::load().unwrap_or_default(); settings.general.show_dock_icon = visible; settings.save()?; - + // Apply the change update_dock_icon_visibility(app_handle).await } @@ -1928,35 +2050,42 @@ pub async fn set_log_level(level: String) -> Result<(), String> { let mut settings = crate::settings::Settings::load().unwrap_or_default(); settings.advanced.log_level = level.clone(); settings.save()?; - + // TODO: Apply the log level change to the running logger tracing::info!("Log level changed to: {}", level); - + Ok(()) } #[tauri::command] -pub async fn test_api_endpoint(endpoint: String, state: State<'_, AppState>) -> Result { +pub async fn test_api_endpoint( + endpoint: String, + state: State<'_, AppState>, +) -> Result { let server = state.http_server.read().await; - + if let Some(http_server) = server.as_ref() { let port = http_server.port(); let url = format!("http://localhost:{}{}", port, endpoint); - + // Create a simple HTTP client request let client = reqwest::Client::new(); - let response = client.get(&url) + let response = client + .get(&url) .send() .await .map_err(|e| format!("Request failed: {}", e))?; - + let status = response.status(); - let body = response.text().await.unwrap_or_else(|_| "Failed to read body".to_string()); - + let body = response + .text() + .await + .unwrap_or_else(|_| "Failed to read body".to_string()); + // Try to parse as JSON, fallback to text let json_body = serde_json::from_str::(&body) .unwrap_or_else(|_| serde_json::json!({ "body": body })); - + Ok(serde_json::json!({ "status": status.as_u16(), "endpoint": endpoint, @@ -1990,7 +2119,7 @@ pub async fn get_server_logs(limit: usize) -> Result, String> { message: "Health check endpoint accessed".to_string(), }, ]; - + Ok(logs.into_iter().take(limit).collect()) } @@ -1998,33 +2127,43 @@ pub async fn get_server_logs(limit: usize) -> Result, String> { pub async fn export_logs(_app_handle: tauri::AppHandle) -> Result<(), String> { // Get logs let logs = get_server_logs(1000).await?; - + // Convert to text format - let log_text = logs.into_iter() - .map(|log| format!("[{}] {} - {}", log.timestamp, log.level.to_uppercase(), log.message)) + let log_text = logs + .into_iter() + .map(|log| { + format!( + "[{}] {} - {}", + log.timestamp, + log.level.to_uppercase(), + log.message + ) + }) .collect::>() .join("\n"); - + // Save to file let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); let filename = format!("vibetunnel_logs_{}.txt", timestamp); - + // In Tauri v2, we should use the dialog plugin instead // For now, let's just save to a default location - let downloads_dir = dirs::download_dir() - .ok_or_else(|| "Could not find downloads directory".to_string())?; + let downloads_dir = + dirs::download_dir().ok_or_else(|| "Could not find downloads directory".to_string())?; let path = downloads_dir.join(&filename); std::fs::write(&path, log_text).map_err(|e| e.to_string())?; - + Ok(()) } #[tauri::command] pub async fn get_local_ip() -> Result { - get_local_ip_address().await.map(|opt| opt.unwrap_or_else(|| "127.0.0.1".to_string())) + get_local_ip_address() + .await + .map(|opt| opt.unwrap_or_else(|| "127.0.0.1".to_string())) } #[tauri::command] pub async fn detect_terminals() -> Result { crate::terminal_detector::detect_terminals() -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/debug_features.rs b/tauri/src-tauri/src/debug_features.rs index 5dc4eef8..c80ca55d 100644 --- a/tauri/src-tauri/src/debug_features.rs +++ b/tauri/src-tauri/src/debug_features.rs @@ -1,9 +1,9 @@ -use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, VecDeque}; +use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; -use std::collections::{HashMap, VecDeque}; -use chrono::{DateTime, Utc}; -use std::path::PathBuf; /// Debug feature types #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -245,7 +245,10 @@ impl DebugFeaturesManager { } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } @@ -260,14 +263,20 @@ impl DebugFeaturesManager { } /// Log a message - pub async fn log(&self, level: LogLevel, component: &str, message: &str, metadata: HashMap) { + pub async fn log( + &self, + level: LogLevel, + component: &str, + message: &str, + metadata: HashMap, + ) { let settings = self.settings.read().await; - + // Check if logging is enabled and level is appropriate if !settings.enabled || level < settings.log_level { return; } - + let entry = LogEntry { timestamp: Utc::now(), level, @@ -275,16 +284,16 @@ impl DebugFeaturesManager { message: message.to_string(), metadata, }; - + // Add to in-memory log let mut logs = self.logs.write().await; logs.push_back(entry.clone()); - + // Limit log size while logs.len() > settings.max_log_entries { logs.pop_front(); } - + // Log to file if enabled if settings.log_to_file { if let Some(path) = &settings.log_file_path { @@ -294,13 +303,19 @@ impl DebugFeaturesManager { } /// Record a performance metric - pub async fn record_metric(&self, name: &str, value: f64, unit: &str, tags: HashMap) { + pub async fn record_metric( + &self, + name: &str, + value: f64, + unit: &str, + tags: HashMap, + ) { let settings = self.settings.read().await; - + if !settings.enabled || !settings.enable_performance_monitoring { return; } - + let metric = PerformanceMetric { name: name.to_string(), value, @@ -308,10 +323,10 @@ impl DebugFeaturesManager { timestamp: Utc::now(), tags, }; - + let mut metrics = self.performance_metrics.write().await; metrics.push_back(metric); - + // Keep only last 1000 metrics while metrics.len() > 1000 { metrics.pop_front(); @@ -321,11 +336,11 @@ impl DebugFeaturesManager { /// Take a memory snapshot pub async fn take_memory_snapshot(&self) -> Result { let settings = self.settings.read().await; - + if !settings.enabled || !settings.enable_memory_profiling { return Err("Memory profiling is disabled".to_string()); } - + // TODO: Implement actual memory profiling let snapshot = MemorySnapshot { timestamp: Utc::now(), @@ -335,29 +350,29 @@ impl DebugFeaturesManager { process_rss_mb: 0.0, details: HashMap::new(), }; - + let mut snapshots = self.memory_snapshots.write().await; snapshots.push_back(snapshot.clone()); - + // Keep only last 100 snapshots while snapshots.len() > 100 { snapshots.pop_front(); } - + Ok(snapshot) } /// Log a network request pub async fn log_network_request(&self, request: NetworkRequest) { let settings = self.settings.read().await; - + if !settings.enabled || !settings.enable_network_inspector { return; } - + let mut requests = self.network_requests.write().await; requests.insert(request.id.clone(), request); - + // Keep only last 500 requests if requests.len() > 500 { // Remove oldest entries @@ -372,28 +387,29 @@ impl DebugFeaturesManager { /// Run API tests pub async fn run_api_tests(&self, tests: Vec) -> Vec { let mut results = Vec::new(); - + for test in tests { let result = self.run_single_api_test(&test).await; results.push(result.clone()); - + // Store result let mut test_results = self.api_test_results.write().await; - test_results.entry(test.id.clone()) + test_results + .entry(test.id.clone()) .or_insert_with(Vec::new) .push(result); } - + results } /// Run a single API test async fn run_single_api_test(&self, test: &APITestCase) -> APITestResult { let start = std::time::Instant::now(); - + // TODO: Implement actual API testing let duration_ms = start.elapsed().as_millis() as u64; - + APITestResult { test_id: test.id.clone(), success: false, @@ -408,15 +424,15 @@ impl DebugFeaturesManager { /// Run benchmarks pub async fn run_benchmarks(&self, configs: Vec) -> Vec { let mut results = Vec::new(); - + for config in configs { let result = self.run_single_benchmark(&config).await; results.push(result.clone()); - + // Store result self.benchmark_results.write().await.push(result); } - + results } @@ -443,8 +459,13 @@ impl DebugFeaturesManager { let app_info = self.get_app_info().await; let performance_summary = self.get_performance_summary().await; let error_summary = self.get_error_summary().await; - let recommendations = self.generate_recommendations(&system_info, &app_info, &performance_summary, &error_summary); - + let recommendations = self.generate_recommendations( + &system_info, + &app_info, + &performance_summary, + &error_summary, + ); + DiagnosticReport { timestamp: Utc::now(), system_info, @@ -459,13 +480,13 @@ impl DebugFeaturesManager { pub async fn get_logs(&self, limit: Option, level: Option) -> Vec { let logs = self.logs.read().await; let iter = logs.iter().rev(); - + let filtered: Vec<_> = if let Some(min_level) = level { iter.filter(|log| log.level >= min_level).cloned().collect() } else { iter.cloned().collect() }; - + match limit { Some(n) => filtered.into_iter().take(n).collect(), None => filtered, @@ -495,7 +516,7 @@ impl DebugFeaturesManager { let requests = self.network_requests.read().await; let mut sorted: Vec<_> = requests.values().cloned().collect(); sorted.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - + match limit { Some(n) => sorted.into_iter().take(n).collect(), None => sorted, @@ -515,21 +536,23 @@ impl DebugFeaturesManager { /// Enable/disable debug mode pub async fn set_debug_mode(&self, enabled: bool) { self.settings.write().await.enabled = enabled; - + if let Some(notification_manager) = &self.notification_manager { let message = if enabled { "Debug mode enabled" } else { "Debug mode disabled" }; - let _ = notification_manager.notify_success("Debug Mode", message).await; + let _ = notification_manager + .notify_success("Debug Mode", message) + .await; } } // Helper methods async fn write_log_to_file(&self, entry: &LogEntry, path: &PathBuf) -> Result<(), String> { use tokio::io::AsyncWriteExt; - + let log_line = format!( "[{}] [{}] [{}] {}\n", entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"), @@ -537,17 +560,18 @@ impl DebugFeaturesManager { entry.component, entry.message ); - + let mut file = tokio::fs::OpenOptions::new() .create(true) .append(true) .open(path) .await .map_err(|e| e.to_string())?; - - file.write_all(log_line.as_bytes()).await + + file.write_all(log_line.as_bytes()) + .await .map_err(|e| e.to_string())?; - + Ok(()) } @@ -568,7 +592,7 @@ impl DebugFeaturesManager { AppInfo { version: env!("CARGO_PKG_VERSION").to_string(), build_date: chrono::Utc::now().to_rfc3339(), // TODO: Get actual build date - uptime_seconds: 0, // TODO: Track uptime + uptime_seconds: 0, // TODO: Track uptime active_sessions: 0, total_requests: 0, error_count: 0, @@ -588,20 +612,23 @@ impl DebugFeaturesManager { async fn get_error_summary(&self) -> ErrorSummary { let logs = self.logs.read().await; - let errors: Vec<_> = logs.iter() + let errors: Vec<_> = logs + .iter() .filter(|log| log.level == LogLevel::Error) .cloned() .collect(); - + let mut errors_by_type = HashMap::new(); for error in &errors { - let error_type = error.metadata.get("type") + let error_type = error + .metadata + .get("type") .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(); *errors_by_type.entry(error_type).or_insert(0) += 1; } - + ErrorSummary { total_errors: errors.len() as u64, errors_by_type, @@ -609,25 +636,37 @@ impl DebugFeaturesManager { } } - fn generate_recommendations(&self, system: &SystemInfo, _app: &AppInfo, perf: &PerformanceSummary, errors: &ErrorSummary) -> Vec { + fn generate_recommendations( + &self, + system: &SystemInfo, + _app: &AppInfo, + perf: &PerformanceSummary, + errors: &ErrorSummary, + ) -> Vec { let mut recommendations = Vec::new(); - + if perf.cpu_usage_percent > 80.0 { - recommendations.push("High CPU usage detected. Consider optimizing performance-critical code.".to_string()); + recommendations.push( + "High CPU usage detected. Consider optimizing performance-critical code." + .to_string(), + ); } - + if perf.memory_usage_mb > (system.total_memory_mb as f64 * 0.8) { recommendations.push("High memory usage detected. Check for memory leaks.".to_string()); } - + if errors.total_errors > 100 { - recommendations.push("High error rate detected. Review error logs for patterns.".to_string()); + recommendations + .push("High error rate detected. Review error logs for patterns.".to_string()); } - + if perf.avg_response_time_ms > 1000.0 { - recommendations.push("Slow response times detected. Consider caching or query optimization.".to_string()); + recommendations.push( + "Slow response times detected. Consider caching or query optimization.".to_string(), + ); } - + recommendations } } @@ -645,4 +684,4 @@ pub struct DebugStats { } // Re-export num_cpus if needed -extern crate num_cpus; \ No newline at end of file +extern crate num_cpus; diff --git a/tauri/src-tauri/src/fs_api.rs b/tauri/src-tauri/src/fs_api.rs index 9765d435..4f2f5a14 100644 --- a/tauri/src-tauri/src/fs_api.rs +++ b/tauri/src-tauri/src/fs_api.rs @@ -1,6 +1,6 @@ use axum::{ extract::Query, - http::{StatusCode, header}, + http::{header, StatusCode}, response::{IntoResponse, Response}, Json, }; @@ -62,8 +62,7 @@ pub struct OperationResult { /// Expand tilde to home directory fn expand_path(path: &str) -> Result { if path.starts_with('~') { - let home = dirs::home_dir() - .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; + let home = dirs::home_dir().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; Ok(home.join(path.strip_prefix("~/").unwrap_or(""))) } else { Ok(PathBuf::from(path)) @@ -75,70 +74,77 @@ pub async fn get_file_info( Query(params): Query, ) -> Result, StatusCode> { let path = expand_path(¶ms.path)?; - - let metadata = fs::metadata(&path).await + + let metadata = fs::metadata(&path) + .await .map_err(|_| StatusCode::NOT_FOUND)?; - - let name = path.file_name() + + let name = path + .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| path.to_string_lossy().to_string()); - - let is_symlink = fs::symlink_metadata(&path).await + + let is_symlink = fs::symlink_metadata(&path) + .await .map(|m| m.file_type().is_symlink()) .unwrap_or(false); - + let hidden = name.starts_with('.'); - - let created = metadata.created() + + let created = metadata + .created() .map(|t| { let datetime: chrono::DateTime = t.into(); datetime.to_rfc3339() }) .ok(); - - let modified = metadata.modified() + + let modified = metadata + .modified() .map(|t| { let datetime: chrono::DateTime = t.into(); datetime.to_rfc3339() }) .ok(); - - let accessed = metadata.accessed() + + let accessed = metadata + .accessed() .map(|t| { let datetime: chrono::DateTime = t.into(); datetime.to_rfc3339() }) .ok(); - + #[cfg(unix)] let permissions = { use std::os::unix::fs::PermissionsExt; Some(format!("{:o}", metadata.permissions().mode() & 0o777)) }; - + let mime_type = if metadata.is_file() { // Simple MIME type detection based on extension - let ext = path.extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - - Some(match ext { - "txt" => "text/plain", - "html" | "htm" => "text/html", - "css" => "text/css", - "js" => "application/javascript", - "json" => "application/json", - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "pdf" => "application/pdf", - "zip" => "application/zip", - _ => "application/octet-stream", - }.to_string()) + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + + Some( + match ext { + "txt" => "text/plain", + "html" | "htm" => "text/html", + "css" => "text/css", + "js" => "application/javascript", + "json" => "application/json", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "pdf" => "application/pdf", + "zip" => "application/zip", + _ => "application/octet-stream", + } + .to_string(), + ) } else { None }; - + Ok(Json(FileMetadata { name, path: path.to_string_lossy().to_string(), @@ -160,29 +166,31 @@ pub async fn get_file_info( } /// Read file contents -pub async fn read_file( - Query(params): Query, -) -> Result { +pub async fn read_file(Query(params): Query) -> Result { let path = expand_path(¶ms.path)?; - + // Check if file exists and is a file - let metadata = fs::metadata(&path).await + let metadata = fs::metadata(&path) + .await .map_err(|_| StatusCode::NOT_FOUND)?; - + if !metadata.is_file() { return Err(StatusCode::BAD_REQUEST); } - + // Read file contents - let mut file = fs::File::open(&path).await + let mut file = fs::File::open(&path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + let mut contents = Vec::new(); - file.read_to_end(&mut contents).await + file.read_to_end(&mut contents) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + // Determine content type - let content_type = path.extension() + let content_type = path + .extension() .and_then(|e| e.to_str()) .and_then(|ext| match ext { "txt" => Some("text/plain"), @@ -197,11 +205,8 @@ pub async fn read_file( _ => None, }) .unwrap_or("application/octet-stream"); - - Ok(( - [(header::CONTENT_TYPE, content_type)], - contents, - ).into_response()) + + Ok(([(header::CONTENT_TYPE, content_type)], contents).into_response()) } /// Write file contents @@ -209,24 +214,27 @@ pub async fn write_file( Json(req): Json, ) -> Result, StatusCode> { let path = expand_path(&req.path)?; - + // Ensure parent directory exists if let Some(parent) = path.parent() { - fs::create_dir_all(parent).await + fs::create_dir_all(parent) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } - + // Write file let content = if req.encoding.as_deref() == Some("base64") { - base64::engine::general_purpose::STANDARD.decode(&req.content) + base64::engine::general_purpose::STANDARD + .decode(&req.content) .map_err(|_| StatusCode::BAD_REQUEST)? } else { req.content.into_bytes() }; - - fs::write(&path, content).await + + fs::write(&path, content) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + Ok(Json(OperationResult { success: true, message: format!("File written successfully: {}", path.display()), @@ -238,20 +246,23 @@ pub async fn delete_file( Query(params): Query, ) -> Result, StatusCode> { let path = expand_path(¶ms.path)?; - + // Check if path exists - let metadata = fs::metadata(&path).await + let metadata = fs::metadata(&path) + .await .map_err(|_| StatusCode::NOT_FOUND)?; - + // Delete based on type if metadata.is_dir() { - fs::remove_dir_all(&path).await + fs::remove_dir_all(&path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } else { - fs::remove_file(&path).await + fs::remove_file(&path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } - + Ok(Json(OperationResult { success: true, message: format!("Deleted: {}", path.display()), @@ -259,100 +270,116 @@ pub async fn delete_file( } /// Move/rename file or directory -pub async fn move_file( - Json(req): Json, -) -> Result, StatusCode> { +pub async fn move_file(Json(req): Json) -> Result, StatusCode> { let from_path = expand_path(&req.from)?; let to_path = expand_path(&req.to)?; - + // Check if source exists if !from_path.exists() { return Err(StatusCode::NOT_FOUND); } - + // Check if destination already exists if to_path.exists() { return Err(StatusCode::CONFLICT); } - + // Ensure destination parent directory exists if let Some(parent) = to_path.parent() { - fs::create_dir_all(parent).await + fs::create_dir_all(parent) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } - + // Move the file/directory - fs::rename(&from_path, &to_path).await + fs::rename(&from_path, &to_path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + Ok(Json(OperationResult { success: true, - message: format!("Moved from {} to {}", from_path.display(), to_path.display()), + message: format!( + "Moved from {} to {}", + from_path.display(), + to_path.display() + ), })) } /// Copy file or directory -pub async fn copy_file( - Json(req): Json, -) -> Result, StatusCode> { +pub async fn copy_file(Json(req): Json) -> Result, StatusCode> { let from_path = expand_path(&req.from)?; let to_path = expand_path(&req.to)?; - + // Check if source exists - let metadata = fs::metadata(&from_path).await + let metadata = fs::metadata(&from_path) + .await .map_err(|_| StatusCode::NOT_FOUND)?; - + // Check if destination already exists if to_path.exists() && !req.overwrite.unwrap_or(false) { return Err(StatusCode::CONFLICT); } - + // Ensure destination parent directory exists if let Some(parent) = to_path.parent() { - fs::create_dir_all(parent).await + fs::create_dir_all(parent) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } - + // Copy based on type if metadata.is_file() { - fs::copy(&from_path, &to_path).await + fs::copy(&from_path, &to_path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } else if metadata.is_dir() { // Recursive directory copy copy_dir_recursive(&from_path, &to_path).await?; } - + Ok(Json(OperationResult { success: true, - message: format!("Copied from {} to {}", from_path.display(), to_path.display()), + message: format!( + "Copied from {} to {}", + from_path.display(), + to_path.display() + ), })) } /// Recursively copy a directory async fn copy_dir_recursive(from: &PathBuf, to: &PathBuf) -> Result<(), StatusCode> { - fs::create_dir_all(to).await + fs::create_dir_all(to) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut entries = fs::read_dir(from).await + + let mut entries = fs::read_dir(from) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - while let Some(entry) = entries.next_entry().await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? { - + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + { let from_path = entry.path(); let to_path = to.join(entry.file_name()); - - let metadata = entry.metadata().await + + let metadata = entry + .metadata() + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + if metadata.is_file() { - fs::copy(&from_path, &to_path).await + fs::copy(&from_path, &to_path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } else if metadata.is_dir() { Box::pin(copy_dir_recursive(&from_path, &to_path)).await?; } } - + Ok(()) } @@ -378,10 +405,10 @@ pub async fn search_files( let base_path = expand_path(¶ms.path)?; let pattern = params.pattern.to_lowercase(); let max_depth = params.max_depth.unwrap_or(5); - + let mut results = Vec::new(); search_recursive(&base_path, &pattern, 0, max_depth, &mut results).await?; - + Ok(Json(results)) } @@ -395,20 +422,25 @@ async fn search_recursive( if depth > max_depth { return Ok(()); } - - let mut entries = fs::read_dir(path).await + + let mut entries = fs::read_dir(path) + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - while let Some(entry) = entries.next_entry().await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? { - + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + { let entry_path = entry.path(); let file_name = entry.file_name().to_string_lossy().to_string(); - + if file_name.to_lowercase().contains(pattern) { - let metadata = entry.metadata().await + let metadata = entry + .metadata() + .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + results.push(SearchResult { path: entry_path.to_string_lossy().to_string(), name: file_name, @@ -416,14 +448,19 @@ async fn search_recursive( size: metadata.len(), }); } - + // Recurse into directories - if entry.file_type().await - .map(|t| t.is_dir()) - .unwrap_or(false) { - Box::pin(search_recursive(&entry_path, pattern, depth + 1, max_depth, results)).await?; + if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) { + Box::pin(search_recursive( + &entry_path, + pattern, + depth + 1, + max_depth, + results, + )) + .await?; } } - + Ok(()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/lib.rs b/tauri/src-tauri/src/lib.rs index a1e43a4d..61f47a71 100644 --- a/tauri/src-tauri/src/lib.rs +++ b/tauri/src-tauri/src/lib.rs @@ -1,33 +1,33 @@ -pub mod commands; -pub mod terminal; -pub mod server; -pub mod state; -pub mod settings; -pub mod auto_launch; -pub mod ngrok; -pub mod auth; -pub mod terminal_detector; -pub mod cli_installer; -pub mod tray_menu; -pub mod cast; -pub mod tty_forward; -pub mod session_monitor; -pub mod port_conflict; -pub mod network_utils; -pub mod notification_manager; -pub mod welcome; -pub mod permissions; -pub mod updater; -pub mod backend_manager; -pub mod debug_features; pub mod api_testing; -pub mod auth_cache; -pub mod terminal_integrations; pub mod app_mover; -pub mod terminal_spawn_service; +pub mod auth; +pub mod auth_cache; +pub mod auto_launch; +pub mod backend_manager; +pub mod cast; +pub mod cli_installer; +pub mod commands; +pub mod debug_features; pub mod fs_api; +pub mod network_utils; +pub mod ngrok; +pub mod notification_manager; +pub mod permissions; +pub mod port_conflict; +pub mod server; +pub mod session_monitor; +pub mod settings; +pub mod state; +pub mod terminal; +pub mod terminal_detector; +pub mod terminal_integrations; +pub mod terminal_spawn_service; +pub mod tray_menu; +pub mod tty_forward; +pub mod updater; +pub mod welcome; #[cfg(mobile)] pub fn init() { // Mobile-specific initialization -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 6c04b599..508aebc2 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -3,44 +3,44 @@ windows_subsystem = "windows" )] -use tauri::{AppHandle, Manager, Emitter, WindowEvent}; -use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::menu::Menu; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; +use tauri::{AppHandle, Emitter, Manager, WindowEvent}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -mod commands; -mod terminal; -mod server; -mod state; -mod settings; -mod auto_launch; -mod ngrok; -mod terminal_detector; -mod cli_installer; -mod auth; -mod tray_menu; -mod cast; -mod tty_forward; -mod session_monitor; -mod port_conflict; -mod network_utils; -mod notification_manager; -mod welcome; -mod permissions; -mod updater; -mod backend_manager; -mod debug_features; mod api_testing; -mod auth_cache; -mod terminal_integrations; mod app_mover; -mod terminal_spawn_service; +mod auth; +mod auth_cache; +mod auto_launch; +mod backend_manager; +mod cast; +mod cli_installer; +mod commands; +mod debug_features; mod fs_api; +mod network_utils; +mod ngrok; +mod notification_manager; +mod permissions; +mod port_conflict; +mod server; +mod session_monitor; +mod settings; +mod state; +mod terminal; +mod terminal_detector; +mod terminal_integrations; +mod terminal_spawn_service; +mod tray_menu; +mod tty_forward; +mod updater; +mod welcome; -use commands::*; -use state::AppState; -use server::HttpServer; use commands::ServerStatus; +use commands::*; +use server::HttpServer; +use state::AppState; #[tauri::command] fn open_settings_window(app: AppHandle) -> Result<(), String> { @@ -53,7 +53,7 @@ fn open_settings_window(app: AppHandle) -> Result<(), String> { tauri::WebviewWindowBuilder::new( &app, "settings", - tauri::WebviewUrl::App("settings.html".into()) + tauri::WebviewUrl::App("settings.html".into()), ) .title("VibeTunnel Settings") .inner_size(800.0, 600.0) @@ -66,10 +66,13 @@ fn open_settings_window(app: AppHandle) -> Result<(), String> { Ok(()) } -fn update_tray_menu_status(_app: &AppHandle, port: u16, _session_count: usize) { - // For now, just log the status update - // TODO: In Tauri v2, dynamic menu updates require rebuilding the menu - tracing::info!("Server status updated: port {}", port); +fn update_tray_menu_status(app: &AppHandle, port: u16, session_count: usize) { + // Update tray menu status using the tray menu manager + let app_handle = app.clone(); + tauri::async_runtime::spawn(async move { + tray_menu::TrayMenuManager::update_server_status(&app_handle, port, true).await; + tray_menu::TrayMenuManager::update_session_count(&app_handle, session_count).await; + }); } fn main() { @@ -290,16 +293,16 @@ fn main() { welcome_manager.set_app_handle(app_handle2).await; permissions_manager.set_app_handle(app_handle3).await; update_manager.set_app_handle(app_handle4).await; - + // Load welcome state and check if should show welcome let _ = welcome_manager.load_state().await; if welcome_manager.should_show_welcome().await { let _ = welcome_manager.show_welcome_window().await; } - + // Check permissions on startup let _ = permissions_manager.check_all_permissions().await; - + // Check if app should be moved to Applications folder (macOS only) #[cfg(target_os = "macos")] { @@ -310,14 +313,18 @@ fn main() { let _ = app_mover::check_and_prompt_move(app_handle_move).await; }); } - + // Load updater settings and start auto-check let _ = update_manager.load_settings().await; update_manager.clone().start_auto_check().await; }); // Create system tray icon using menu-bar-icon.png with template mode - let icon_path = app.path().resource_dir().unwrap().join("icons/menu-bar-icon.png"); + let icon_path = app + .path() + .resource_dir() + .unwrap() + .join("icons/menu-bar-icon.png"); let tray_icon = if let Ok(icon_data) = std::fs::read(&icon_path) { tauri::image::Image::from_bytes(&icon_data).ok() } else { @@ -360,16 +367,17 @@ fn main() { // Load settings to determine initial dock icon visibility let settings = settings::Settings::load().unwrap_or_default(); - + // Check if launched at startup (auto-launch) - let is_auto_launched = std::env::args().any(|arg| arg == "--auto-launch" || arg == "--minimized"); - + let is_auto_launched = + std::env::args().any(|arg| arg == "--auto-launch" || arg == "--minimized"); + let window = app.get_webview_window("main").unwrap(); - + // Hide window if auto-launched if is_auto_launched { window.hide()?; - + // On macOS, apply dock icon visibility based on settings #[cfg(target_os = "macos")] { @@ -393,13 +401,15 @@ fn main() { if let WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); let _ = window_clone.hide(); - + // Hide dock icon on macOS when window is hidden (only if settings say so) #[cfg(target_os = "macos")] { if let Ok(settings) = settings::Settings::load() { if !settings.general.show_dock_icon { - let _ = window_clone.app_handle().set_activation_policy(tauri::ActivationPolicy::Accessory); + let _ = window_clone + .app_handle() + .set_activation_policy(tauri::ActivationPolicy::Accessory); } } } @@ -420,10 +430,11 @@ fn main() { } #[cfg(target_os = "macos")] +#[allow(dead_code)] fn create_app_menu(app: &tauri::App) -> Result, tauri::Error> { // Create the menu using the builder pattern let menu = Menu::new(app)?; - + // For now, return a basic menu // TODO: Once we understand the correct Tauri v2 menu API, implement full menu Ok(menu) @@ -546,7 +557,7 @@ fn show_main_window(app: AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("main") { window.show().map_err(|e| e.to_string())?; window.set_focus().map_err(|e| e.to_string())?; - + // Show dock icon on macOS when window is shown #[cfg(target_os = "macos")] { @@ -560,87 +571,111 @@ fn show_main_window(app: AppHandle) -> Result<(), String> { fn quit_app(app: AppHandle) { // Stop monitoring before exit let state = app.state::(); - state.server_monitoring.store(false, std::sync::atomic::Ordering::Relaxed); + state + .server_monitoring + .store(false, std::sync::atomic::Ordering::Relaxed); + + // Close all terminal sessions + let terminal_manager = state.terminal_manager.clone(); + tauri::async_runtime::block_on(async move { + let _ = terminal_manager.close_all_sessions().await; + }); + app.exit(0); } async fn start_server_with_monitoring(app_handle: AppHandle) { let state = app_handle.state::(); let state_clone = state.inner().clone(); - + // Start initial server match start_server_internal(&*state).await { Ok(status) => { tracing::info!("Server started on port {}", status.port); *state.server_target_port.write().await = Some(status.port); - + // Update tray menu with server status update_tray_menu_status(&app_handle, status.port, 0); - + // Show notification - let _ = state.notification_manager.notify_server_status(true, status.port).await; + let _ = state + .notification_manager + .notify_server_status(true, status.port) + .await; } Err(e) => { tracing::error!("Failed to start server: {}", e); - let _ = state.notification_manager.notify_error( - "Server Start Failed", - &format!("Failed to start server: {}", e) - ).await; + let _ = state + .notification_manager + .notify_error( + "Server Start Failed", + &format!("Failed to start server: {}", e), + ) + .await; } } - + // Monitor server health let monitoring_state = state_clone.clone(); let monitoring_app = app_handle.clone(); - + tauri::async_runtime::spawn(async move { let mut check_interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); - - while monitoring_state.server_monitoring.load(std::sync::atomic::Ordering::Relaxed) { + + while monitoring_state + .server_monitoring + .load(std::sync::atomic::Ordering::Relaxed) + { check_interval.tick().await; - + // Check if server is still running let server_running = { let server = monitoring_state.http_server.read().await; server.is_some() }; - + if server_running { // Perform health check let health_check_result = perform_server_health_check(&monitoring_state).await; - + if !health_check_result { tracing::warn!("Server health check failed, attempting restart..."); - + // Stop current server let _ = stop_server_internal(&monitoring_state).await; - + // Wait a bit before restart tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - + // Restart server match start_server_internal(&monitoring_state).await { Ok(status) => { tracing::info!("Server restarted on port {}", status.port); *monitoring_state.server_target_port.write().await = Some(status.port); - + // Update tray menu with server status update_tray_menu_status(&monitoring_app, status.port, 0); - + // Notify frontend of server restart if let Some(window) = monitoring_app.get_webview_window("main") { let _ = window.emit("server:restarted", &status); } - + // Show notification - let _ = monitoring_state.notification_manager.notify_server_status(true, status.port).await; + let _ = monitoring_state + .notification_manager + .notify_server_status(true, status.port) + .await; } Err(e) => { tracing::error!("Failed to restart server: {}", e); - let _ = monitoring_state.notification_manager.notify_error( - "Server Restart Failed", - &format!("Failed to restart server: {}", e) - ).await; + let _ = monitoring_state + .notification_manager + .notify_error( + "Server Restart Failed", + &format!("Failed to restart server: {}", e), + ) + .await; } } } @@ -649,31 +684,37 @@ async fn start_server_with_monitoring(app_handle: AppHandle) { let target_port = *monitoring_state.server_target_port.read().await; if target_port.is_some() { tracing::info!("Server not running, attempting to start..."); - + match start_server_internal(&monitoring_state).await { Ok(status) => { tracing::info!("Server started on port {}", status.port); - + // Notify frontend of server restart if let Some(window) = monitoring_app.get_webview_window("main") { let _ = window.emit("server:restarted", &status); } - + // Show notification - let _ = monitoring_state.notification_manager.notify_server_status(true, status.port).await; + let _ = monitoring_state + .notification_manager + .notify_server_status(true, status.port) + .await; } Err(e) => { tracing::error!("Failed to start server: {}", e); - let _ = monitoring_state.notification_manager.notify_error( - "Server Start Failed", - &format!("Failed to start server: {}", e) - ).await; + let _ = monitoring_state + .notification_manager + .notify_error( + "Server Start Failed", + &format!("Failed to start server: {}", e), + ) + .await; } } } } } - + tracing::info!("Server monitoring stopped"); }); } @@ -685,7 +726,7 @@ async fn perform_server_health_check(state: &AppState) -> bool { // Server reports as running, perform additional check // by trying to access the API endpoint let url = format!("http://localhost:{}/api/sessions", status.port); - + match reqwest::Client::new() .get(&url) .timeout(std::time::Duration::from_secs(2)) @@ -703,50 +744,62 @@ async fn perform_server_health_check(state: &AppState) -> bool { // Internal server management functions that work directly with AppState async fn start_server_internal(state: &AppState) -> Result { let mut server = state.http_server.write().await; - + if let Some(http_server) = server.as_ref() { // Get actual port from running server let port = http_server.port(); - + // Check if ngrok is active let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() { ngrok_tunnel.url } else { format!("http://localhost:{}", port) }; - + return Ok(ServerStatus { running: true, port, url, }); } - + // Load settings to check if password is enabled let settings = crate::settings::Settings::load().unwrap_or_default(); - + // Start HTTP server with auth if configured - let mut http_server = if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() { - let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password)); - HttpServer::with_auth(state.terminal_manager.clone(), state.session_monitor.clone(), auth_config) - } else { - HttpServer::new(state.terminal_manager.clone(), state.session_monitor.clone()) - }; - + let mut http_server = + if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() { + let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password)); + HttpServer::with_auth( + state.terminal_manager.clone(), + state.session_monitor.clone(), + auth_config, + ) + } else { + HttpServer::new( + state.terminal_manager.clone(), + state.session_monitor.clone(), + ) + }; + // Start server with appropriate access mode let (port, url) = match settings.dashboard.access_mode.as_str() { "network" => { let port = http_server.start_with_mode("network").await?; (port, format!("http://0.0.0.0:{}", port)) - }, + } "ngrok" => { // For ngrok mode, start in localhost and let ngrok handle the tunneling let port = http_server.start_with_mode("localhost").await?; - + // Try to start ngrok tunnel if auth token is configured let url = if let Some(auth_token) = settings.advanced.ngrok_auth_token { if !auth_token.is_empty() { - match state.ngrok_manager.start_tunnel(port, Some(auth_token)).await { + match state + .ngrok_manager + .start_tunnel(port, Some(auth_token)) + .await + { Ok(tunnel) => tunnel.url, Err(e) => { tracing::error!("Failed to start ngrok tunnel: {}", e); @@ -759,17 +812,17 @@ async fn start_server_internal(state: &AppState) -> Result } else { return Err("Ngrok auth token is required for ngrok access mode".to_string()); }; - + (port, url) - }, + } _ => { let port = http_server.start_with_mode("localhost").await?; (port, format!("http://localhost:{}", port)) } }; - + *server = Some(http_server); - + Ok(ServerStatus { running: true, port, @@ -779,23 +832,23 @@ async fn start_server_internal(state: &AppState) -> Result async fn stop_server_internal(state: &AppState) -> Result<(), String> { let mut server = state.http_server.write().await; - + if let Some(mut http_server) = server.take() { http_server.stop().await?; } - + // Also stop ngrok tunnel if active let _ = state.ngrok_manager.stop_tunnel().await; - + Ok(()) } async fn get_server_status_internal(state: &AppState) -> Result { let server = state.http_server.read().await; - + if let Some(http_server) = server.as_ref() { let port = http_server.port(); - + // Check if ngrok is active and return its URL let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() { ngrok_tunnel.url @@ -807,7 +860,7 @@ async fn get_server_status_internal(state: &AppState) -> Result format!("http://localhost:{}", port), } }; - + Ok(ServerStatus { running: true, port, @@ -820,4 +873,4 @@ async fn get_server_status_internal(state: &AppState) -> Result Option { // Try to get network interfaces let interfaces = Self::get_all_interfaces(); - + // First, try to find a private network address (192.168.x.x, 10.x.x.x, 172.16-31.x.x) for interface in &interfaces { if interface.is_loopback || !interface.is_up { continue; } - + for addr in &interface.addresses { if addr.is_ipv4 && addr.is_private { return Some(addr.address.clone()); } } } - + // If no private address found, return any non-loopback IPv4 for interface in &interfaces { if interface.is_loopback || !interface.is_up { continue; } - + for addr in &interface.addresses { if addr.is_ipv4 { return Some(addr.address.clone()); } } } - + None } - + /// Get all IP addresses pub fn get_all_ip_addresses() -> Vec { let interfaces = Self::get_all_interfaces(); let mut addresses = Vec::new(); - + for interface in interfaces { if interface.is_loopback { continue; } - + for addr in interface.addresses { addresses.push(addr.address); } } - + addresses } - + /// Get all network interfaces pub fn get_all_interfaces() -> Vec { #[cfg(unix)] { Self::get_interfaces_unix() } - + #[cfg(windows)] { Self::get_interfaces_windows() } - + #[cfg(not(any(unix, windows)))] { Vec::new() } } - + #[cfg(unix)] fn get_interfaces_unix() -> Vec { use nix::ifaddrs::getifaddrs; - + let mut interfaces = std::collections::HashMap::new(); - + match getifaddrs() { Ok(addrs) => { for ifaddr in addrs { let name = ifaddr.interface_name.clone(); let flags = ifaddr.flags; - - let interface = interfaces.entry(name.clone()).or_insert_with(|| NetworkInterface { - name, - addresses: Vec::new(), - is_up: flags.contains(nix::net::if_::InterfaceFlags::IFF_UP), - is_loopback: flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK), - }); - + + let interface = + interfaces + .entry(name.clone()) + .or_insert_with(|| NetworkInterface { + name, + addresses: Vec::new(), + is_up: flags.contains(nix::net::if_::InterfaceFlags::IFF_UP), + is_loopback: flags + .contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK), + }); + if let Some(address) = ifaddr.address { if let Some(sockaddr) = address.as_sockaddr_in() { let ip = IpAddr::V4(Ipv4Addr::from(sockaddr.ip())); @@ -138,21 +142,21 @@ impl NetworkUtils { error!("Failed to get network interfaces: {}", e); } } - + interfaces.into_values().collect() } - + #[cfg(windows)] fn get_interfaces_windows() -> Vec { use ipconfig::get_adapters; - + let mut interfaces = Vec::new(); - + match get_adapters() { Ok(adapters) => { for adapter in adapters { let mut addresses = Vec::new(); - + // Get IPv4 addresses for addr in adapter.ipv4_addresses() { addresses.push(IpAddress { @@ -162,7 +166,7 @@ impl NetworkUtils { is_private: Self::is_private_ipv4(addr), }); } - + // Get IPv6 addresses for addr in adapter.ipv6_addresses() { addresses.push(IpAddress { @@ -172,7 +176,7 @@ impl NetworkUtils { is_private: Self::is_private_ipv6(addr), }); } - + interfaces.push(NetworkInterface { name: adapter.friendly_name().to_string(), addresses, @@ -185,10 +189,10 @@ impl NetworkUtils { error!("Failed to get network interfaces: {}", e); } } - + interfaces } - + /// Check if an IP address is private fn is_private_ip(ip: &IpAddr) -> bool { match ip { @@ -196,29 +200,29 @@ impl NetworkUtils { IpAddr::V6(ipv6) => Self::is_private_ipv6(ipv6), } } - + /// Check if an IPv4 address is private fn is_private_ipv4(ip: &Ipv4Addr) -> bool { let octets = ip.octets(); - + // 10.0.0.0/8 if octets[0] == 10 { return true; } - + // 172.16.0.0/12 if octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31) { return true; } - + // 192.168.0.0/16 if octets[0] == 192 && octets[1] == 168 { return true; } - + false } - + /// Check if an IPv6 address is private fn is_private_ipv6(ip: &Ipv6Addr) -> bool { // Check for link-local addresses (fe80::/10) @@ -226,35 +230,35 @@ impl NetworkUtils { if segments[0] & 0xffc0 == 0xfe80 { return true; } - + // Check for unique local addresses (fc00::/7) if segments[0] & 0xfe00 == 0xfc00 { return true; } - + false } - + /// Get hostname pub fn get_hostname() -> Option { hostname::get() .ok() .and_then(|name| name.into_string().ok()) } - + /// Test network connectivity to a host pub async fn test_connectivity(host: &str, port: u16) -> bool { + use std::time::Duration; use tokio::net::TcpStream; use tokio::time::timeout; - use std::time::Duration; - + let addr = format!("{}:{}", host, port); match timeout(Duration::from_secs(3), TcpStream::connect(&addr)).await { Ok(Ok(_)) => true, _ => false, } } - + /// Get network statistics pub fn get_network_stats() -> NetworkStats { NetworkStats { @@ -278,12 +282,16 @@ pub struct NetworkStats { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_private_ipv4() { assert!(NetworkUtils::is_private_ipv4(&"10.0.0.1".parse().unwrap())); - assert!(NetworkUtils::is_private_ipv4(&"172.16.0.1".parse().unwrap())); - assert!(NetworkUtils::is_private_ipv4(&"192.168.1.1".parse().unwrap())); + assert!(NetworkUtils::is_private_ipv4( + &"172.16.0.1".parse().unwrap() + )); + assert!(NetworkUtils::is_private_ipv4( + &"192.168.1.1".parse().unwrap() + )); assert!(!NetworkUtils::is_private_ipv4(&"8.8.8.8".parse().unwrap())); } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/ngrok.rs b/tauri/src-tauri/src/ngrok.rs index 6e25c84e..03f34974 100644 --- a/tauri/src-tauri/src/ngrok.rs +++ b/tauri/src-tauri/src/ngrok.rs @@ -1,8 +1,8 @@ +use crate::state::AppState; use serde::{Deserialize, Serialize}; -use std::process::{Command, Child}; +use std::process::{Child, Command}; use std::sync::{Arc, Mutex}; use tauri::State; -use crate::state::AppState; use tracing::info; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -24,12 +24,16 @@ impl NgrokManager { tunnel_info: Arc::new(Mutex::new(None)), } } - - pub async fn start_tunnel(&self, port: u16, auth_token: Option) -> Result { + + pub async fn start_tunnel( + &self, + port: u16, + auth_token: Option, + ) -> Result { // Check if ngrok is installed let ngrok_path = which::which("ngrok") .map_err(|_| "ngrok not found. Please install ngrok first.".to_string())?; - + // Set auth token if provided if let Some(token) = auth_token { Command::new(&ngrok_path) @@ -37,72 +41,78 @@ impl NgrokManager { .output() .map_err(|e| format!("Failed to set ngrok auth token: {}", e))?; } - + // Start ngrok tunnel let child = Command::new(&ngrok_path) .args(&["http", &port.to_string(), "--log=stdout"]) .spawn() .map_err(|e| format!("Failed to start ngrok: {}", e))?; - + // Wait a bit for ngrok to start tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - + // Get tunnel information via ngrok API let tunnel_info = self.get_tunnel_info().await?; - + // Store process and tunnel info *self.process.lock().unwrap() = Some(child); *self.tunnel_info.lock().unwrap() = Some(tunnel_info.clone()); - + info!("ngrok tunnel started: {}", tunnel_info.url); - + Ok(tunnel_info) } - + pub async fn stop_tunnel(&self) -> Result<(), String> { if let Some(mut child) = self.process.lock().unwrap().take() { - child.kill() + child + .kill() .map_err(|e| format!("Failed to stop ngrok: {}", e))?; - + info!("ngrok tunnel stopped"); } - + *self.tunnel_info.lock().unwrap() = None; - + Ok(()) } - + pub fn get_tunnel_status(&self) -> Option { self.tunnel_info.lock().unwrap().clone() } - + async fn get_tunnel_info(&self) -> Result { // Query ngrok local API let response = reqwest::get("http://localhost:4040/api/tunnels") .await .map_err(|e| format!("Failed to query ngrok API: {}", e))?; - - let data: serde_json::Value = response.json() + + let data: serde_json::Value = response + .json() .await .map_err(|e| format!("Failed to parse ngrok API response: {}", e))?; - + // Extract tunnel URL - let tunnels = data["tunnels"].as_array() + let tunnels = data["tunnels"] + .as_array() .ok_or_else(|| "No tunnels found".to_string())?; - - let tunnel = tunnels.iter() + + let tunnel = tunnels + .iter() .find(|t| t["proto"].as_str() == Some("https")) .or_else(|| tunnels.first()) .ok_or_else(|| "No tunnel found".to_string())?; - - let url = tunnel["public_url"].as_str() + + let url = tunnel["public_url"] + .as_str() .ok_or_else(|| "No public URL found".to_string())?; - - let port = tunnel["config"]["addr"].as_str() + + let port = tunnel["config"]["addr"] + .as_str() .and_then(|addr| addr.split(':').last()) .and_then(|p| p.parse::().ok()) .unwrap_or(3000); - + Ok(NgrokTunnel { url: url.to_string(), port, @@ -121,15 +131,11 @@ pub async fn start_ngrok_tunnel( } #[tauri::command] -pub async fn stop_ngrok_tunnel( - state: State<'_, AppState>, -) -> Result<(), String> { +pub async fn stop_ngrok_tunnel(state: State<'_, AppState>) -> Result<(), String> { state.ngrok_manager.stop_tunnel().await } #[tauri::command] -pub async fn get_ngrok_status( - state: State<'_, AppState>, -) -> Result, String> { +pub async fn get_ngrok_status(state: State<'_, AppState>) -> Result, String> { Ok(state.ngrok_manager.get_tunnel_status()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/notification_manager.rs b/tauri/src-tauri/src/notification_manager.rs index 21971d79..45162de0 100644 --- a/tauri/src-tauri/src/notification_manager.rs +++ b/tauri/src-tauri/src/notification_manager.rs @@ -1,10 +1,10 @@ -use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; use tauri::{AppHandle, Emitter}; use tauri_plugin_notification::NotificationExt; -use std::sync::Arc; use tokio::sync::RwLock; -use std::collections::HashMap; -use chrono::{DateTime, Utc}; /// Notification type enumeration #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -127,7 +127,7 @@ impl NotificationManager { metadata: HashMap, ) -> Result { let settings = self.settings.read().await; - + // Check if notifications are enabled if !settings.enabled { return Ok("notifications_disabled".to_string()); @@ -154,12 +154,15 @@ impl NotificationManager { }; // Store notification - self.notifications.write().await.insert(notification_id.clone(), notification.clone()); + self.notifications + .write() + .await + .insert(notification_id.clone(), notification.clone()); // Add to history let mut history = self.notification_history.write().await; history.push(notification.clone()); - + // Trim history if it exceeds max size if history.len() > self.max_history_size { let drain_count = history.len() - self.max_history_size; @@ -168,8 +171,11 @@ impl NotificationManager { // Show system notification if enabled if settings.show_in_system { - match self.show_system_notification(&title, &body, notification_type).await { - Ok(_) => {}, + match self + .show_system_notification(&title, &body, notification_type) + .await + { + Ok(_) => {} Err(e) => { tracing::error!("Failed to show system notification: {}", e); } @@ -178,7 +184,8 @@ impl NotificationManager { // Emit notification event to frontend if let Some(app_handle) = self.app_handle.read().await.as_ref() { - app_handle.emit("notification:new", ¬ification) + app_handle + .emit("notification:new", ¬ification) .map_err(|e| format!("Failed to emit notification event: {}", e))?; } @@ -193,13 +200,11 @@ impl NotificationManager { notification_type: NotificationType, ) -> Result<(), String> { let app_handle_guard = self.app_handle.read().await; - let app_handle = app_handle_guard.as_ref() + let app_handle = app_handle_guard + .as_ref() .ok_or_else(|| "App handle not set".to_string())?; - let mut builder = app_handle.notification() - .builder() - .title(title) - .body(body); + let mut builder = app_handle.notification().builder().title(title).body(body); // Set icon based on notification type let icon = match notification_type { @@ -217,7 +222,8 @@ impl NotificationManager { builder = builder.icon(icon_str); } - builder.show() + builder + .show() .map_err(|e| format!("Failed to show notification: {}", e))?; Ok(()) @@ -228,13 +234,13 @@ impl NotificationManager { let mut notifications = self.notifications.write().await; if let Some(notification) = notifications.get_mut(notification_id) { notification.read = true; - + // Update history let mut history = self.notification_history.write().await; if let Some(hist_notification) = history.iter_mut().find(|n| n.id == notification_id) { hist_notification.read = true; } - + Ok(()) } else { Err("Notification not found".to_string()) @@ -247,12 +253,12 @@ impl NotificationManager { for notification in notifications.values_mut() { notification.read = true; } - + let mut history = self.notification_history.write().await; for notification in history.iter_mut() { notification.read = true; } - + Ok(()) } @@ -263,7 +269,9 @@ impl NotificationManager { /// Get unread notification count pub async fn get_unread_count(&self) -> usize { - self.notifications.read().await + self.notifications + .read() + .await .values() .filter(|n| !n.read) .count() @@ -311,50 +319,69 @@ impl NotificationManager { body, vec![], HashMap::new(), - ).await + ) + .await } /// Show update available notification - pub async fn notify_update_available(&self, version: &str, download_url: &str) -> Result { + pub async fn notify_update_available( + &self, + version: &str, + download_url: &str, + ) -> Result { let mut metadata = HashMap::new(); - metadata.insert("version".to_string(), serde_json::Value::String(version.to_string())); - metadata.insert("download_url".to_string(), serde_json::Value::String(download_url.to_string())); + metadata.insert( + "version".to_string(), + serde_json::Value::String(version.to_string()), + ); + metadata.insert( + "download_url".to_string(), + serde_json::Value::String(download_url.to_string()), + ); self.show_notification( NotificationType::UpdateAvailable, NotificationPriority::High, "Update Available".to_string(), - format!("VibeTunnel {} is now available. Click to download.", version), - vec![ - NotificationAction { - id: "download".to_string(), - label: "Download".to_string(), - action_type: "open_url".to_string(), - } - ], + format!( + "VibeTunnel {} is now available. Click to download.", + version + ), + vec![NotificationAction { + id: "download".to_string(), + label: "Download".to_string(), + action_type: "open_url".to_string(), + }], metadata, - ).await + ) + .await } /// Show permission required notification - pub async fn notify_permission_required(&self, permission: &str, reason: &str) -> Result { + pub async fn notify_permission_required( + &self, + permission: &str, + reason: &str, + ) -> Result { let mut metadata = HashMap::new(); - metadata.insert("permission".to_string(), serde_json::Value::String(permission.to_string())); + metadata.insert( + "permission".to_string(), + serde_json::Value::String(permission.to_string()), + ); self.show_notification( NotificationType::PermissionRequired, NotificationPriority::High, "Permission Required".to_string(), format!("{} permission is required: {}", permission, reason), - vec![ - NotificationAction { - id: "grant".to_string(), - label: "Grant Permission".to_string(), - action_type: "request_permission".to_string(), - } - ], + vec![NotificationAction { + id: "grant".to_string(), + label: "Grant Permission".to_string(), + action_type: "request_permission".to_string(), + }], metadata, - ).await + ) + .await } /// Show error notification @@ -366,7 +393,8 @@ impl NotificationManager { error_message.to_string(), vec![], HashMap::new(), - ).await + ) + .await } /// Show success notification @@ -378,6 +406,7 @@ impl NotificationManager { message.to_string(), vec![], HashMap::new(), - ).await + ) + .await } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/permissions.rs b/tauri/src-tauri/src/permissions.rs index 4063380f..fd8745cc 100644 --- a/tauri/src-tauri/src/permissions.rs +++ b/tauri/src-tauri/src/permissions.rs @@ -1,9 +1,9 @@ -use serde::{Serialize, Deserialize}; -use std::sync::Arc; -use tokio::sync::RwLock; -use std::collections::HashMap; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; use tauri::AppHandle; +use tokio::sync::RwLock; /// Permission type enumeration #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -75,7 +75,7 @@ impl PermissionsManager { app_handle: Arc::new(RwLock::new(None)), notification_manager: None, }; - + // Initialize default permissions tokio::spawn({ let permissions = manager.permissions.clone(); @@ -84,7 +84,7 @@ impl PermissionsManager { *permissions.write().await = default_permissions; } }); - + manager } @@ -94,137 +94,167 @@ impl PermissionsManager { } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } /// Initialize default permissions based on platform fn initialize_permissions() -> HashMap { let mut permissions = HashMap::new(); - + // Get current platform let platform = std::env::consts::OS; - + match platform { "macos" => { - permissions.insert(PermissionType::ScreenRecording, PermissionInfo { - permission_type: PermissionType::ScreenRecording, - status: PermissionStatus::NotDetermined, - required: false, - platform_specific: true, - description: "Required for recording terminal sessions with system UI".to_string(), - last_checked: None, - request_count: 0, - }); - - permissions.insert(PermissionType::Accessibility, PermissionInfo { - permission_type: PermissionType::Accessibility, - status: PermissionStatus::NotDetermined, - required: false, - platform_specific: true, - description: "Required for advanced terminal integration features".to_string(), - last_checked: None, - request_count: 0, - }); - - permissions.insert(PermissionType::NotificationAccess, PermissionInfo { - permission_type: PermissionType::NotificationAccess, - status: PermissionStatus::NotDetermined, - required: false, - platform_specific: true, - description: "Required to show system notifications".to_string(), - last_checked: None, - request_count: 0, - }); + permissions.insert( + PermissionType::ScreenRecording, + PermissionInfo { + permission_type: PermissionType::ScreenRecording, + status: PermissionStatus::NotDetermined, + required: false, + platform_specific: true, + description: "Required for recording terminal sessions with system UI" + .to_string(), + last_checked: None, + request_count: 0, + }, + ); + + permissions.insert( + PermissionType::Accessibility, + PermissionInfo { + permission_type: PermissionType::Accessibility, + status: PermissionStatus::NotDetermined, + required: false, + platform_specific: true, + description: "Required for advanced terminal integration features" + .to_string(), + last_checked: None, + request_count: 0, + }, + ); + + permissions.insert( + PermissionType::NotificationAccess, + PermissionInfo { + permission_type: PermissionType::NotificationAccess, + status: PermissionStatus::NotDetermined, + required: false, + platform_specific: true, + description: "Required to show system notifications".to_string(), + last_checked: None, + request_count: 0, + }, + ); } "windows" => { - permissions.insert(PermissionType::TerminalAccess, PermissionInfo { - permission_type: PermissionType::TerminalAccess, - status: PermissionStatus::NotDetermined, - required: true, - platform_specific: true, - description: "Required to create and manage terminal sessions".to_string(), - last_checked: None, - request_count: 0, - }); - - permissions.insert(PermissionType::AutoStart, PermissionInfo { - permission_type: PermissionType::AutoStart, - status: PermissionStatus::NotDetermined, - required: false, - platform_specific: true, - description: "Required to start VibeTunnel with Windows".to_string(), - last_checked: None, - request_count: 0, - }); + permissions.insert( + PermissionType::TerminalAccess, + PermissionInfo { + permission_type: PermissionType::TerminalAccess, + status: PermissionStatus::NotDetermined, + required: true, + platform_specific: true, + description: "Required to create and manage terminal sessions".to_string(), + last_checked: None, + request_count: 0, + }, + ); + + permissions.insert( + PermissionType::AutoStart, + PermissionInfo { + permission_type: PermissionType::AutoStart, + status: PermissionStatus::NotDetermined, + required: false, + platform_specific: true, + description: "Required to start VibeTunnel with Windows".to_string(), + last_checked: None, + request_count: 0, + }, + ); } "linux" => { - permissions.insert(PermissionType::FileSystemFull, PermissionInfo { - permission_type: PermissionType::FileSystemFull, - status: PermissionStatus::Granted, - required: true, - platform_specific: false, - description: "Required for saving recordings and configurations".to_string(), - last_checked: None, - request_count: 0, - }); + permissions.insert( + PermissionType::FileSystemFull, + PermissionInfo { + permission_type: PermissionType::FileSystemFull, + status: PermissionStatus::Granted, + required: true, + platform_specific: false, + description: "Required for saving recordings and configurations" + .to_string(), + last_checked: None, + request_count: 0, + }, + ); } _ => {} } - + // Add common permissions - permissions.insert(PermissionType::NetworkAccess, PermissionInfo { - permission_type: PermissionType::NetworkAccess, - status: PermissionStatus::Granted, - required: true, - platform_specific: false, - description: "Required for web server and remote access features".to_string(), - last_checked: None, - request_count: 0, - }); - - permissions.insert(PermissionType::FileSystemRestricted, PermissionInfo { - permission_type: PermissionType::FileSystemRestricted, - status: PermissionStatus::Granted, - required: true, - platform_specific: false, - description: "Required for basic application functionality".to_string(), - last_checked: None, - request_count: 0, - }); - + permissions.insert( + PermissionType::NetworkAccess, + PermissionInfo { + permission_type: PermissionType::NetworkAccess, + status: PermissionStatus::Granted, + required: true, + platform_specific: false, + description: "Required for web server and remote access features".to_string(), + last_checked: None, + request_count: 0, + }, + ); + + permissions.insert( + PermissionType::FileSystemRestricted, + PermissionInfo { + permission_type: PermissionType::FileSystemRestricted, + status: PermissionStatus::Granted, + required: true, + platform_specific: false, + description: "Required for basic application functionality".to_string(), + last_checked: None, + request_count: 0, + }, + ); + permissions } /// Check all permissions pub async fn check_all_permissions(&self) -> Vec { let mut permissions = self.permissions.write().await; - + for (permission_type, info) in permissions.iter_mut() { info.status = self.check_permission_internal(*permission_type).await; info.last_checked = Some(Utc::now()); } - + permissions.values().cloned().collect() } /// Check specific permission pub async fn check_permission(&self, permission_type: PermissionType) -> PermissionStatus { let status = self.check_permission_internal(permission_type).await; - + // Update stored status if let Some(info) = self.permissions.write().await.get_mut(&permission_type) { info.status = status; info.last_checked = Some(Utc::now()); } - + status } /// Internal permission checking logic async fn check_permission_internal(&self, permission_type: PermissionType) -> PermissionStatus { let platform = std::env::consts::OS; - + match (platform, permission_type) { #[cfg(target_os = "macos")] ("macos", PermissionType::ScreenRecording) => { @@ -251,14 +281,17 @@ impl PermissionsManager { } /// Request permission - pub async fn request_permission(&self, permission_type: PermissionType) -> Result { + pub async fn request_permission( + &self, + permission_type: PermissionType, + ) -> Result { // Update request count if let Some(info) = self.permissions.write().await.get_mut(&permission_type) { info.request_count += 1; } - + let platform = std::env::consts::OS; - + match (platform, permission_type) { #[cfg(target_os = "macos")] ("macos", PermissionType::ScreenRecording) => { @@ -283,7 +316,10 @@ impl PermissionsManager { } /// Get permission info - pub async fn get_permission_info(&self, permission_type: PermissionType) -> Option { + pub async fn get_permission_info( + &self, + permission_type: PermissionType, + ) -> Option { self.permissions.read().await.get(&permission_type).cloned() } @@ -294,7 +330,9 @@ impl PermissionsManager { /// Get required permissions pub async fn get_required_permissions(&self) -> Vec { - self.permissions.read().await + self.permissions + .read() + .await .values() .filter(|info| info.required) .cloned() @@ -303,7 +341,9 @@ impl PermissionsManager { /// Get missing required permissions pub async fn get_missing_required_permissions(&self) -> Vec { - self.permissions.read().await + self.permissions + .read() + .await .values() .filter(|info| info.required && info.status != PermissionStatus::Granted) .cloned() @@ -312,15 +352,21 @@ impl PermissionsManager { /// Check if all required permissions are granted pub async fn all_required_permissions_granted(&self) -> bool { - !self.permissions.read().await + !self + .permissions + .read() + .await .values() .any(|info| info.required && info.status != PermissionStatus::Granted) } /// Open system settings for permission - pub async fn open_system_settings(&self, permission_type: PermissionType) -> Result<(), String> { + pub async fn open_system_settings( + &self, + permission_type: PermissionType, + ) -> Result<(), String> { let platform = std::env::consts::OS; - + match (platform, permission_type) { #[cfg(target_os = "macos")] ("macos", PermissionType::ScreenRecording) => { @@ -335,9 +381,7 @@ impl PermissionsManager { self.open_notification_settings_macos().await } #[cfg(target_os = "windows")] - ("windows", PermissionType::AutoStart) => { - self.open_startup_settings_windows().await - } + ("windows", PermissionType::AutoStart) => self.open_startup_settings_windows().await, _ => Err("No system settings available for this permission".to_string()), } } @@ -347,12 +391,12 @@ impl PermissionsManager { async fn check_screen_recording_permission_macos(&self) -> PermissionStatus { // Use CGDisplayStream API to check screen recording permission use std::process::Command; - + let output = Command::new("osascript") .arg("-e") .arg("tell application \"System Events\" to get properties") .output(); - + match output { Ok(output) if output.status.success() => PermissionStatus::Granted, _ => PermissionStatus::NotDetermined, @@ -360,22 +404,28 @@ impl PermissionsManager { } #[cfg(target_os = "macos")] - async fn request_screen_recording_permission_macos(&self) -> Result { + async fn request_screen_recording_permission_macos( + &self, + ) -> Result { // Show notification about needing to grant permission if let Some(notification_manager) = &self.notification_manager { - let _ = notification_manager.notify_permission_required( - "Screen Recording", - "VibeTunnel needs screen recording permission to capture terminal sessions" - ).await; + let _ = notification_manager + .notify_permission_required( + "Screen Recording", + "VibeTunnel needs screen recording permission to capture terminal sessions", + ) + .await; } - + // Open system preferences let _ = self.open_screen_recording_settings_macos().await; - + Ok(PermissionRequestResult { permission_type: PermissionType::ScreenRecording, status: PermissionStatus::NotDetermined, - message: Some("Please grant screen recording permission in System Settings".to_string()), + message: Some( + "Please grant screen recording permission in System Settings".to_string(), + ), requires_restart: true, requires_system_settings: true, }) @@ -384,24 +434,24 @@ impl PermissionsManager { #[cfg(target_os = "macos")] async fn open_screen_recording_settings_macos(&self) -> Result<(), String> { use std::process::Command; - + Command::new("open") .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") .spawn() .map_err(|e| format!("Failed to open system preferences: {}", e))?; - + Ok(()) } #[cfg(target_os = "macos")] async fn check_accessibility_permission_macos(&self) -> PermissionStatus { use std::process::Command; - + let output = Command::new("osascript") .arg("-e") .arg("tell application \"System Events\" to get UI elements enabled") .output(); - + match output { Ok(output) if output.status.success() => { let result = String::from_utf8_lossy(&output.stdout); @@ -416,9 +466,11 @@ impl PermissionsManager { } #[cfg(target_os = "macos")] - async fn request_accessibility_permission_macos(&self) -> Result { + async fn request_accessibility_permission_macos( + &self, + ) -> Result { let _ = self.open_accessibility_settings_macos().await; - + Ok(PermissionRequestResult { permission_type: PermissionType::Accessibility, status: PermissionStatus::NotDetermined, @@ -431,12 +483,12 @@ impl PermissionsManager { #[cfg(target_os = "macos")] async fn open_accessibility_settings_macos(&self) -> Result<(), String> { use std::process::Command; - + Command::new("open") .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") .spawn() .map_err(|e| format!("Failed to open system preferences: {}", e))?; - + Ok(()) } @@ -447,7 +499,9 @@ impl PermissionsManager { } #[cfg(target_os = "macos")] - async fn request_notification_permission_macos(&self) -> Result { + async fn request_notification_permission_macos( + &self, + ) -> Result { Ok(PermissionRequestResult { permission_type: PermissionType::NotificationAccess, status: PermissionStatus::Granted, @@ -460,12 +514,12 @@ impl PermissionsManager { #[cfg(target_os = "macos")] async fn open_notification_settings_macos(&self) -> Result<(), String> { use std::process::Command; - + Command::new("open") .arg("x-apple.systempreferences:com.apple.preference.notifications") .spawn() .map_err(|e| format!("Failed to open system preferences: {}", e))?; - + Ok(()) } @@ -479,7 +533,7 @@ impl PermissionsManager { async fn check_auto_start_permission_windows(&self) -> PermissionStatus { // Check if auto-start is configured use crate::auto_launch; - + match auto_launch::get_auto_launch().await { Ok(enabled) => { if enabled { @@ -495,24 +549,29 @@ impl PermissionsManager { #[cfg(target_os = "windows")] async fn open_startup_settings_windows(&self) -> Result<(), String> { use std::process::Command; - + Command::new("cmd") .args(&["/c", "start", "ms-settings:startupapps"]) .spawn() .map_err(|e| format!("Failed to open startup settings: {}", e))?; - + Ok(()) } /// Show permission required notification - pub async fn notify_permission_required(&self, permission_info: &PermissionInfo) -> Result<(), String> { + pub async fn notify_permission_required( + &self, + permission_info: &PermissionInfo, + ) -> Result<(), String> { if let Some(notification_manager) = &self.notification_manager { - notification_manager.notify_permission_required( - &format!("{:?}", permission_info.permission_type), - &permission_info.description - ).await?; + notification_manager + .notify_permission_required( + &format!("{:?}", permission_info.permission_type), + &permission_info.description, + ) + .await?; } - + Ok(()) } } @@ -526,4 +585,4 @@ pub struct PermissionStats { pub required_permissions: usize, pub missing_required: usize, pub platform: String, -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/port_conflict.rs b/tauri/src-tauri/src/port_conflict.rs index 98332aee..2a2e3fcb 100644 --- a/tauri/src-tauri/src/port_conflict.rs +++ b/tauri/src-tauri/src/port_conflict.rs @@ -1,7 +1,7 @@ -use std::process::Command; +use serde::{Deserialize, Serialize}; use std::net::TcpListener; -use serde::{Serialize, Deserialize}; -use tracing::{info, error}; +use std::process::Command; +use tracing::{error, info}; /// Information about a process using a port #[derive(Debug, Clone, Serialize, Deserialize)] @@ -20,11 +20,16 @@ impl ProcessDetails { } self.name.contains("vibetunnel") || self.name.contains("VibeTunnel") } - + /// Check if this is one of our managed servers pub fn is_managed_server(&self) -> bool { - self.name == "vibetunnel" || - self.name.contains("node") && self.path.as_ref().map(|p| p.contains("VibeTunnel")).unwrap_or(false) + self.name == "vibetunnel" + || self.name.contains("node") + && self + .path + .as_ref() + .map(|p| p.contains("VibeTunnel")) + .unwrap_or(false) } } @@ -55,25 +60,25 @@ impl PortConflictResolver { pub async fn is_port_available(port: u16) -> bool { TcpListener::bind(format!("127.0.0.1:{}", port)).is_ok() } - + /// Detect what process is using a port pub async fn detect_conflict(port: u16) -> Option { // First check if port is actually in use if Self::is_port_available(port).await { return None; } - + // Platform-specific conflict detection #[cfg(target_os = "macos")] return Self::detect_conflict_macos(port).await; - + #[cfg(target_os = "linux")] return Self::detect_conflict_linux(port).await; - + #[cfg(target_os = "windows")] return Self::detect_conflict_windows(port).await; } - + #[cfg(target_os = "macos")] async fn detect_conflict_macos(port: u16) -> Option { // Use lsof to find process using the port @@ -81,23 +86,23 @@ impl PortConflictResolver { .args(&["-i", &format!(":{}", port), "-n", "-P", "-F"]) .output() .ok()?; - + if !output.status.success() { return None; } - + let stdout = String::from_utf8_lossy(&output.stdout); let process_info = Self::parse_lsof_output(&stdout)?; - + // Get root process let root_process = Self::find_root_process(&process_info).await; - + // Find alternative ports let alternatives = Self::find_available_ports(port, 3).await; - + // Determine action let action = Self::determine_action(&process_info, &root_process); - + Some(PortConflict { port, process: process_info, @@ -106,7 +111,7 @@ impl PortConflictResolver { alternative_ports: alternatives, }) } - + #[cfg(target_os = "linux")] async fn detect_conflict_linux(port: u16) -> Option { // Try lsof first @@ -120,7 +125,7 @@ impl PortConflictResolver { let root_process = Self::find_root_process(&process_info).await; let alternatives = Self::find_available_ports(port, 3).await; let action = Self::determine_action(&process_info, &root_process); - + return Some(PortConflict { port, process: process_info, @@ -131,12 +136,9 @@ impl PortConflictResolver { } } } - + // Fallback to netstat - if let Ok(output) = Command::new("netstat") - .args(&["-tulpn"]) - .output() - { + if let Ok(output) = Command::new("netstat").args(&["-tulpn"]).output() { let stdout = String::from_utf8_lossy(&output.stdout); // Parse netstat output (simplified) for line in stdout.lines() { @@ -145,17 +147,18 @@ impl PortConflictResolver { if let Some(pid_part) = line.split_whitespace().last() { if let Some(pid_str) = pid_part.split('/').next() { if let Ok(pid) = pid_str.parse::() { - let name = pid_part.split('/').nth(1).unwrap_or("unknown").to_string(); + let name = + pid_part.split('/').nth(1).unwrap_or("unknown").to_string(); let process_info = ProcessDetails { pid, name, path: None, parent_pid: None, }; - + let alternatives = Self::find_available_ports(port, 3).await; let action = Self::determine_action(&process_info, &None); - + return Some(PortConflict { port, process: process_info, @@ -169,10 +172,10 @@ impl PortConflictResolver { } } } - + None } - + #[cfg(target_os = "windows")] async fn detect_conflict_windows(port: u16) -> Option { // Use netstat to find process using the port @@ -180,9 +183,9 @@ impl PortConflictResolver { .args(&["-ano", "-p", "tcp"]) .output() .ok()?; - + let stdout = String::from_utf8_lossy(&output.stdout); - + // Parse netstat output to find the PID for line in stdout.lines() { if line.contains(&format!(":{}", port)) && line.contains("LISTENING") { @@ -205,10 +208,10 @@ impl PortConflictResolver { path: None, parent_pid: None, }; - + let alternatives = Self::find_available_ports(port, 3).await; let action = Self::determine_action(&process_info, &None); - + return Some(PortConflict { port, process: process_info, @@ -223,16 +226,16 @@ impl PortConflictResolver { } } } - + None } - + /// Parse lsof output fn parse_lsof_output(output: &str) -> Option { let mut pid: Option = None; let mut name: Option = None; let mut ppid: Option = None; - + // Parse lsof field output format for line in output.lines() { if line.starts_with('p') { @@ -243,11 +246,11 @@ impl PortConflictResolver { ppid = line[1..].parse().ok(); } } - + if let (Some(pid), Some(name)) = (pid, name) { // Get additional process info let path = Self::get_process_path(pid); - + Some(ProcessDetails { pid, name, @@ -258,7 +261,7 @@ impl PortConflictResolver { None } } - + /// Get process path fn get_process_path(pid: u32) -> Option { #[cfg(unix)] @@ -267,29 +270,27 @@ impl PortConflictResolver { .args(&["-p", &pid.to_string(), "-o", "comm="]) .output() { - let path = String::from_utf8_lossy(&output.stdout) - .trim() - .to_string(); + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !path.is_empty() { return Some(path); } } } - + None } - + /// Find root process async fn find_root_process(process: &ProcessDetails) -> Option { let mut current = process.clone(); let mut visited = std::collections::HashSet::new(); - + while let Some(parent_pid) = current.parent_pid { if parent_pid <= 1 || visited.contains(&parent_pid) { break; } visited.insert(current.pid); - + // Get parent process info if let Some(parent_info) = Self::get_process_info(parent_pid).await { // If parent is VibeTunnel, it's our root @@ -301,10 +302,10 @@ impl PortConflictResolver { break; } } - + None } - + /// Get process info by PID async fn get_process_info(pid: u32) -> Option { #[cfg(unix)] @@ -315,13 +316,13 @@ impl PortConflictResolver { { let stdout = String::from_utf8_lossy(&output.stdout); let parts: Vec<&str> = stdout.trim().split_whitespace().collect(); - + if parts.len() >= 3 { let pid = parts[0].parse().ok()?; let ppid = parts[1].parse().ok(); let name = parts[2..].join(" "); let path = Self::get_process_path(pid); - + return Some(ProcessDetails { pid, name, @@ -331,22 +332,22 @@ impl PortConflictResolver { } } } - + #[cfg(windows)] { // Windows implementation would use WMI or similar // For now, return None } - + None } - + /// Find available ports near a given port async fn find_available_ports(near_port: u16, count: usize) -> Vec { let mut available_ports = Vec::new(); let start = near_port.saturating_sub(10).max(1024); let end = near_port.saturating_add(100).min(65535); - + for port in start..=end { if port != near_port && Self::is_port_available(port).await { available_ports.push(port); @@ -355,12 +356,15 @@ impl PortConflictResolver { } } } - + available_ports } - + /// Determine action for conflict resolution - fn determine_action(process: &ProcessDetails, root_process: &Option) -> ConflictAction { + fn determine_action( + process: &ProcessDetails, + root_process: &Option, + ) -> ConflictAction { // If it's our managed server, kill it if process.is_managed_server() { return ConflictAction::KillOurInstance { @@ -368,7 +372,7 @@ impl PortConflictResolver { process_name: process.name.clone(), }; } - + // If root process is VibeTunnel, kill the whole app if let Some(root) = root_process { if root.is_vibetunnel() { @@ -378,7 +382,7 @@ impl PortConflictResolver { }; } } - + // If the process itself is VibeTunnel if process.is_vibetunnel() { return ConflictAction::KillOurInstance { @@ -386,43 +390,46 @@ impl PortConflictResolver { process_name: process.name.clone(), }; } - + // Otherwise, it's an external app ConflictAction::ReportExternalApp { name: process.name.clone(), } } - + /// Resolve a port conflict pub async fn resolve_conflict(conflict: &PortConflict) -> Result<(), String> { match &conflict.suggested_action { ConflictAction::KillOurInstance { pid, process_name } => { - info!("Killing conflicting process: {} (PID: {})", process_name, pid); - + info!( + "Killing conflicting process: {} (PID: {})", + process_name, pid + ); + #[cfg(unix)] { let output = Command::new("kill") .args(&["-9", &pid.to_string()]) .output() .map_err(|e| format!("Failed to execute kill command: {}", e))?; - + if !output.status.success() { return Err(format!("Failed to kill process {}", pid)); } } - + #[cfg(windows)] { let output = Command::new("taskkill") .args(&["/F", "/PID", &pid.to_string()]) .output() .map_err(|e| format!("Failed to execute taskkill command: {}", e))?; - + if !output.status.success() { return Err(format!("Failed to kill process {}", pid)); } } - + // Wait for port to be released tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; Ok(()) @@ -433,38 +440,41 @@ impl PortConflictResolver { } } } - + /// Force kill a process pub async fn force_kill_process(conflict: &PortConflict) -> Result<(), String> { - info!("Force killing process: {} (PID: {})", conflict.process.name, conflict.process.pid); - + info!( + "Force killing process: {} (PID: {})", + conflict.process.name, conflict.process.pid + ); + #[cfg(unix)] { let output = Command::new("kill") .args(&["-9", &conflict.process.pid.to_string()]) .output() .map_err(|e| format!("Failed to execute kill command: {}", e))?; - + if !output.status.success() { error!("Failed to kill process with regular permissions"); return Err(format!("Failed to kill process {}", conflict.process.pid)); } } - + #[cfg(windows)] { let output = Command::new("taskkill") .args(&["/F", "/PID", &conflict.process.pid.to_string()]) .output() .map_err(|e| format!("Failed to execute taskkill command: {}", e))?; - + if !output.status.success() { return Err(format!("Failed to kill process {}", conflict.process.pid)); } } - + // Wait for port to be released tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; Ok(()) } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/server.rs b/tauri/src-tauri/src/server.rs index 503f258d..54dfcf98 100644 --- a/tauri/src-tauri/src/server.rs +++ b/tauri/src-tauri/src/server.rs @@ -1,29 +1,28 @@ -use axum::{ - Router, - routing::{get, post, delete}, - response::IntoResponse, - extract::{ws::WebSocketUpgrade, Path, State as AxumState, Query}, - http::StatusCode, - Json, - middleware, -}; -use axum::extract::ws::{WebSocket, Message}; +use axum::extract::ws::{Message, WebSocket}; use axum::response::sse::{Event, KeepAlive, Sse}; +use axum::{ + extract::{ws::WebSocketUpgrade, Path, Query, State as AxumState}, + http::StatusCode, + middleware, + response::IntoResponse, + routing::{delete, get, post}, + Json, Router, +}; +use futures::sink::SinkExt; use futures::stream::{Stream, StreamExt as FuturesStreamExt}; +use serde::{Deserialize, Serialize}; use std::convert::Infallible; -use tokio::time::{interval, Duration}; -use tower_http::cors::{CorsLayer, Any}; +use std::fs; +use std::path::PathBuf; use std::sync::Arc; use tokio::net::TcpListener; -use futures::sink::SinkExt; -use serde::{Deserialize, Serialize}; -use tracing::{info, error, debug}; -use std::path::PathBuf; -use std::fs; +use tokio::time::{interval, Duration}; +use tower_http::cors::{Any, CorsLayer}; +use tracing::{debug, error, info}; -use crate::terminal::TerminalManager; -use crate::auth::{AuthConfig, auth_middleware, check_auth, login}; +use crate::auth::{auth_middleware, check_auth, login, AuthConfig}; use crate::session_monitor::SessionMonitor; +use crate::terminal::TerminalManager; // Combined app state for Axum #[derive(Clone)] @@ -99,8 +98,11 @@ impl HttpServer { pub fn port(&self) -> u16 { self.port } - - pub fn new(terminal_manager: Arc, session_monitor: Arc) -> Self { + + pub fn new( + terminal_manager: Arc, + session_monitor: Arc, + ) -> Self { Self { terminal_manager, auth_config: Arc::new(AuthConfig::new(false, None)), @@ -110,8 +112,12 @@ impl HttpServer { handle: None, } } - - pub fn with_auth(terminal_manager: Arc, session_monitor: Arc, auth_config: AuthConfig) -> Self { + + pub fn with_auth( + terminal_manager: Arc, + session_monitor: Arc, + auth_config: AuthConfig, + ) -> Self { Self { terminal_manager, auth_config: Arc::new(auth_config), @@ -122,71 +128,69 @@ impl HttpServer { } } + #[allow(dead_code)] pub async fn start(&mut self) -> Result { self.start_with_mode("localhost").await } - + pub async fn start_with_mode(&mut self, mode: &str) -> Result { // Determine bind address based on mode let bind_addr = match mode { "localhost" => "127.0.0.1:0", - "network" => "0.0.0.0:0", // Bind to all interfaces + "network" => "0.0.0.0:0", // Bind to all interfaces _ => "127.0.0.1:0", }; - + // Find available port let listener = TcpListener::bind(bind_addr) .await .map_err(|e| format!("Failed to bind to {}: {}", bind_addr, e))?; - - let addr = listener.local_addr() + + let addr = listener + .local_addr() .map_err(|e| format!("Failed to get local address: {}", e))?; - + self.port = addr.port(); - + info!("Starting HTTP server on port {}", self.port); - + // Create shutdown channel let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); self.shutdown_tx = Some(shutdown_tx); - + // Build router let app = self.build_router(); - + // Start server let handle = tokio::spawn(async move { - let server = axum::serve(listener, app) - .with_graceful_shutdown(async { - let _ = shutdown_rx.await; - info!("Graceful shutdown initiated"); - }); - + let server = axum::serve(listener, app).with_graceful_shutdown(async { + let _ = shutdown_rx.await; + info!("Graceful shutdown initiated"); + }); + if let Err(e) = server.await { error!("Server error: {}", e); } - + info!("Server task completed"); }); - + self.handle = Some(handle); - + Ok(self.port) } - + pub async fn stop(&mut self) -> Result<(), String> { info!("Stopping HTTP server..."); - + // Send shutdown signal if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()); } - + // Wait for server task to complete if let Some(handle) = self.handle.take() { - match tokio::time::timeout( - tokio::time::Duration::from_secs(10), - handle - ).await { + match tokio::time::timeout(tokio::time::Duration::from_secs(10), handle).await { Ok(Ok(())) => { info!("HTTP server stopped gracefully"); } @@ -200,26 +204,26 @@ impl HttpServer { } } } - + Ok(()) } - + fn build_router(&self) -> Router { let app_state = AppState { terminal_manager: self.terminal_manager.clone(), auth_config: self.auth_config.clone(), session_monitor: self.session_monitor.clone(), }; - + // Don't serve static files in Tauri - the frontend is served by Tauri itself // This server is only for the terminal API - + // Create auth routes that use auth config let auth_routes = Router::new() .route("/api/auth/check", get(check_auth)) .route("/api/auth/login", post(login)) .with_state(app_state.auth_config.clone()); - + // Create protected routes that use full app state let protected_routes = Router::new() .route("/api/sessions", get(list_sessions).post(create_session)) @@ -243,17 +247,19 @@ impl HttpServer { .route("/api/cleanup-exited", post(cleanup_exited)) .layer(middleware::from_fn_with_state( app_state.auth_config.clone(), - auth_middleware + auth_middleware, )) .with_state(app_state); - + Router::new() .merge(auth_routes) .merge(protected_routes) - .layer(CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any)) + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any), + ) } } @@ -262,14 +268,17 @@ async fn list_sessions( AxumState(state): AxumState, ) -> Result>, StatusCode> { let sessions = state.terminal_manager.list_sessions().await; - - let session_infos: Vec = sessions.into_iter().map(|s| SessionInfo { - id: s.id, - name: s.name, - status: "running".to_string(), - created_at: s.created_at, - }).collect(); - + + let session_infos: Vec = sessions + .into_iter() + .map(|s| SessionInfo { + id: s.id, + name: s.name, + status: "running".to_string(), + created_at: s.created_at, + }) + .collect(); + Ok(Json(session_infos)) } @@ -277,18 +286,22 @@ async fn create_session( AxumState(state): AxumState, Json(req): Json, ) -> Result, StatusCode> { - let session = state.terminal_manager.create_session( - req.name.unwrap_or_else(|| "Terminal".to_string()), - req.rows.unwrap_or(24), - req.cols.unwrap_or(80), - req.cwd, - None, - None, - ).await.map_err(|e| { - error!("Failed to create session: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - + let session = state + .terminal_manager + .create_session( + req.name.unwrap_or_else(|| "Terminal".to_string()), + req.rows.unwrap_or(24), + req.cols.unwrap_or(80), + req.cwd, + None, + None, + ) + .await + .map_err(|e| { + error!("Failed to create session: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + Ok(Json(SessionInfo { id: session.id, name: session.name, @@ -302,15 +315,18 @@ async fn get_session( AxumState(state): AxumState, ) -> Result, StatusCode> { let sessions = state.terminal_manager.list_sessions().await; - - sessions.into_iter() + + sessions + .into_iter() .find(|s| s.id == id) - .map(|s| Json(SessionInfo { - id: s.id, - name: s.name, - status: "running".to_string(), - created_at: s.created_at, - })) + .map(|s| { + Json(SessionInfo { + id: s.id, + name: s.name, + status: "running".to_string(), + created_at: s.created_at, + }) + }) .ok_or(StatusCode::NOT_FOUND) } @@ -318,7 +334,10 @@ async fn delete_session( Path(id): Path, AxumState(state): AxumState, ) -> Result { - state.terminal_manager.close_session(&id).await + state + .terminal_manager + .close_session(&id) + .await .map(|_| StatusCode::NO_CONTENT) .map_err(|_| StatusCode::NOT_FOUND) } @@ -334,7 +353,10 @@ async fn resize_session( AxumState(state): AxumState, Json(req): Json, ) -> Result { - state.terminal_manager.resize_session(&id, req.rows, req.cols).await + state + .terminal_manager + .resize_session(&id, req.rows, req.cols) + .await .map(|_| StatusCode::NO_CONTENT) .map_err(|_| StatusCode::NOT_FOUND) } @@ -353,22 +375,27 @@ async fn handle_terminal_websocket( terminal_manager: Arc, ) { let (mut sender, mut receiver) = socket.split(); - + // Get the terminal session let _session = match terminal_manager.get_session(&session_id).await { Some(s) => s, None => { - let _ = sender.send(Message::Text("Session not found".to_string())).await; + let _ = sender + .send(Message::Text("Session not found".to_string())) + .await; return; } }; - + // Spawn task to read from terminal and send to WebSocket let session_id_clone = session_id.clone(); let terminal_manager_clone = terminal_manager.clone(); let read_task = tokio::spawn(async move { loop { - match terminal_manager_clone.read_from_session(&session_id_clone).await { + match terminal_manager_clone + .read_from_session(&session_id_clone) + .await + { Ok(data) if !data.is_empty() => { if sender.send(Message::Binary(data)).await.is_err() { break; @@ -385,7 +412,7 @@ async fn handle_terminal_websocket( } } }); - + // Handle incoming WebSocket messages while let Some(msg) = receiver.next().await { match msg { @@ -399,15 +426,12 @@ async fn handle_terminal_websocket( // Handle text messages (e.g., resize commands) if let Ok(json) = serde_json::from_str::(&text) { if json["type"] == "resize" { - if let (Some(rows), Some(cols)) = ( - json["rows"].as_u64(), - json["cols"].as_u64() - ) { - let _ = terminal_manager.resize_session( - &session_id, - rows as u16, - cols as u16 - ).await; + if let (Some(rows), Some(cols)) = + (json["rows"].as_u64(), json["cols"].as_u64()) + { + let _ = terminal_manager + .resize_session(&session_id, rows as u16, cols as u16) + .await; } } } @@ -420,10 +444,10 @@ async fn handle_terminal_websocket( _ => {} } } - + // Cancel the read task read_task.abort(); - + debug!("WebSocket connection closed for session {}", session_id); } @@ -437,7 +461,10 @@ async fn send_input( AxumState(state): AxumState, Json(req): Json, ) -> Result { - state.terminal_manager.write_to_session(&id, req.input.as_bytes()).await + state + .terminal_manager + .write_to_session(&id, req.input.as_bytes()) + .await .map(|_| StatusCode::NO_CONTENT) .map_err(|_| StatusCode::NOT_FOUND) } @@ -448,14 +475,15 @@ async fn terminal_stream( ) -> Result>>, StatusCode> { // Check if session exists let sessions = state.terminal_manager.list_sessions().await; - let session = sessions.into_iter() + let session = sessions + .into_iter() .find(|s| s.id == id) .ok_or(StatusCode::NOT_FOUND)?; - + // Create the SSE stream let session_id = id.clone(); let terminal_manager = state.terminal_manager.clone(); - + let stream = async_stream::stream! { // Send initial header let header = serde_json::json!({ @@ -463,18 +491,18 @@ async fn terminal_stream( "width": session.cols, "height": session.rows }); - + yield Ok(Event::default() .event("header") .data(header.to_string())); - + // Poll for terminal output let mut poll_interval = interval(Duration::from_millis(10)); let exit_sent = false; - + loop { poll_interval.tick().await; - + // Check if session still exists let sessions = terminal_manager.list_sessions().await; if !sessions.iter().any(|s| s.id == session_id) && !exit_sent { @@ -486,7 +514,7 @@ async fn terminal_stream( let _ = exit_sent; // Prevent duplicate exit events break; } - + // Read any available output match terminal_manager.read_from_session(&session_id).await { Ok(data) if !data.is_empty() => { @@ -494,7 +522,7 @@ async fn terminal_stream( let timestamp = chrono::Utc::now().timestamp(); let output = String::from_utf8_lossy(&data); let event_data = serde_json::json!([timestamp, "o", output]); - + yield Ok(Event::default() .event("data") .data(event_data.to_string())); @@ -515,7 +543,7 @@ async fn terminal_stream( } } }; - + Ok(Sse::new(stream).keep_alive(KeepAlive::default())) } @@ -525,17 +553,16 @@ async fn session_events_stream( ) -> Result>>, StatusCode> { // Clone the session monitor Arc to avoid lifetime issues let session_monitor = state.session_monitor.clone(); - + // Start monitoring if not already started session_monitor.start_monitoring().await; - + // Create SSE stream from session monitor - let stream = session_monitor.create_sse_stream() - .map(|data| { - data.map(|json| Event::default().data(json)) - .map_err(|_| unreachable!()) - }); - + let stream = session_monitor.create_sse_stream().map(|data| { + data.map(|json| Event::default().data(json)) + .map_err(|_| unreachable!()) + }); + Ok(Sse::new(stream).keep_alive(KeepAlive::default())) } @@ -544,49 +571,50 @@ async fn browse_directory( Query(params): Query, ) -> Result, StatusCode> { let path_str = params.path.unwrap_or_else(|| "~".to_string()); - + // Expand tilde to home directory let path = if path_str.starts_with('~') { - let home = dirs::home_dir() - .ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?; + let home = dirs::home_dir().ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?; home.join(path_str.strip_prefix("~/").unwrap_or("")) } else { PathBuf::from(&path_str) }; - + // Check if path exists and is a directory if !path.exists() { return Err(StatusCode::NOT_FOUND); } - + if !path.is_dir() { return Err(StatusCode::BAD_REQUEST); } - + // Read directory entries let mut files = Vec::new(); - let entries = fs::read_dir(&path) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + let entries = fs::read_dir(&path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + for entry in entries { let entry = entry.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let metadata = entry.metadata() + let metadata = entry + .metadata() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let created = metadata.created() + + let created = metadata + .created() .map(|t| { let datetime: chrono::DateTime = t.into(); datetime.to_rfc3339() }) .unwrap_or_else(|_| String::new()); - - let modified = metadata.modified() + + let modified = metadata + .modified() .map(|t| { let datetime: chrono::DateTime = t.into(); datetime.to_rfc3339() }) .unwrap_or_else(|_| String::new()); - + files.push(FileInfo { name: entry.file_name().to_string_lossy().to_string(), created, @@ -595,54 +623,49 @@ async fn browse_directory( is_dir: metadata.is_dir(), }); } - + // Sort directories first, then files, alphabetically - files.sort_by(|a, b| { - match (a.is_dir, b.is_dir) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => a.name.cmp(&b.name), - } + files.sort_by(|a, b| match (a.is_dir, b.is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.cmp(&b.name), }); - + Ok(Json(DirectoryListing { absolute_path: path.to_string_lossy().to_string(), files, })) } -async fn create_directory( - Json(req): Json, -) -> Result { +async fn create_directory(Json(req): Json) -> Result { // Validate directory name - if req.name.is_empty() || - req.name.contains('/') || - req.name.contains('\\') || - req.name.starts_with('.') { + if req.name.is_empty() + || req.name.contains('/') + || req.name.contains('\\') + || req.name.starts_with('.') + { return Err(StatusCode::BAD_REQUEST); } - + // Expand path let base_path = if req.path.starts_with('~') { - let home = dirs::home_dir() - .ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?; + let home = dirs::home_dir().ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?; home.join(req.path.strip_prefix("~/").unwrap_or("")) } else { PathBuf::from(&req.path) }; - + // Create full path let full_path = base_path.join(&req.name); - + // Check if directory already exists if full_path.exists() { return Err(StatusCode::CONFLICT); } - + // Create directory - fs::create_dir(&full_path) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - + fs::create_dir(&full_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(StatusCode::CREATED) } @@ -652,20 +675,30 @@ async fn cleanup_exited( // Get list of all sessions let sessions = state.terminal_manager.list_sessions().await; let mut cleaned_sessions = Vec::new(); - + // Check each session and close if the process has exited for session in sessions { // Try to write empty data to check if session is alive - if state.terminal_manager.write_to_session(&session.id, &[]).await.is_err() { + if state + .terminal_manager + .write_to_session(&session.id, &[]) + .await + .is_err() + { // Session is dead, clean it up - if state.terminal_manager.close_session(&session.id).await.is_ok() { + if state + .terminal_manager + .close_session(&session.id) + .await + .is_ok() + { cleaned_sessions.push(session.id); } } } - + let count = cleaned_sessions.len(); - + Ok(Json(CleanupResponse { success: true, message: format!("{} exited sessions cleaned up", count), @@ -679,14 +712,15 @@ async fn get_snapshot( ) -> Result { // Check if session exists let sessions = state.terminal_manager.list_sessions().await; - let session = sessions.into_iter() + let session = sessions + .into_iter() .find(|s| s.id == id) .ok_or(StatusCode::NOT_FOUND)?; - + // For Tauri, we don't have access to the stream-out file like the Node.js version // Instead, we'll return a minimal snapshot with just the header // The frontend can use the regular stream endpoint for actual content - + let cast_data = serde_json::json!({ "version": 2, "width": session.cols, @@ -703,7 +737,7 @@ async fn get_snapshot( ] } }); - + // Return as a single line JSON (asciicast v2 format) Ok(cast_data.to_string()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/session_monitor.rs b/tauri/src-tauri/src/session_monitor.rs index 7a853dc5..05927bd2 100644 --- a/tauri/src-tauri/src/session_monitor.rs +++ b/tauri/src-tauri/src/session_monitor.rs @@ -1,10 +1,10 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use serde_json; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::{RwLock, mpsc}; +use tokio::sync::{mpsc, RwLock}; use tokio::time::{interval, Duration}; -use chrono::Utc; -use serde::{Serialize, Deserialize}; -use serde_json; use uuid::Uuid; /// Information about a terminal session @@ -25,19 +25,10 @@ pub struct SessionInfo { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum SessionEvent { - SessionCreated { - session: SessionInfo, - }, - SessionUpdated { - session: SessionInfo, - }, - SessionClosed { - id: String, - }, - SessionActivity { - id: String, - timestamp: String, - }, + SessionCreated { session: SessionInfo }, + SessionUpdated { session: SessionInfo }, + SessionClosed { id: String }, + SessionActivity { id: String, timestamp: String }, } /// Session monitoring service @@ -64,15 +55,15 @@ impl SessionMonitor { tokio::spawn(async move { let mut monitor_interval = interval(Duration::from_secs(5)); - + loop { monitor_interval.tick().await; - + // Get current sessions from terminal manager let current_sessions = terminal_manager.list_sessions().await; let mut sessions_map = sessions.write().await; let mut updated_sessions = HashMap::new(); - + // Check for new or updated sessions for session in current_sessions { let session_info = SessionInfo { @@ -86,51 +77,56 @@ impl SessionMonitor { is_active: true, client_count: 0, // TODO: Track actual client count }; - + // Check if this is a new session if !sessions_map.contains_key(&session.id) { // Broadcast session created event Self::broadcast_event( &subscribers, SessionEvent::SessionCreated { - session: session_info.clone() - } - ).await; + session: session_info.clone(), + }, + ) + .await; } else { // Check if session was updated if let Some(existing) = sessions_map.get(&session.id) { - if existing.rows != session_info.rows || - existing.cols != session_info.cols { + if existing.rows != session_info.rows + || existing.cols != session_info.cols + { // Broadcast session updated event Self::broadcast_event( &subscribers, SessionEvent::SessionUpdated { - session: session_info.clone() - } - ).await; + session: session_info.clone(), + }, + ) + .await; } } } - + updated_sessions.insert(session.id.clone(), session_info); } - + // Check for closed sessions - let closed_sessions: Vec = sessions_map.keys() + let closed_sessions: Vec = sessions_map + .keys() .filter(|id| !updated_sessions.contains_key(*id)) .cloned() .collect(); - + for session_id in closed_sessions { // Broadcast session closed event Self::broadcast_event( &subscribers, SessionEvent::SessionClosed { - id: session_id.clone() - } - ).await; + id: session_id.clone(), + }, + ) + .await; } - + // Update the sessions map *sessions_map = updated_sessions; } @@ -138,21 +134,27 @@ impl SessionMonitor { } /// Subscribe to session events + #[allow(dead_code)] pub async fn subscribe(&self) -> mpsc::UnboundedReceiver { let (tx, rx) = mpsc::unbounded_channel(); let subscriber_id = Uuid::new_v4().to_string(); - - self.event_subscribers.write().await.insert(subscriber_id, tx); - + + self.event_subscribers + .write() + .await + .insert(subscriber_id, tx); + rx } /// Unsubscribe from session events + #[allow(dead_code)] pub async fn unsubscribe(&self, subscriber_id: &str) { self.event_subscribers.write().await.remove(subscriber_id); } /// Get current session count + #[allow(dead_code)] pub async fn get_session_count(&self) -> usize { self.sessions.read().await.len() } @@ -163,23 +165,26 @@ impl SessionMonitor { } /// Get a specific session + #[allow(dead_code)] pub async fn get_session(&self, id: &str) -> Option { self.sessions.read().await.get(id).cloned() } /// Notify activity for a session + #[allow(dead_code)] pub async fn notify_activity(&self, session_id: &str) { if let Some(session) = self.sessions.write().await.get_mut(session_id) { session.last_activity = Utc::now().to_rfc3339(); - + // Broadcast activity event Self::broadcast_event( &self.event_subscribers, SessionEvent::SessionActivity { id: session_id.to_string(), timestamp: session.last_activity.clone(), - } - ).await; + }, + ) + .await; } } @@ -190,13 +195,13 @@ impl SessionMonitor { ) { let subscribers_read = subscribers.read().await; let mut dead_subscribers = Vec::new(); - + for (id, tx) in subscribers_read.iter() { if tx.send(event.clone()).is_err() { dead_subscribers.push(id.clone()); } } - + // Remove dead subscribers if !dead_subscribers.is_empty() { drop(subscribers_read); @@ -208,13 +213,16 @@ impl SessionMonitor { } /// Create an SSE stream for session events - pub fn create_sse_stream(self: Arc) -> impl futures::Stream> + Send + 'static { + pub fn create_sse_stream( + self: Arc, + ) -> impl futures::Stream> + Send + 'static + { async_stream::stream! { // Subscribe to events let (tx, mut rx) = mpsc::unbounded_channel(); let subscriber_id = Uuid::new_v4().to_string(); self.event_subscribers.write().await.insert(subscriber_id.clone(), tx); - + // Send initial sessions let session_list = self.sessions.read().await.values().cloned().collect::>(); let initial_event = serde_json::json!({ @@ -222,16 +230,16 @@ impl SessionMonitor { "sessions": session_list, "count": session_list.len() }); - + yield Ok(format!("data: {}\n\n", initial_event)); - + // Send events as they come while let Some(event) = rx.recv().await { if let Ok(json) = serde_json::to_string(&event) { yield Ok(format!("data: {}\n\n", json)); } } - + // Clean up subscriber on drop self.event_subscribers.write().await.remove(&subscriber_id); } @@ -254,14 +262,14 @@ impl SessionMonitor { let sessions = self.sessions.read().await; let active_sessions = sessions.values().filter(|s| s.is_active).count(); let total_clients = sessions.values().map(|s| s.client_count).sum(); - + // TODO: Track more detailed statistics SessionStats { total_sessions: sessions.len(), active_sessions, total_clients, - uptime_seconds: 0, // TODO: Track uptime + uptime_seconds: 0, // TODO: Track uptime sessions_created_today: 0, // TODO: Track daily stats } } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/settings.rs b/tauri/src-tauri/src/settings.rs index 734b7ba6..18cd3d93 100644 --- a/tauri/src-tauri/src/settings.rs +++ b/tauri/src-tauri/src/settings.rs @@ -1,9 +1,9 @@ -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use directories::ProjectDirs; -use tauri::{Manager, State}; use crate::state::AppState; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::path::PathBuf; +use tauri::{Manager, State}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GeneralSettings { @@ -193,7 +193,7 @@ impl Default for Settings { default_notification_types.insert("error".to_string(), true); default_notification_types.insert("server_status".to_string(), true); default_notification_types.insert("update_available".to_string(), true); - + let mut enabled_terminals = HashMap::new(); enabled_terminals.insert("Terminal".to_string(), true); enabled_terminals.insert("iTerm2".to_string(), true); @@ -202,7 +202,7 @@ impl Default for Settings { enabled_terminals.insert("Warp".to_string(), true); enabled_terminals.insert("Ghostty".to_string(), false); enabled_terminals.insert("WezTerm".to_string(), false); - + Self { general: GeneralSettings { launch_at_login: false, @@ -328,48 +328,45 @@ impl Default for Settings { impl Settings { pub fn load() -> Result { let config_path = Self::config_path()?; - + if !config_path.exists() { return Ok(Self::default()); } - + let contents = std::fs::read_to_string(&config_path) .map_err(|e| format!("Failed to read settings: {}", e))?; - - toml::from_str(&contents) - .map_err(|e| format!("Failed to parse settings: {}", e)) + + toml::from_str(&contents).map_err(|e| format!("Failed to parse settings: {}", e)) } - + pub fn save(&self) -> Result<(), String> { let config_path = Self::config_path()?; - + // Ensure the config directory exists if let Some(parent) = config_path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("Failed to create config directory: {}", e))?; } - + let contents = toml::to_string_pretty(self) .map_err(|e| format!("Failed to serialize settings: {}", e))?; - + std::fs::write(&config_path, contents) .map_err(|e| format!("Failed to write settings: {}", e))?; - + Ok(()) } - + fn config_path() -> Result { let proj_dirs = ProjectDirs::from("com", "vibetunnel", "VibeTunnel") .ok_or_else(|| "Failed to get project directories".to_string())?; - + Ok(proj_dirs.config_dir().join("settings.toml")) } } #[tauri::command] -pub async fn get_settings( - _state: State<'_, AppState>, -) -> Result { +pub async fn get_settings(_state: State<'_, AppState>) -> Result { Settings::load() } @@ -380,20 +377,23 @@ pub async fn save_settings( app: tauri::AppHandle, ) -> Result<(), String> { settings.save()?; - + // Apply settings that need immediate effect if settings.general.launch_at_login { crate::auto_launch::enable_auto_launch()?; } else { crate::auto_launch::disable_auto_launch()?; } - + // Apply dock icon visibility on macOS #[cfg(target_os = "macos")] { // Check if any windows are visible - let has_visible_windows = app.windows().values().any(|w| w.is_visible().unwrap_or(false)); - + let has_visible_windows = app + .windows() + .values() + .any(|w| w.is_visible().unwrap_or(false)); + if !has_visible_windows && !settings.general.show_dock_icon { // Hide dock icon if no windows are visible and setting is disabled let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory); @@ -403,6 +403,6 @@ pub async fn save_settings( } // Note: If windows are visible, we always show the dock icon regardless of setting } - + Ok(()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/state.rs b/tauri/src-tauri/src/state.rs index 5930447b..2d2f02e2 100644 --- a/tauri/src-tauri/src/state.rs +++ b/tauri/src-tauri/src/state.rs @@ -1,22 +1,22 @@ -use tokio::sync::RwLock; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use crate::terminal::TerminalManager; -use crate::server::HttpServer; -use crate::ngrok::NgrokManager; -use crate::cast::CastManager; -use crate::tty_forward::TTYForwardManager; -use crate::session_monitor::SessionMonitor; -use crate::notification_manager::NotificationManager; -use crate::welcome::WelcomeManager; -use crate::permissions::PermissionsManager; -use crate::updater::UpdateManager; -use crate::backend_manager::BackendManager; -use crate::debug_features::DebugFeaturesManager; use crate::api_testing::APITestingManager; use crate::auth_cache::AuthCacheManager; +use crate::backend_manager::BackendManager; +use crate::cast::CastManager; +use crate::debug_features::DebugFeaturesManager; +use crate::ngrok::NgrokManager; +use crate::notification_manager::NotificationManager; +use crate::permissions::PermissionsManager; +use crate::server::HttpServer; +use crate::session_monitor::SessionMonitor; +use crate::terminal::TerminalManager; use crate::terminal_integrations::TerminalIntegrationsManager; use crate::terminal_spawn_service::TerminalSpawnService; +use crate::tty_forward::TTYForwardManager; +use crate::updater::UpdateManager; +use crate::welcome::WelcomeManager; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use tokio::sync::RwLock; #[derive(Clone)] pub struct AppState { @@ -44,40 +44,40 @@ impl AppState { pub fn new() -> Self { let mut terminal_manager = TerminalManager::new(); let cast_manager = Arc::new(CastManager::new()); - + // Connect terminal manager to cast manager terminal_manager.set_cast_manager(cast_manager.clone()); - + let terminal_manager = Arc::new(terminal_manager); let session_monitor = Arc::new(SessionMonitor::new(terminal_manager.clone())); let notification_manager = Arc::new(NotificationManager::new()); let mut permissions_manager = PermissionsManager::new(); permissions_manager.set_notification_manager(notification_manager.clone()); - + let current_version = env!("CARGO_PKG_VERSION").to_string(); let mut update_manager = UpdateManager::new(current_version); update_manager.set_notification_manager(notification_manager.clone()); - + let mut backend_manager = BackendManager::new(); backend_manager.set_notification_manager(notification_manager.clone()); - + let mut debug_features_manager = DebugFeaturesManager::new(); debug_features_manager.set_notification_manager(notification_manager.clone()); - + let mut api_testing_manager = APITestingManager::new(); api_testing_manager.set_notification_manager(notification_manager.clone()); - + let mut auth_cache_manager = AuthCacheManager::new(); auth_cache_manager.set_notification_manager(notification_manager.clone()); - + let mut terminal_integrations_manager = TerminalIntegrationsManager::new(); terminal_integrations_manager.set_notification_manager(notification_manager.clone()); - + let terminal_integrations_manager = Arc::new(terminal_integrations_manager); let terminal_spawn_service = Arc::new(TerminalSpawnService::new( - terminal_integrations_manager.clone() + terminal_integrations_manager.clone(), )); - + Self { terminal_manager, http_server: Arc::new(RwLock::new(None)), @@ -99,4 +99,4 @@ impl AppState { terminal_spawn_service, } } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/terminal.rs b/tauri/src-tauri/src/terminal.rs index 87235d68..83245fa0 100644 --- a/tauri/src-tauri/src/terminal.rs +++ b/tauri/src-tauri/src/terminal.rs @@ -1,13 +1,13 @@ -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::io::{Read, Write}; -use portable_pty::{CommandBuilder, PtySize, native_pty_system, PtyPair, Child}; -use tokio::sync::{mpsc, RwLock}; -use bytes::Bytes; -use uuid::Uuid; -use chrono::Utc; -use tracing::{info, error, debug}; use crate::cast::CastManager; +use bytes::Bytes; +use chrono::Utc; +use portable_pty::{native_pty_system, Child, CommandBuilder, PtyPair, PtySize}; +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::sync::{Arc, Mutex}; +use tokio::sync::{mpsc, RwLock}; +use tracing::{debug, error, info}; +use uuid::Uuid; #[derive(Clone)] pub struct TerminalManager { @@ -24,9 +24,12 @@ pub struct TerminalSession { pub created_at: String, pub cwd: String, pty_pair: PtyPair, + #[allow(dead_code)] child: Box, writer: Box, + #[allow(dead_code)] reader_thread: Option>, + #[allow(dead_code)] output_tx: mpsc::UnboundedSender, pub output_rx: Arc>>, } @@ -53,7 +56,7 @@ impl TerminalManager { shell: Option, ) -> Result { let id = Uuid::new_v4().to_string(); - + // Set up PTY let pty_system = native_pty_system(); let pty_pair = pty_system @@ -77,12 +80,12 @@ impl TerminalManager { }); let mut cmd = CommandBuilder::new(&shell); - + // Set working directory if let Some(cwd) = &cwd { cmd.cwd(cwd); } - + // Set environment variables if let Some(env_vars) = env { for (key, value) in env_vars { @@ -97,16 +100,16 @@ impl TerminalManager { .map_err(|e| format!("Failed to spawn shell: {}", e))?; let pid = child.process_id().unwrap_or(0); - + // Set up output channel let (output_tx, output_rx) = mpsc::unbounded_channel(); - + // Get reader and writer let reader = pty_pair .master .try_clone_reader() .map_err(|e| format!("Failed to clone reader: {}", e))?; - + let writer = pty_pair .master .take_writer() @@ -119,7 +122,7 @@ impl TerminalManager { let reader_thread = std::thread::spawn(move || { let mut reader = reader; let mut buffer = [0u8; 4096]; - + loop { match reader.read(&mut buffer) { Ok(0) => { @@ -128,7 +131,7 @@ impl TerminalManager { } Ok(n) => { let data = Bytes::copy_from_slice(&buffer[..n]); - + // Record output to cast file if recording if let Some(cast_manager) = &cast_manager_clone { let cm = cast_manager.clone(); @@ -138,7 +141,7 @@ impl TerminalManager { let _ = cm.add_output(&sid, &data_clone).await; }); } - + if output_tx_clone.send(data).is_err() { debug!("Output channel closed"); break; @@ -159,7 +162,12 @@ impl TerminalManager { rows, cols, created_at: Utc::now().to_rfc3339(), - cwd: cwd.unwrap_or_else(|| std::env::current_dir().unwrap().to_string_lossy().to_string()), + cwd: cwd.unwrap_or_else(|| { + std::env::current_dir() + .unwrap() + .to_string_lossy() + .to_string() + }), pty_pair, child, writer, @@ -169,7 +177,10 @@ impl TerminalManager { }; // Store session - self.sessions.write().await.insert(id.clone(), Arc::new(RwLock::new(session))); + self.sessions + .write() + .await + .insert(id.clone(), Arc::new(RwLock::new(session))); info!("Created terminal session: {} ({})", name, id); @@ -186,7 +197,7 @@ impl TerminalManager { pub async fn list_sessions(&self) -> Vec { let sessions = self.sessions.read().await; let mut result = Vec::new(); - + for (id, session) in sessions.iter() { let session = session.read().await; result.push(crate::commands::Terminal { @@ -198,7 +209,7 @@ impl TerminalManager { created_at: session.created_at.clone(), }); } - + result } @@ -209,26 +220,26 @@ impl TerminalManager { pub async fn close_all_sessions(&self) -> Result<(), String> { let mut sessions = self.sessions.write().await; let session_count = sessions.len(); - + // Clear all sessions sessions.clear(); - + info!("Closed all {} terminal sessions", session_count); Ok(()) } - + pub async fn close_session(&self, id: &str) -> Result<(), String> { let mut sessions = self.sessions.write().await; - + if let Some(session_arc) = sessions.remove(id) { // Stop recording if active if let Some(cast_manager) = &self.cast_manager { let _ = cast_manager.remove_recorder(id).await; } - + // Session will be dropped when it goes out of scope drop(session_arc); - + info!("Closed terminal session: {}", id); Ok(()) } else { @@ -239,8 +250,9 @@ impl TerminalManager { pub async fn resize_session(&self, id: &str, rows: u16, cols: u16) -> Result<(), String> { if let Some(session_arc) = self.get_session(id).await { let mut session = session_arc.write().await; - - session.pty_pair + + session + .pty_pair .master .resize(PtySize { rows, @@ -249,10 +261,10 @@ impl TerminalManager { pixel_height: 0, }) .map_err(|e| format!("Failed to resize PTY: {}", e))?; - + session.rows = rows; session.cols = cols; - + // Update recorder dimensions if recording if let Some(cast_manager) = &self.cast_manager { if let Some(recorder) = cast_manager.get_recorder(id).await { @@ -260,7 +272,7 @@ impl TerminalManager { rec.resize(cols, rows).await; } } - + debug!("Resized terminal {} to {}x{}", id, cols, rows); Ok(()) } else { @@ -271,20 +283,22 @@ impl TerminalManager { pub async fn write_to_session(&self, id: &str, data: &[u8]) -> Result<(), String> { if let Some(session_arc) = self.get_session(id).await { let mut session = session_arc.write().await; - + // Record input to cast file if recording if let Some(cast_manager) = &self.cast_manager { let _ = cast_manager.add_input(id, data).await; } - - session.writer + + session + .writer .write_all(data) .map_err(|e| format!("Failed to write to PTY: {}", e))?; - - session.writer + + session + .writer .flush() .map_err(|e| format!("Failed to flush PTY: {}", e))?; - + Ok(()) } else { Err(format!("Session not found: {}", id)) @@ -295,7 +309,7 @@ impl TerminalManager { if let Some(session_arc) = self.get_session(id).await { let session = session_arc.read().await; let mut rx = session.output_rx.lock().unwrap(); - + // Try to receive data without blocking match rx.try_recv() { Ok(data) => Ok(data.to_vec()), @@ -312,4 +326,4 @@ impl TerminalManager { // Make TerminalSession Send + Sync unsafe impl Send for TerminalSession {} -unsafe impl Sync for TerminalSession {} \ No newline at end of file +unsafe impl Sync for TerminalSession {} diff --git a/tauri/src-tauri/src/terminal_detector.rs b/tauri/src-tauri/src/terminal_detector.rs index 4c40e14c..5aa16adc 100644 --- a/tauri/src-tauri/src/terminal_detector.rs +++ b/tauri/src-tauri/src/terminal_detector.rs @@ -21,10 +21,7 @@ pub fn detect_terminals() -> Result { #[cfg(target_os = "macos")] { // Check for Terminal.app - if let Ok(_) = Command::new("open") - .args(&["-Ra", "Terminal.app"]) - .output() - { + if let Ok(_) = Command::new("open").args(&["-Ra", "Terminal.app"]).output() { available_terminals.push(TerminalInfo { name: "Terminal".to_string(), path: "/System/Applications/Utilities/Terminal.app".to_string(), @@ -33,10 +30,7 @@ pub fn detect_terminals() -> Result { } // Check for iTerm2 - if let Ok(_) = Command::new("open") - .args(&["-Ra", "iTerm.app"]) - .output() - { + if let Ok(_) = Command::new("open").args(&["-Ra", "iTerm.app"]).output() { available_terminals.push(TerminalInfo { name: "iTerm2".to_string(), path: "/Applications/iTerm.app".to_string(), @@ -45,10 +39,7 @@ pub fn detect_terminals() -> Result { } // Check for Warp - if let Ok(output) = Command::new("which") - .arg("warp") - .output() - { + if let Ok(output) = Command::new("which").arg("warp").output() { if output.status.success() { available_terminals.push(TerminalInfo { name: "Warp".to_string(), @@ -59,10 +50,7 @@ pub fn detect_terminals() -> Result { } // Check for Hyper - if let Ok(_) = Command::new("open") - .args(&["-Ra", "Hyper.app"]) - .output() - { + if let Ok(_) = Command::new("open").args(&["-Ra", "Hyper.app"]).output() { available_terminals.push(TerminalInfo { name: "Hyper".to_string(), path: "/Applications/Hyper.app".to_string(), @@ -71,10 +59,7 @@ pub fn detect_terminals() -> Result { } // Check for Alacritty - if let Ok(output) = Command::new("which") - .arg("alacritty") - .output() - { + if let Ok(output) = Command::new("which").arg("alacritty").output() { if output.status.success() { available_terminals.push(TerminalInfo { name: "Alacritty".to_string(), @@ -114,10 +99,7 @@ pub fn detect_terminals() -> Result { #[cfg(target_os = "windows")] { // Check for Windows Terminal - if let Ok(output) = Command::new("where") - .arg("wt.exe") - .output() - { + if let Ok(output) = Command::new("where").arg("wt.exe").output() { if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); available_terminals.push(TerminalInfo { @@ -134,10 +116,7 @@ pub fn detect_terminals() -> Result { } // Check for PowerShell - if let Ok(output) = Command::new("where") - .arg("powershell.exe") - .output() - { + if let Ok(output) = Command::new("where").arg("powershell.exe").output() { if output.status.success() { available_terminals.push(TerminalInfo { name: "PowerShell".to_string(), @@ -148,10 +127,7 @@ pub fn detect_terminals() -> Result { } // Check for Command Prompt - if let Ok(output) = Command::new("where") - .arg("cmd.exe") - .output() - { + if let Ok(output) = Command::new("where").arg("cmd.exe").output() { if output.status.success() { available_terminals.push(TerminalInfo { name: "Command Prompt".to_string(), @@ -187,10 +163,7 @@ pub fn detect_terminals() -> Result { ]; for (cmd, name) in terminals { - if let Ok(output) = Command::new("which") - .arg(cmd) - .output() - { + if let Ok(output) = Command::new("which").arg(cmd).output() { if output.status.success() { available_terminals.push(TerminalInfo { name: name.to_string(), @@ -205,17 +178,20 @@ pub fn detect_terminals() -> Result { if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") { match desktop.to_lowercase().as_str() { "gnome" | "ubuntu" => { - default_terminal = available_terminals.iter() + default_terminal = available_terminals + .iter() .find(|t| t.name == "GNOME Terminal") .cloned(); } "kde" => { - default_terminal = available_terminals.iter() + default_terminal = available_terminals + .iter() .find(|t| t.name == "Konsole") .cloned(); } "xfce" => { - default_terminal = available_terminals.iter() + default_terminal = available_terminals + .iter() .find(|t| t.name == "XFCE Terminal") .cloned(); } @@ -247,7 +223,7 @@ pub async fn get_default_shell() -> Result { if let Ok(shell) = std::env::var("SHELL") { return Ok(shell); } - + // Fallback to common shells let shells = vec!["/bin/zsh", "/bin/bash", "/bin/sh"]; for shell in shells { @@ -256,22 +232,19 @@ pub async fn get_default_shell() -> Result { } } } - + #[cfg(windows)] { // On Windows, default to PowerShell - if let Ok(output) = Command::new("where") - .arg("powershell.exe") - .output() - { + if let Ok(output) = Command::new("where").arg("powershell.exe").output() { if output.status.success() { return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); } } - + // Fallback to cmd return Ok("cmd.exe".to_string()); } - + Err("Could not detect default shell".to_string()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/terminal_integrations.rs b/tauri/src-tauri/src/terminal_integrations.rs index 3a57cd6d..8af72c78 100644 --- a/tauri/src-tauri/src/terminal_integrations.rs +++ b/tauri/src-tauri/src/terminal_integrations.rs @@ -1,29 +1,29 @@ -use serde::{Serialize, Deserialize}; -use std::sync::Arc; -use tokio::sync::RwLock; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use std::process::Command; +use std::sync::Arc; +use tokio::sync::RwLock; /// Terminal emulator type #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum TerminalEmulator { SystemDefault, - Terminal, // macOS Terminal.app - ITerm2, // iTerm2 - Hyper, // Hyper - Alacritty, // Alacritty - Kitty, // Kitty - WezTerm, // WezTerm - Ghostty, // Ghostty - Warp, // Warp + Terminal, // macOS Terminal.app + ITerm2, // iTerm2 + Hyper, // Hyper + Alacritty, // Alacritty + Kitty, // Kitty + WezTerm, // WezTerm + Ghostty, // Ghostty + Warp, // Warp WindowsTerminal, // Windows Terminal - ConEmu, // ConEmu - Cmder, // Cmder - Gnome, // GNOME Terminal - Konsole, // KDE Konsole - Xterm, // XTerm - Custom, // Custom terminal + ConEmu, // ConEmu + Cmder, // Cmder + Gnome, // GNOME Terminal + Konsole, // KDE Konsole + Xterm, // XTerm + Custom, // Custom terminal } impl TerminalEmulator { @@ -141,7 +141,7 @@ impl TerminalIntegrationsManager { async move { let default_configs = Self::initialize_default_configs(); *configs.write().await = default_configs; - + let default_schemes = Self::initialize_url_schemes(); *url_schemes.write().await = default_schemes; } @@ -151,7 +151,10 @@ impl TerminalIntegrationsManager { } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } @@ -160,123 +163,148 @@ impl TerminalIntegrationsManager { let mut configs = HashMap::new(); // WezTerm configuration - configs.insert(TerminalEmulator::WezTerm, TerminalConfig { - emulator: TerminalEmulator::WezTerm, - name: "WezTerm".to_string(), - executable_path: PathBuf::from("/Applications/WezTerm.app/Contents/MacOS/wezterm"), - args_template: vec![ - "start".to_string(), - "--cwd".to_string(), - "{working_directory}".to_string(), - "--".to_string(), - "{command}".to_string(), - "{args}".to_string(), - ], - env_vars: HashMap::new(), - features: TerminalFeatures { - supports_tabs: true, - supports_splits: true, - supports_profiles: true, - supports_themes: true, - supports_scripting: true, - supports_url_scheme: false, - supports_remote_control: true, + configs.insert( + TerminalEmulator::WezTerm, + TerminalConfig { + emulator: TerminalEmulator::WezTerm, + name: "WezTerm".to_string(), + executable_path: PathBuf::from("/Applications/WezTerm.app/Contents/MacOS/wezterm"), + args_template: vec![ + "start".to_string(), + "--cwd".to_string(), + "{working_directory}".to_string(), + "--".to_string(), + "{command}".to_string(), + "{args}".to_string(), + ], + env_vars: HashMap::new(), + features: TerminalFeatures { + supports_tabs: true, + supports_splits: true, + supports_profiles: true, + supports_themes: true, + supports_scripting: true, + supports_url_scheme: false, + supports_remote_control: true, + }, + platform: vec![ + "macos".to_string(), + "windows".to_string(), + "linux".to_string(), + ], }, - platform: vec!["macos".to_string(), "windows".to_string(), "linux".to_string()], - }); + ); // Ghostty configuration - configs.insert(TerminalEmulator::Ghostty, TerminalConfig { - emulator: TerminalEmulator::Ghostty, - name: "Ghostty".to_string(), - executable_path: PathBuf::from("/Applications/Ghostty.app/Contents/MacOS/ghostty"), - args_template: vec![ - "--working-directory".to_string(), - "{working_directory}".to_string(), - "--command".to_string(), - "{command}".to_string(), - "{args}".to_string(), - ], - env_vars: HashMap::new(), - features: TerminalFeatures { - supports_tabs: true, - supports_splits: true, - supports_profiles: true, - supports_themes: true, - supports_scripting: false, - supports_url_scheme: false, - supports_remote_control: false, + configs.insert( + TerminalEmulator::Ghostty, + TerminalConfig { + emulator: TerminalEmulator::Ghostty, + name: "Ghostty".to_string(), + executable_path: PathBuf::from("/Applications/Ghostty.app/Contents/MacOS/ghostty"), + args_template: vec![ + "--working-directory".to_string(), + "{working_directory}".to_string(), + "--command".to_string(), + "{command}".to_string(), + "{args}".to_string(), + ], + env_vars: HashMap::new(), + features: TerminalFeatures { + supports_tabs: true, + supports_splits: true, + supports_profiles: true, + supports_themes: true, + supports_scripting: false, + supports_url_scheme: false, + supports_remote_control: false, + }, + platform: vec!["macos".to_string()], }, - platform: vec!["macos".to_string()], - }); + ); // iTerm2 configuration - configs.insert(TerminalEmulator::ITerm2, TerminalConfig { - emulator: TerminalEmulator::ITerm2, - name: "iTerm2".to_string(), - executable_path: PathBuf::from("/Applications/iTerm.app/Contents/MacOS/iTerm2"), - args_template: vec![], - env_vars: HashMap::new(), - features: TerminalFeatures { - supports_tabs: true, - supports_splits: true, - supports_profiles: true, - supports_themes: true, - supports_scripting: true, - supports_url_scheme: true, - supports_remote_control: true, + configs.insert( + TerminalEmulator::ITerm2, + TerminalConfig { + emulator: TerminalEmulator::ITerm2, + name: "iTerm2".to_string(), + executable_path: PathBuf::from("/Applications/iTerm.app/Contents/MacOS/iTerm2"), + args_template: vec![], + env_vars: HashMap::new(), + features: TerminalFeatures { + supports_tabs: true, + supports_splits: true, + supports_profiles: true, + supports_themes: true, + supports_scripting: true, + supports_url_scheme: true, + supports_remote_control: true, + }, + platform: vec!["macos".to_string()], }, - platform: vec!["macos".to_string()], - }); + ); // Alacritty configuration - configs.insert(TerminalEmulator::Alacritty, TerminalConfig { - emulator: TerminalEmulator::Alacritty, - name: "Alacritty".to_string(), - executable_path: PathBuf::from("/Applications/Alacritty.app/Contents/MacOS/alacritty"), - args_template: vec![ - "--working-directory".to_string(), - "{working_directory}".to_string(), - "-e".to_string(), - "{command}".to_string(), - "{args}".to_string(), - ], - env_vars: HashMap::new(), - features: TerminalFeatures { - supports_tabs: false, - supports_splits: false, - supports_profiles: true, - supports_themes: true, - supports_scripting: false, - supports_url_scheme: false, - supports_remote_control: false, + configs.insert( + TerminalEmulator::Alacritty, + TerminalConfig { + emulator: TerminalEmulator::Alacritty, + name: "Alacritty".to_string(), + executable_path: PathBuf::from( + "/Applications/Alacritty.app/Contents/MacOS/alacritty", + ), + args_template: vec![ + "--working-directory".to_string(), + "{working_directory}".to_string(), + "-e".to_string(), + "{command}".to_string(), + "{args}".to_string(), + ], + env_vars: HashMap::new(), + features: TerminalFeatures { + supports_tabs: false, + supports_splits: false, + supports_profiles: true, + supports_themes: true, + supports_scripting: false, + supports_url_scheme: false, + supports_remote_control: false, + }, + platform: vec![ + "macos".to_string(), + "windows".to_string(), + "linux".to_string(), + ], }, - platform: vec!["macos".to_string(), "windows".to_string(), "linux".to_string()], - }); + ); // Kitty configuration - configs.insert(TerminalEmulator::Kitty, TerminalConfig { - emulator: TerminalEmulator::Kitty, - name: "Kitty".to_string(), - executable_path: PathBuf::from("/Applications/kitty.app/Contents/MacOS/kitty"), - args_template: vec![ - "--directory".to_string(), - "{working_directory}".to_string(), - "{command}".to_string(), - "{args}".to_string(), - ], - env_vars: HashMap::new(), - features: TerminalFeatures { - supports_tabs: true, - supports_splits: true, - supports_profiles: true, - supports_themes: true, - supports_scripting: true, - supports_url_scheme: false, - supports_remote_control: true, + configs.insert( + TerminalEmulator::Kitty, + TerminalConfig { + emulator: TerminalEmulator::Kitty, + name: "Kitty".to_string(), + executable_path: PathBuf::from("/Applications/kitty.app/Contents/MacOS/kitty"), + args_template: vec![ + "--directory".to_string(), + "{working_directory}".to_string(), + "{command}".to_string(), + "{args}".to_string(), + ], + env_vars: HashMap::new(), + features: TerminalFeatures { + supports_tabs: true, + supports_splits: true, + supports_profiles: true, + supports_themes: true, + supports_scripting: true, + supports_url_scheme: false, + supports_remote_control: true, + }, + platform: vec!["macos".to_string(), "linux".to_string()], }, - platform: vec!["macos".to_string(), "linux".to_string()], - }); + ); configs } @@ -285,12 +313,15 @@ impl TerminalIntegrationsManager { fn initialize_url_schemes() -> HashMap { let mut schemes = HashMap::new(); - schemes.insert(TerminalEmulator::ITerm2, TerminalURLScheme { - scheme: "iterm2".to_string(), - supports_ssh: true, - supports_local: true, - template: "iterm2://ssh/{user}@{host}:{port}".to_string(), - }); + schemes.insert( + TerminalEmulator::ITerm2, + TerminalURLScheme { + scheme: "iterm2".to_string(), + supports_ssh: true, + supports_local: true, + template: "iterm2://ssh/{user}@{host}:{port}".to_string(), + }, + ); schemes } @@ -304,7 +335,10 @@ impl TerminalIntegrationsManager { let info = self.check_terminal_installation(emulator, config).await; if info.installed { detected.push(info.clone()); - self.detected_terminals.write().await.insert(*emulator, info); + self.detected_terminals + .write() + .await + .insert(*emulator, info); } } @@ -316,10 +350,15 @@ impl TerminalIntegrationsManager { } /// Check if a specific terminal is installed - async fn check_terminal_installation(&self, emulator: &TerminalEmulator, config: &TerminalConfig) -> TerminalIntegrationInfo { + async fn check_terminal_installation( + &self, + emulator: &TerminalEmulator, + config: &TerminalConfig, + ) -> TerminalIntegrationInfo { let installed = config.executable_path.exists(); let version = if installed { - self.get_terminal_version(emulator, &config.executable_path).await + self.get_terminal_version(emulator, &config.executable_path) + .await } else { None }; @@ -328,31 +367,39 @@ impl TerminalIntegrationsManager { emulator: *emulator, installed, version, - path: if installed { Some(config.executable_path.clone()) } else { None }, + path: if installed { + Some(config.executable_path.clone()) + } else { + None + }, is_default: false, - config: if installed { Some(config.clone()) } else { None }, + config: if installed { + Some(config.clone()) + } else { + None + }, } } /// Get terminal version - async fn get_terminal_version(&self, emulator: &TerminalEmulator, path: &PathBuf) -> Option { + async fn get_terminal_version( + &self, + emulator: &TerminalEmulator, + path: &PathBuf, + ) -> Option { match emulator { - TerminalEmulator::WezTerm => { - Command::new(path) - .arg("--version") - .output() - .ok() - .and_then(|output| String::from_utf8(output.stdout).ok()) - .map(|v| v.trim().to_string()) - } - TerminalEmulator::Alacritty => { - Command::new(path) - .arg("--version") - .output() - .ok() - .and_then(|output| String::from_utf8(output.stdout).ok()) - .map(|v| v.trim().to_string()) - } + TerminalEmulator::WezTerm => Command::new(path) + .arg("--version") + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|v| v.trim().to_string()), + TerminalEmulator::Alacritty => Command::new(path) + .arg("--version") + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|v| v.trim().to_string()), _ => None, } } @@ -413,10 +460,12 @@ impl TerminalIntegrationsManager { // Notify user if let Some(notification_manager) = &self.notification_manager { - let _ = notification_manager.notify_success( - "Default Terminal Changed", - &format!("Default terminal set to {}", emulator.display_name()) - ).await; + let _ = notification_manager + .notify_success( + "Default Terminal Changed", + &format!("Default terminal set to {}", emulator.display_name()), + ) + .await; } Ok(()) @@ -461,7 +510,8 @@ impl TerminalIntegrationsManager { options: TerminalLaunchOptions, ) -> Result<(), String> { let configs = self.configs.read().await; - let config = configs.get(&emulator) + let config = configs + .get(&emulator) .ok_or_else(|| "Terminal configuration not found".to_string())?; let mut command = Command::new(&config.executable_path); @@ -488,7 +538,8 @@ impl TerminalIntegrationsManager { } // Launch terminal - command.spawn() + command + .spawn() .map_err(|e| format!("Failed to launch terminal: {}", e))?; Ok(()) @@ -503,11 +554,16 @@ impl TerminalIntegrationsManager { script.push_str(" activate\n"); if options.tab { - script.push_str(" tell application \"System Events\" to keystroke \"t\" using command down\n"); + script.push_str( + " tell application \"System Events\" to keystroke \"t\" using command down\n", + ); } if let Some(cwd) = options.working_directory { - script.push_str(&format!(" do script \"cd '{}'\" in front window\n", cwd.display())); + script.push_str(&format!( + " do script \"cd '{}'\" in front window\n", + cwd.display() + )); } if let Some(command) = options.command { @@ -516,7 +572,10 @@ impl TerminalIntegrationsManager { } else { format!("{} {}", command, options.args.join(" ")) }; - script.push_str(&format!(" do script \"{}\" in front window\n", full_command)); + script.push_str(&format!( + " do script \"{}\" in front window\n", + full_command + )); } script.push_str("end tell\n"); @@ -552,7 +611,8 @@ impl TerminalIntegrationsManager { } } - command.spawn() + command + .spawn() .map_err(|e| format!("Failed to launch Windows Terminal: {}", e))?; Ok(()) @@ -565,7 +625,7 @@ impl TerminalIntegrationsManager { // Try common terminal emulators let terminals = ["gnome-terminal", "konsole", "xfce4-terminal", "xterm"]; - + for terminal in &terminals { if let Ok(output) = Command::new("which").arg(terminal).output() { if output.status.success() { @@ -600,7 +660,8 @@ impl TerminalIntegrationsManager { } } - return command.spawn() + return command + .spawn() .map_err(|e| format!("Failed to launch terminal: {}", e)) .map(|_| ()); } @@ -620,7 +681,8 @@ impl TerminalIntegrationsManager { ) -> Option { let schemes = self.url_schemes.read().await; schemes.get(&emulator).map(|scheme| { - scheme.template + scheme + .template .replace("{user}", user) .replace("{host}", host) .replace("{port}", &port.to_string()) @@ -639,11 +701,20 @@ impl TerminalIntegrationsManager { /// List detected terminals pub async fn list_detected_terminals(&self) -> Vec { - self.detected_terminals.read().await.values().cloned().collect() + self.detected_terminals + .read() + .await + .values() + .cloned() + .collect() } // Helper methods - fn replace_template_variables(&self, template: &str, options: &TerminalLaunchOptions) -> String { + fn replace_template_variables( + &self, + template: &str, + options: &TerminalLaunchOptions, + ) -> String { let mut result = template.to_string(); if let Some(cwd) = &options.working_directory { @@ -677,4 +748,4 @@ pub struct TerminalIntegrationStats { pub installed_terminals: usize, pub default_terminal: TerminalEmulator, pub terminals_by_platform: HashMap>, -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/terminal_spawn_service.rs b/tauri/src-tauri/src/terminal_spawn_service.rs index bee48384..329cbddb 100644 --- a/tauri/src-tauri/src/terminal_spawn_service.rs +++ b/tauri/src-tauri/src/terminal_spawn_service.rs @@ -1,6 +1,6 @@ -use tokio::sync::mpsc; -use std::sync::Arc; use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::mpsc; /// Request to spawn a terminal #[derive(Debug, Clone, Serialize, Deserialize)] @@ -23,17 +23,20 @@ pub struct TerminalSpawnResponse { /// Terminal Spawn Service - manages background terminal spawning pub struct TerminalSpawnService { request_tx: mpsc::Sender, + #[allow(dead_code)] terminal_integrations_manager: Arc, } impl TerminalSpawnService { pub fn new( - terminal_integrations_manager: Arc, + terminal_integrations_manager: Arc< + crate::terminal_integrations::TerminalIntegrationsManager, + >, ) -> Self { let (tx, mut rx) = mpsc::channel::(100); - + let manager_clone = terminal_integrations_manager.clone(); - + // Spawn background worker to handle terminal spawn requests tokio::spawn(async move { while let Some(request) = rx.recv().await { @@ -43,23 +46,27 @@ impl TerminalSpawnService { }); } }); - + Self { request_tx: tx, terminal_integrations_manager, } } - + /// Queue a terminal spawn request pub async fn spawn_terminal(&self, request: TerminalSpawnRequest) -> Result<(), String> { - self.request_tx.send(request).await + self.request_tx + .send(request) + .await .map_err(|e| format!("Failed to queue terminal spawn: {}", e)) } - + /// Handle a spawn request async fn handle_spawn_request( request: TerminalSpawnRequest, - terminal_integrations_manager: Arc, + terminal_integrations_manager: Arc< + crate::terminal_integrations::TerminalIntegrationsManager, + >, ) -> Result { // Determine which terminal to use let terminal_type = if let Some(terminal) = &request.terminal_type { @@ -78,11 +85,13 @@ impl TerminalSpawnService { } else { terminal_integrations_manager.get_default_terminal().await }; - + // Build launch options let mut launch_options = crate::terminal_integrations::TerminalLaunchOptions { command: request.command, - working_directory: request.working_directory.map(|s| std::path::PathBuf::from(s)), + working_directory: request + .working_directory + .map(|s| std::path::PathBuf::from(s)), args: vec![], env_vars: request.environment.unwrap_or_default(), title: Some(format!("VibeTunnel Session {}", request.session_id)), @@ -91,16 +100,22 @@ impl TerminalSpawnService { split: None, window_size: None, }; - + // If no command specified, create a VibeTunnel session command if launch_options.command.is_none() { // Get server status to build the correct URL let port = 4020; // Default port, should get from settings - launch_options.command = Some(format!("vt connect localhost:{}/{}", port, request.session_id)); + launch_options.command = Some(format!( + "vt connect localhost:{}/{}", + port, request.session_id + )); } - + // Launch the terminal - match terminal_integrations_manager.launch_terminal(Some(terminal_type), launch_options).await { + match terminal_integrations_manager + .launch_terminal(Some(terminal_type), launch_options) + .await + { Ok(_) => Ok(TerminalSpawnResponse { success: true, error: None, @@ -113,7 +128,7 @@ impl TerminalSpawnService { }), } } - + /// Spawn terminal for a specific session pub async fn spawn_terminal_for_session( &self, @@ -127,10 +142,10 @@ impl TerminalSpawnService { working_directory: None, environment: None, }; - + self.spawn_terminal(request).await } - + /// Spawn terminal with custom command pub async fn spawn_terminal_with_command( &self, @@ -145,7 +160,7 @@ impl TerminalSpawnService { working_directory, environment: None, }; - + self.spawn_terminal(request).await } } @@ -158,7 +173,9 @@ pub async fn spawn_terminal_for_session( state: tauri::State<'_, crate::state::AppState>, ) -> Result<(), String> { let spawn_service = &state.terminal_spawn_service; - spawn_service.spawn_terminal_for_session(session_id, terminal_type).await + spawn_service + .spawn_terminal_for_session(session_id, terminal_type) + .await } #[tauri::command] @@ -169,7 +186,9 @@ pub async fn spawn_terminal_with_command( state: tauri::State<'_, crate::state::AppState>, ) -> Result<(), String> { let spawn_service = &state.terminal_spawn_service; - spawn_service.spawn_terminal_with_command(command, working_directory, terminal_type).await + spawn_service + .spawn_terminal_with_command(command, working_directory, terminal_type) + .await } #[tauri::command] @@ -179,4 +198,4 @@ pub async fn spawn_custom_terminal( ) -> Result<(), String> { let spawn_service = &state.terminal_spawn_service; spawn_service.spawn_terminal(request).await -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/tray_menu.rs b/tauri/src-tauri/src/tray_menu.rs index bc430d67..cc0e170f 100644 --- a/tauri/src-tauri/src/tray_menu.rs +++ b/tauri/src-tauri/src/tray_menu.rs @@ -1,5 +1,5 @@ +use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder}; use tauri::AppHandle; -use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder, Menu}; pub struct TrayMenuManager; @@ -10,35 +10,33 @@ impl TrayMenuManager { .id("server_status") .enabled(false) .build(app)?; - + // Dashboard access let dashboard = MenuItemBuilder::new("Open Dashboard") .id("dashboard") .build(app)?; - + // Session info let sessions_info = MenuItemBuilder::new("0 active sessions") .id("sessions_info") .enabled(false) .build(app)?; - + // Help submenu let show_tutorial = MenuItemBuilder::new("Show Tutorial") .id("show_tutorial") .build(app)?; - - let website = MenuItemBuilder::new("Website") - .id("website") - .build(app)?; - + + let website = MenuItemBuilder::new("Website").id("website").build(app)?; + let report_issue = MenuItemBuilder::new("Report Issue") .id("report_issue") .build(app)?; - + let check_updates = MenuItemBuilder::new("Check for Updates...") .id("check_updates") .build(app)?; - + // Version info (disabled menu item) - read from Cargo.toml let version = env!("CARGO_PKG_VERSION"); let version_text = format!("Version {}", version); @@ -46,11 +44,11 @@ impl TrayMenuManager { .id("version_info") .enabled(false) .build(app)?; - + let about = MenuItemBuilder::new("About VibeTunnel") .id("about") .build(app)?; - + let help_menu = SubmenuBuilder::new(app, "Help") .item(&show_tutorial) .separator() @@ -63,17 +61,15 @@ impl TrayMenuManager { .separator() .item(&about) .build()?; - + // Settings let settings = MenuItemBuilder::new("Settings...") .id("settings") .build(app)?; - + // Quit - let quit = MenuItemBuilder::new("Quit") - .id("quit") - .build(app)?; - + let quit = MenuItemBuilder::new("Quit").id("quit").build(app)?; + // Build the complete menu - matching Mac app exactly let menu = MenuBuilder::new(app) .item(&server_status) @@ -86,10 +82,10 @@ impl TrayMenuManager { .separator() .item(&quit) .build()?; - + Ok(menu) } - + pub async fn update_server_status(app: &AppHandle, port: u16, running: bool) { if let Some(_tray) = app.tray_by_id("main") { let status_text = if running { @@ -97,16 +93,16 @@ impl TrayMenuManager { } else { "Server: Stopped".to_string() }; - + // Note: In Tauri v2, dynamic menu updates require rebuilding the menu // For now, we'll just log the status tracing::debug!("Server status: {}", status_text); - + // TODO: Implement menu rebuilding for dynamic updates // This would involve recreating the entire menu with updated text } } - + pub async fn update_session_count(app: &AppHandle, count: usize) { if let Some(_tray) = app.tray_by_id("main") { let text = if count == 0 { @@ -116,13 +112,13 @@ impl TrayMenuManager { } else { format!("{} active sessions", count) }; - + tracing::debug!("Session count: {}", text); - + // TODO: Implement menu rebuilding for dynamic updates } } - + pub async fn update_access_mode(_app: &AppHandle, mode: &str) { // Update checkmarks in access mode menu let _modes = vec![ @@ -130,10 +126,10 @@ impl TrayMenuManager { ("access_network", mode == "network"), ("access_ngrok", mode == "ngrok"), ]; - + // Note: In Tauri v2, we need to rebuild the menu to update checkmarks tracing::debug!("Access mode updated to: {}", mode); - + // TODO: Implement menu rebuilding for dynamic updates } -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/tty_forward.rs b/tauri/src-tauri/src/tty_forward.rs index fbef241f..faef1f99 100644 --- a/tauri/src-tauri/src/tty_forward.rs +++ b/tauri/src-tauri/src/tty_forward.rs @@ -1,13 +1,13 @@ -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{mpsc, RwLock, oneshot}; -use std::io::{Read, Write}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use portable_pty::{CommandBuilder, PtySize, native_pty_system}; -use uuid::Uuid; -use tracing::{info, error}; use bytes::Bytes; +use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::{mpsc, oneshot, RwLock}; +use tracing::{error, info}; +use uuid::Uuid; /// Represents a forwarded TTY session pub struct ForwardedSession { @@ -42,16 +42,17 @@ impl TTYForwardManager { shell: Option, ) -> Result { let id = Uuid::new_v4().to_string(); - + // Create TCP listener let listener = TcpListener::bind(format!("127.0.0.1:{}", local_port)) .await .map_err(|e| format!("Failed to bind to port {}: {}", local_port, e))?; - - let actual_port = listener.local_addr() + + let actual_port = listener + .local_addr() .map_err(|e| format!("Failed to get local address: {}", e))? .port(); - + // Create session let session = ForwardedSession { id: id.clone(), @@ -61,14 +62,14 @@ impl TTYForwardManager { connected: false, client_count: 0, }; - + // Store session self.sessions.write().await.insert(id.clone(), session); - + // Create shutdown channel let (shutdown_tx, shutdown_rx) = oneshot::channel(); self.listeners.write().await.insert(id.clone(), shutdown_tx); - + // Start listening for connections let sessions = self.sessions.clone(); let session_id = id.clone(); @@ -81,7 +82,7 @@ impl TTYForwardManager { } }) }); - + tokio::spawn(async move { Self::accept_connections( listener, @@ -91,9 +92,10 @@ impl TTYForwardManager { remote_port, shell, shutdown_rx, - ).await; + ) + .await; }); - + info!("Started TTY forward on port {} (ID: {})", actual_port, id); Ok(id) } @@ -114,18 +116,18 @@ impl TTYForwardManager { match accept_result { Ok((stream, addr)) => { info!("New TTY forward connection from {}", addr); - + // Update client count if let Some(session) = sessions.write().await.get_mut(&session_id) { session.client_count += 1; session.connected = true; } - + // Handle the connection let sessions_clone = sessions.clone(); let session_id_clone = session_id.clone(); let shell_clone = shell.clone(); - + tokio::spawn(async move { if let Err(e) = Self::handle_client( stream, @@ -135,7 +137,7 @@ impl TTYForwardManager { ).await { error!("Error handling TTY forward client: {}", e); } - + // Decrease client count if let Some(session) = sessions_clone.write().await.get_mut(&session_id_clone) { session.client_count = session.client_count.saturating_sub(1); @@ -188,7 +190,7 @@ impl TTYForwardManager { .master .try_clone_reader() .map_err(|e| format!("Failed to clone reader: {}", e))?; - + let mut writer = pty_pair .master .take_writer() @@ -221,7 +223,7 @@ impl TTYForwardManager { } }); - // Task 2: Read from PTY and write to TCP + // Task 2: Read from PTY and write to TCP let pty_to_tcp = tokio::spawn(async move { while let Some(data) = rx_from_pty.recv().await { if tcp_writer.write_all(&data).await.is_err() { @@ -262,7 +264,7 @@ impl TTYForwardManager { .enable_all() .build() .unwrap(); - + rt.block_on(async { while let Some(data) = rx_from_tcp.recv().await { if writer.write_all(&data).is_err() { @@ -293,19 +295,21 @@ impl TTYForwardManager { pub async fn stop_forward(&self, id: &str) -> Result<(), String> { // Remove session self.sessions.write().await.remove(id); - + // Send shutdown signal if let Some(shutdown_tx) = self.listeners.write().await.remove(id) { let _ = shutdown_tx.send(()); } - + info!("Stopped TTY forward session: {}", id); Ok(()) } /// List all active forwarding sessions pub async fn list_forwards(&self) -> Vec { - self.sessions.read().await + self.sessions + .read() + .await .values() .map(|s| ForwardedSession { id: s.id.clone(), @@ -320,36 +324,38 @@ impl TTYForwardManager { /// Get a specific forwarding session pub async fn get_forward(&self, id: &str) -> Option { - self.sessions.read().await.get(id).map(|s| ForwardedSession { - id: s.id.clone(), - local_port: s.local_port, - remote_host: s.remote_host.clone(), - remote_port: s.remote_port, - connected: s.connected, - client_count: s.client_count, - }) + self.sessions + .read() + .await + .get(id) + .map(|s| ForwardedSession { + id: s.id.clone(), + local_port: s.local_port, + remote_host: s.remote_host.clone(), + remote_port: s.remote_port, + connected: s.connected, + client_count: s.client_count, + }) } } /// HTTP endpoint handler for terminal spawn requests -pub async fn handle_terminal_spawn( - port: u16, - _shell: Option, -) -> Result<(), String> { +pub async fn handle_terminal_spawn(port: u16, _shell: Option) -> Result<(), String> { // Listen for HTTP requests on the specified port let listener = TcpListener::bind(format!("127.0.0.1:{}", port)) .await .map_err(|e| format!("Failed to bind spawn listener: {}", e))?; - + info!("Terminal spawn service listening on port {}", port); - + loop { - let (stream, addr) = listener.accept() + let (stream, addr) = listener + .accept() .await .map_err(|e| format!("Failed to accept spawn connection: {}", e))?; - + info!("Terminal spawn request from {}", addr); - + // Handle the spawn request tokio::spawn(async move { if let Err(e) = handle_spawn_request(stream, None).await { @@ -360,18 +366,16 @@ pub async fn handle_terminal_spawn( } /// Handle a single terminal spawn request -async fn handle_spawn_request( - mut stream: TcpStream, - _shell: Option, -) -> Result<(), String> { +async fn handle_spawn_request(mut stream: TcpStream, _shell: Option) -> Result<(), String> { // Simple HTTP response let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nTerminal spawned\r\n"; - stream.write_all(response) + stream + .write_all(response) .await .map_err(|e| format!("Failed to write response: {}", e))?; - + // TODO: Implement actual terminal spawning logic // This would integrate with the system's terminal emulator - + Ok(()) -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/updater.rs b/tauri/src-tauri/src/updater.rs index 623acb82..e3b48399 100644 --- a/tauri/src-tauri/src/updater.rs +++ b/tauri/src-tauri/src/updater.rs @@ -1,9 +1,9 @@ -use serde::{Serialize, Deserialize}; +use chrono::{DateTime, TimeZone, Utc}; +use serde::{Deserialize, Serialize}; use std::sync::Arc; -use tokio::sync::RwLock; -use chrono::{DateTime, Utc, TimeZone}; use tauri::{AppHandle, Emitter}; use tauri_plugin_updater::UpdaterExt; +use tokio::sync::RwLock; /// Update channel type #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -23,7 +23,7 @@ impl UpdateChannel { UpdateChannel::Custom => "custom", } } - + pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "stable" => UpdateChannel::Stable, @@ -155,7 +155,10 @@ impl UpdateManager { } /// Set the notification manager - pub fn set_notification_manager(&mut self, notification_manager: Arc) { + pub fn set_notification_manager( + &mut self, + notification_manager: Arc, + ) { self.notification_manager = Some(notification_manager); } @@ -166,12 +169,13 @@ impl UpdateManager { let mut updater_settings = self.settings.write().await; updater_settings.channel = UpdateChannel::from_str(&update_settings.channel); updater_settings.check_on_startup = true; - updater_settings.check_interval_hours = match update_settings.check_frequency.as_str() { - "daily" => 24, - "weekly" => 168, - "monthly" => 720, - _ => 24, - }; + updater_settings.check_interval_hours = + match update_settings.check_frequency.as_str() { + "daily" => 24, + "weekly" => 168, + "monthly" => 720, + _ => 24, + }; updater_settings.auto_download = update_settings.auto_download; updater_settings.auto_install = update_settings.auto_install; updater_settings.show_release_notes = update_settings.show_release_notes; @@ -189,7 +193,7 @@ impl UpdateManager { /// Update settings pub async fn update_settings(&self, settings: UpdaterSettings) -> Result<(), String> { *self.settings.write().await = settings.clone(); - + // Save to persistent settings if let Ok(mut app_settings) = crate::settings::Settings::load() { app_settings.updates = Some(crate::settings::UpdateSettings { @@ -207,7 +211,7 @@ impl UpdateManager { }); app_settings.save()?; } - + Ok(()) } @@ -224,30 +228,37 @@ impl UpdateManager { state.status = UpdateStatus::Checking; state.last_error = None; } - + // Emit checking event self.emit_update_event("checking", None).await; - + let app_handle_guard = self.app_handle.read().await; - let app_handle = app_handle_guard.as_ref() + let app_handle = app_handle_guard + .as_ref() .ok_or_else(|| "App handle not set".to_string())?; - + // Get the updater instance let updater = app_handle.updater_builder(); - + // Configure updater based on settings let settings = self.settings.read().await; - + // Build updater with channel-specific endpoint let updater_result = match settings.channel { UpdateChannel::Stable => updater.endpoints(vec![ - "https://releases.vibetunnel.com/stable/{{target}}/{{arch}}/{{current_version}}".parse().unwrap() + "https://releases.vibetunnel.com/stable/{{target}}/{{arch}}/{{current_version}}" + .parse() + .unwrap(), ]), UpdateChannel::Beta => updater.endpoints(vec![ - "https://releases.vibetunnel.com/beta/{{target}}/{{arch}}/{{current_version}}".parse().unwrap() + "https://releases.vibetunnel.com/beta/{{target}}/{{arch}}/{{current_version}}" + .parse() + .unwrap(), ]), UpdateChannel::Nightly => updater.endpoints(vec![ - "https://releases.vibetunnel.com/nightly/{{target}}/{{arch}}/{{current_version}}".parse().unwrap() + "https://releases.vibetunnel.com/nightly/{{target}}/{{arch}}/{{current_version}}" + .parse() + .unwrap(), ]), UpdateChannel::Custom => { if let Some(endpoint) = &settings.custom_endpoint { @@ -260,90 +271,97 @@ impl UpdateManager { } } }; - + // Build and check match updater_result { Ok(updater_builder) => match updater_builder.build() { Ok(updater) => { - match updater.check().await { - Ok(Some(update)) => { - let update_info = UpdateInfo { - version: update.version.clone(), - notes: update.body.clone().unwrap_or_default(), - pub_date: update.date.map(|d| Utc.timestamp_opt(d.unix_timestamp(), 0).single().unwrap_or(Utc::now())), - download_size: None, // TODO: Get from update - signature: None, - download_url: String::new(), // Will be set by updater - channel: settings.channel, - }; - - // Update state - { + match updater.check().await { + Ok(Some(update)) => { + let update_info = UpdateInfo { + version: update.version.clone(), + notes: update.body.clone().unwrap_or_default(), + pub_date: update.date.map(|d| { + Utc.timestamp_opt(d.unix_timestamp(), 0) + .single() + .unwrap_or(Utc::now()) + }), + download_size: None, // TODO: Get from update + signature: None, + download_url: String::new(), // Will be set by updater + channel: settings.channel, + }; + + // Update state + { + let mut state = self.state.write().await; + state.status = UpdateStatus::Available; + state.available_update = Some(update_info.clone()); + state.last_check = Some(Utc::now()); + } + + // Emit available event + self.emit_update_event("available", Some(&update_info)) + .await; + + // Show notification + if let Some(notification_manager) = &self.notification_manager { + let _ = notification_manager + .notify_update_available( + &update_info.version, + &update_info.download_url, + ) + .await; + } + + // Auto-download if enabled + if settings.auto_download { + let _ = self.download_update().await; + } + + Ok(Some(update_info)) + } + Ok(None) => { + // No update available let mut state = self.state.write().await; - state.status = UpdateStatus::Available; - state.available_update = Some(update_info.clone()); + state.status = UpdateStatus::NoUpdate; state.last_check = Some(Utc::now()); + + self.emit_update_event("no-update", None).await; + + Ok(None) } - - // Emit available event - self.emit_update_event("available", Some(&update_info)).await; - - // Show notification - if let Some(notification_manager) = &self.notification_manager { - let _ = notification_manager.notify_update_available( - &update_info.version, - &update_info.download_url - ).await; + Err(e) => { + let error_msg = format!("Failed to check for updates: {}", e); + + let mut state = self.state.write().await; + state.status = UpdateStatus::Error; + state.last_error = Some(error_msg.clone()); + state.last_check = Some(Utc::now()); + + self.emit_update_event("error", None).await; + + Err(error_msg) } - - // Auto-download if enabled - if settings.auto_download { - let _ = self.download_update().await; - } - - Ok(Some(update_info)) - } - Ok(None) => { - // No update available - let mut state = self.state.write().await; - state.status = UpdateStatus::NoUpdate; - state.last_check = Some(Utc::now()); - - self.emit_update_event("no-update", None).await; - - Ok(None) - } - Err(e) => { - let error_msg = format!("Failed to check for updates: {}", e); - - let mut state = self.state.write().await; - state.status = UpdateStatus::Error; - state.last_error = Some(error_msg.clone()); - state.last_check = Some(Utc::now()); - - self.emit_update_event("error", None).await; - - Err(error_msg) } } - } - Err(e) => { - let error_msg = format!("Failed to build updater: {}", e); - - let mut state = self.state.write().await; - state.status = UpdateStatus::Error; - state.last_error = Some(error_msg.clone()); - - Err(error_msg) - } + Err(e) => { + let error_msg = format!("Failed to build updater: {}", e); + + let mut state = self.state.write().await; + state.status = UpdateStatus::Error; + state.last_error = Some(error_msg.clone()); + + Err(error_msg) + } }, Err(e) => { let error_msg = format!("Failed to configure updater endpoints: {}", e); - + let mut state = self.state.write().await; state.status = UpdateStatus::Error; state.last_error = Some(error_msg.clone()); - + Err(error_msg) } } @@ -355,11 +373,11 @@ impl UpdateManager { let state = self.state.read().await; state.available_update.is_some() }; - + if !update_available { return Err("No update available to download".to_string()); } - + // Update status { let mut state = self.state.write().await; @@ -372,28 +390,28 @@ impl UpdateManager { eta_seconds: None, }); } - + self.emit_update_event("downloading", None).await; - + // TODO: Implement actual download with progress tracking // For now, simulate download completion tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - + // Update status to ready { let mut state = self.state.write().await; state.status = UpdateStatus::Ready; state.progress = None; } - + self.emit_update_event("ready", None).await; - + // Auto-install if enabled let settings = self.settings.read().await; if settings.auto_install { let _ = self.install_update().await; } - + Ok(()) } @@ -406,17 +424,17 @@ impl UpdateManager { } state.available_update.clone() }; - + let update_info = update_info.ok_or_else(|| "No update available".to_string())?; - + // Update status { let mut state = self.state.write().await; state.status = UpdateStatus::Installing; } - + self.emit_update_event("installing", None).await; - + // Add to history { let mut state = self.state.write().await; @@ -430,19 +448,19 @@ impl UpdateManager { notes: Some(update_info.notes.clone()), }); } - + // TODO: Implement actual installation // For now, return success - + self.emit_update_event("installed", None).await; - + Ok(()) } /// Cancel update pub async fn cancel_update(&self) -> Result<(), String> { let mut state = self.state.write().await; - + match state.status { UpdateStatus::Downloading => { // TODO: Cancel download @@ -459,15 +477,15 @@ impl UpdateManager { let mut settings = self.settings.write().await; settings.channel = channel; drop(settings); - + // Save settings self.update_settings(self.get_settings().await).await?; - + // Clear current update info when switching channels let mut state = self.state.write().await; state.available_update = None; state.status = UpdateStatus::Idle; - + Ok(()) } @@ -486,10 +504,11 @@ impl UpdateManager { if !settings.check_on_startup { return; } - - let check_interval = std::time::Duration::from_secs(settings.check_interval_hours as u64 * 3600); + + let check_interval = + std::time::Duration::from_secs(settings.check_interval_hours as u64 * 3600); drop(settings); - + tokio::spawn(async move { loop { let _ = self.check_for_updates().await; @@ -506,7 +525,7 @@ impl UpdateManager { "update": update_info, "state": self.get_state().await, }); - + let _ = app_handle.emit("updater:event", event_data); } } @@ -520,4 +539,4 @@ pub struct UpdateCheckResult { pub latest_version: Option, pub channel: UpdateChannel, pub checked_at: DateTime, -} \ No newline at end of file +} diff --git a/tauri/src-tauri/src/welcome.rs b/tauri/src-tauri/src/welcome.rs index 501cf6fb..8ad0d87a 100644 --- a/tauri/src-tauri/src/welcome.rs +++ b/tauri/src-tauri/src/welcome.rs @@ -1,9 +1,9 @@ -use serde::{Serialize, Deserialize}; -use std::sync::Arc; -use tokio::sync::RwLock; -use std::collections::HashMap; use chrono::{DateTime, Utc}; -use tauri::{AppHandle, Manager, Emitter}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tauri::{AppHandle, Emitter, Manager}; +use tokio::sync::RwLock; /// Tutorial step structure #[derive(Debug, Clone, Serialize, Deserialize)] @@ -73,7 +73,7 @@ impl WelcomeManager { tutorials: Arc::new(RwLock::new(Vec::new())), app_handle: Arc::new(RwLock::new(None)), }; - + // Initialize default tutorials tokio::spawn({ let tutorials = manager.tutorials.clone(); @@ -82,7 +82,7 @@ impl WelcomeManager { *tutorials.write().await = default_tutorials; } }); - + manager } @@ -98,7 +98,7 @@ impl WelcomeManager { // Check if this is first launch based on settings let mut state = self.state.write().await; state.first_launch = settings.general.show_welcome_on_startup.unwrap_or(true); - + // Mark first launch as false for next time if state.first_launch { state.onboarding_date = Some(Utc::now()); @@ -110,13 +110,14 @@ impl WelcomeManager { /// Save welcome state pub async fn save_state(&self) -> Result<(), String> { let state = self.state.read().await; - + // Update settings to reflect welcome state if let Ok(mut settings) = crate::settings::Settings::load() { - settings.general.show_welcome_on_startup = Some(!state.tutorial_completed && !state.tutorial_skipped); + settings.general.show_welcome_on_startup = + Some(!state.tutorial_completed && !state.tutorial_skipped); settings.save().map_err(|e| e.to_string())?; } - + Ok(()) } @@ -138,7 +139,9 @@ impl WelcomeManager { /// Get specific tutorial category pub async fn get_tutorial_category(&self, category_id: &str) -> Option { - self.tutorials.read().await + self.tutorials + .read() + .await .iter() .find(|c| c.id == category_id) .cloned() @@ -147,31 +150,29 @@ impl WelcomeManager { /// Complete a tutorial step pub async fn complete_step(&self, step_id: &str) -> Result<(), String> { let mut state = self.state.write().await; - + if !state.completed_steps.contains(&step_id.to_string()) { state.completed_steps.push(step_id.to_string()); - + // Check if all steps are completed let tutorials = self.tutorials.read().await; - let total_steps: usize = tutorials.iter() - .map(|c| c.steps.len()) - .sum(); - + let total_steps: usize = tutorials.iter().map(|c| c.steps.len()).sum(); + if state.completed_steps.len() >= total_steps { state.tutorial_completed = true; } - + // Save state drop(state); drop(tutorials); self.save_state().await?; - + // Emit progress event if let Some(app_handle) = self.app_handle.read().await.as_ref() { let _ = app_handle.emit("tutorial:step_completed", step_id); } } - + Ok(()) } @@ -181,9 +182,9 @@ impl WelcomeManager { state.tutorial_skipped = true; state.first_launch = false; drop(state); - + self.save_state().await?; - + Ok(()) } @@ -194,9 +195,9 @@ impl WelcomeManager { state.tutorial_completed = false; state.tutorial_skipped = false; drop(state); - + self.save_state().await?; - + Ok(()) } @@ -212,7 +213,7 @@ impl WelcomeManager { tauri::WebviewWindowBuilder::new( app_handle, "welcome", - tauri::WebviewUrl::App("welcome.html".into()) + tauri::WebviewUrl::App("welcome.html".into()), ) .title("Welcome to VibeTunnel") .inner_size(800.0, 600.0) @@ -224,7 +225,7 @@ impl WelcomeManager { } else { return Err("App handle not set".to_string()); } - + Ok(()) } @@ -408,34 +409,37 @@ VibeTunnel will be ready whenever you need it."#.to_string(), pub async fn get_progress(&self) -> TutorialProgress { let state = self.state.read().await; let tutorials = self.tutorials.read().await; - - let total_steps: usize = tutorials.iter() - .map(|c| c.steps.len()) - .sum(); - + + let total_steps: usize = tutorials.iter().map(|c| c.steps.len()).sum(); + let completed_steps = state.completed_steps.len(); let percentage = if total_steps > 0 { (completed_steps as f32 / total_steps as f32 * 100.0) as u32 } else { 0 }; - + TutorialProgress { total_steps, completed_steps, percentage, - categories: tutorials.iter().map(|category| { - let category_completed = category.steps.iter() - .filter(|s| state.completed_steps.contains(&s.id)) - .count(); - - CategoryProgress { - category_id: category.id.clone(), - category_name: category.name.clone(), - total_steps: category.steps.len(), - completed_steps: category_completed, - } - }).collect(), + categories: tutorials + .iter() + .map(|category| { + let category_completed = category + .steps + .iter() + .filter(|s| state.completed_steps.contains(&s.id)) + .count(); + + CategoryProgress { + category_id: category.id.clone(), + category_name: category.name.clone(), + total_steps: category.steps.len(), + completed_steps: category_completed, + } + }) + .collect(), } } } @@ -456,4 +460,4 @@ pub struct CategoryProgress { pub category_name: String, pub total_steps: usize, pub completed_steps: usize, -} \ No newline at end of file +}