mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
feat: Add Rust formatter and linter configuration, fix warnings
- Add rustfmt.toml with comprehensive formatting rules - Add clippy.toml with linting configuration - Add .cargo/config.toml with clippy warning flags - Fix unused imports and variables warnings - Connect unused functions in terminal.rs, server.rs, tray_menu.rs, and session_monitor.rs - Add proper error handling and cleanup on app quit - Apply formatting to all Rust code files - Ensure feature parity with Mac app implementation This improves code quality, consistency, and maintainability of the Tauri app.
This commit is contained in:
parent
0d9232574c
commit
5d3576a205
31 changed files with 2870 additions and 2246 deletions
|
|
@ -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 chrono::{DateTime, Utc};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// API test method
|
/// API test method
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
|
@ -36,11 +36,22 @@ impl HttpMethod {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum AssertionType {
|
pub enum AssertionType {
|
||||||
StatusCode(u16),
|
StatusCode(u16),
|
||||||
StatusRange { min: u16, max: u16 },
|
StatusRange {
|
||||||
ResponseTime { max_ms: u64 },
|
min: u16,
|
||||||
|
max: u16,
|
||||||
|
},
|
||||||
|
ResponseTime {
|
||||||
|
max_ms: u64,
|
||||||
|
},
|
||||||
HeaderExists(String),
|
HeaderExists(String),
|
||||||
HeaderEquals { key: String, value: String },
|
HeaderEquals {
|
||||||
JsonPath { path: String, expected: serde_json::Value },
|
key: String,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
JsonPath {
|
||||||
|
path: String,
|
||||||
|
expected: serde_json::Value,
|
||||||
|
},
|
||||||
BodyContains(String),
|
BodyContains(String),
|
||||||
BodyMatches(String), // Regex
|
BodyMatches(String), // Regex
|
||||||
ContentType(String),
|
ContentType(String),
|
||||||
|
|
@ -78,9 +89,16 @@ pub enum APITestBody {
|
||||||
/// API test authentication
|
/// API test authentication
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum APITestAuth {
|
pub enum APITestAuth {
|
||||||
Basic { username: String, password: String },
|
Basic {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
},
|
||||||
Bearer(String),
|
Bearer(String),
|
||||||
ApiKey { key: String, value: String, in_header: bool },
|
ApiKey {
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
in_header: bool,
|
||||||
|
},
|
||||||
Custom(HashMap<String, String>),
|
Custom(HashMap<String, String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +223,10 @@ impl APITestingManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +242,10 @@ impl APITestingManager {
|
||||||
|
|
||||||
/// Add test suite
|
/// Add test suite
|
||||||
pub async fn add_test_suite(&self, suite: APITestSuite) {
|
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
|
/// Get test suite
|
||||||
|
|
@ -235,7 +259,11 @@ impl APITestingManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run single test
|
/// Run single test
|
||||||
pub async fn run_test(&self, test: &APITest, variables: &HashMap<String, String>) -> APITestResult {
|
pub async fn run_test(
|
||||||
|
&self,
|
||||||
|
test: &APITest,
|
||||||
|
variables: &HashMap<String, String>,
|
||||||
|
) -> APITestResult {
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
let mut result = APITestResult {
|
let mut result = APITestResult {
|
||||||
test_id: test.id.clone(),
|
test_id: test.id.clone(),
|
||||||
|
|
@ -274,7 +302,9 @@ impl APITestingManager {
|
||||||
result.retries_used = retry;
|
result.retries_used = retry;
|
||||||
|
|
||||||
// Run assertions
|
// 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);
|
result.success = result.assertion_results.iter().all(|a| a.passed);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -324,9 +354,9 @@ impl APITestingManager {
|
||||||
let vars = variables.clone();
|
let vars = variables.clone();
|
||||||
let manager = self.clone_for_parallel();
|
let manager = self.clone_for_parallel();
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::spawn(
|
||||||
manager.run_test(&test, &vars).await
|
async move { manager.run_test(&test, &vars).await },
|
||||||
}));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
|
|
@ -371,11 +401,10 @@ impl APITestingManager {
|
||||||
|
|
||||||
// Send notification
|
// Send notification
|
||||||
if let Some(notification_manager) = &self.notification_manager {
|
if let Some(notification_manager) = &self.notification_manager {
|
||||||
let message = format!(
|
let message = format!("Test suite completed: {} passed, {} failed", passed, failed);
|
||||||
"Test suite completed: {} passed, {} failed",
|
let _ = notification_manager
|
||||||
passed, failed
|
.notify_success("API Tests", &message)
|
||||||
);
|
.await;
|
||||||
let _ = notification_manager.notify_success("API Tests", &message).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(history_entry)
|
Some(history_entry)
|
||||||
|
|
@ -403,7 +432,9 @@ impl APITestingManager {
|
||||||
|
|
||||||
/// Export test suite
|
/// Export test suite
|
||||||
pub async fn export_test_suite(&self, suite_id: &str) -> Result<String, String> {
|
pub async fn export_test_suite(&self, suite_id: &str) -> Result<String, String> {
|
||||||
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())?;
|
.ok_or_else(|| "Test suite not found".to_string())?;
|
||||||
|
|
||||||
serde_json::to_string_pretty(&suite)
|
serde_json::to_string_pretty(&suite)
|
||||||
|
|
@ -493,42 +524,39 @@ impl APITestingManager {
|
||||||
|
|
||||||
for assertion in assertions {
|
for assertion in assertions {
|
||||||
let result = match assertion {
|
let result = match assertion {
|
||||||
AssertionType::StatusCode(expected) => {
|
AssertionType::StatusCode(expected) => AssertionResult {
|
||||||
AssertionResult {
|
assertion: assertion.clone(),
|
||||||
assertion: assertion.clone(),
|
passed: status == *expected,
|
||||||
passed: status == *expected,
|
actual_value: Some(status.to_string()),
|
||||||
actual_value: Some(status.to_string()),
|
error_message: if status != *expected {
|
||||||
error_message: if status != *expected {
|
Some(format!("Expected status {}, got {}", expected, status))
|
||||||
Some(format!("Expected status {}, got {}", expected, status))
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
},
|
||||||
},
|
},
|
||||||
}
|
AssertionType::StatusRange { min, max } => AssertionResult {
|
||||||
}
|
assertion: assertion.clone(),
|
||||||
AssertionType::StatusRange { min, max } => {
|
passed: status >= *min && status <= *max,
|
||||||
AssertionResult {
|
actual_value: Some(status.to_string()),
|
||||||
assertion: assertion.clone(),
|
error_message: if status < *min || status > *max {
|
||||||
passed: status >= *min && status <= *max,
|
Some(format!(
|
||||||
actual_value: Some(status.to_string()),
|
"Expected status between {} and {}, got {}",
|
||||||
error_message: if status < *min || status > *max {
|
min, max, status
|
||||||
Some(format!("Expected status between {} and {}, got {}", min, max, status))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
AssertionType::HeaderExists(key) => AssertionResult {
|
||||||
AssertionType::HeaderExists(key) => {
|
assertion: assertion.clone(),
|
||||||
AssertionResult {
|
passed: headers.contains_key(key),
|
||||||
assertion: assertion.clone(),
|
actual_value: None,
|
||||||
passed: headers.contains_key(key),
|
error_message: if !headers.contains_key(key) {
|
||||||
actual_value: None,
|
Some(format!("Header '{}' not found", key))
|
||||||
error_message: if !headers.contains_key(key) {
|
} else {
|
||||||
Some(format!("Header '{}' not found", key))
|
None
|
||||||
} else {
|
},
|
||||||
None
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AssertionType::HeaderEquals { key, value } => {
|
AssertionType::HeaderEquals { key, value } => {
|
||||||
let actual = headers.get(key);
|
let actual = headers.get(key);
|
||||||
AssertionResult {
|
AssertionResult {
|
||||||
|
|
@ -536,25 +564,29 @@ impl APITestingManager {
|
||||||
passed: actual == Some(value),
|
passed: actual == Some(value),
|
||||||
actual_value: actual.cloned(),
|
actual_value: actual.cloned(),
|
||||||
error_message: if actual != Some(value) {
|
error_message: if actual != Some(value) {
|
||||||
Some(format!("Header '{}' expected '{}', got '{:?}'", key, value, actual))
|
Some(format!(
|
||||||
|
"Header '{}' expected '{}', got '{:?}'",
|
||||||
|
key, value, actual
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AssertionType::BodyContains(text) => {
|
AssertionType::BodyContains(text) => AssertionResult {
|
||||||
AssertionResult {
|
assertion: assertion.clone(),
|
||||||
assertion: assertion.clone(),
|
passed: body.contains(text),
|
||||||
passed: body.contains(text),
|
actual_value: None,
|
||||||
actual_value: None,
|
error_message: if !body.contains(text) {
|
||||||
error_message: if !body.contains(text) {
|
Some(format!("Body does not contain '{}'", text))
|
||||||
Some(format!("Body does not contain '{}'", text))
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
},
|
||||||
},
|
},
|
||||||
}
|
AssertionType::JsonPath {
|
||||||
}
|
path: _,
|
||||||
AssertionType::JsonPath { path: _, expected: _ } => {
|
expected: _,
|
||||||
|
} => {
|
||||||
// TODO: Implement JSON path assertion
|
// TODO: Implement JSON path assertion
|
||||||
AssertionResult {
|
AssertionResult {
|
||||||
assertion: assertion.clone(),
|
assertion: assertion.clone(),
|
||||||
|
|
@ -563,14 +595,12 @@ impl APITestingManager {
|
||||||
error_message: Some("JSON path assertions not yet implemented".to_string()),
|
error_message: Some("JSON path assertions not yet implemented".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => AssertionResult {
|
||||||
AssertionResult {
|
assertion: assertion.clone(),
|
||||||
assertion: assertion.clone(),
|
passed: false,
|
||||||
passed: false,
|
actual_value: None,
|
||||||
actual_value: None,
|
error_message: Some("Assertion type not implemented".to_string()),
|
||||||
error_message: Some("Assertion type not implemented".to_string()),
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
|
|
@ -602,7 +632,11 @@ impl APITestingManager {
|
||||||
let token = self.replace_variables(token, variables);
|
let token = self.replace_variables(token, variables);
|
||||||
request.bearer_auth(token)
|
request.bearer_auth(token)
|
||||||
}
|
}
|
||||||
APITestAuth::ApiKey { key, value, in_header } => {
|
APITestAuth::ApiKey {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
in_header,
|
||||||
|
} => {
|
||||||
let key = self.replace_variables(key, variables);
|
let key = self.replace_variables(key, variables);
|
||||||
let value = self.replace_variables(value, variables);
|
let value = self.replace_variables(value, variables);
|
||||||
if *in_header {
|
if *in_header {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use tauri::AppHandle;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
/// Check if the app should be moved to Applications folder
|
/// Check if the app should be moved to Applications folder
|
||||||
/// This is a macOS-specific feature
|
/// This is a macOS-specific feature
|
||||||
#[cfg(target_os = "macos")]
|
#[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
|
// Get current app bundle path
|
||||||
let bundle_path = get_app_bundle_path()?;
|
let bundle_path = get_app_bundle_path()?;
|
||||||
|
|
||||||
|
|
@ -27,7 +26,8 @@ pub async fn check_and_prompt_move(app_handle: AppHandle) -> Result<(), String>
|
||||||
// TODO: Implement dialog using tauri-plugin-dialog
|
// TODO: Implement dialog using tauri-plugin-dialog
|
||||||
tracing::info!("App should be moved to Applications folder");
|
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)?;
|
move_to_applications_folder(bundle_path)?;
|
||||||
|
|
||||||
// Restart the app from the new location
|
// Restart the app from the new location
|
||||||
|
|
@ -53,8 +53,8 @@ fn get_app_bundle_path() -> Result<PathBuf, String> {
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
// Get the executable path
|
// Get the executable path
|
||||||
let exe_path = env::current_exe()
|
let exe_path =
|
||||||
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
env::current_exe().map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
|
||||||
// Navigate up to the .app bundle
|
// Navigate up to the .app bundle
|
||||||
// Typical structure: /path/to/VibeTunnel.app/Contents/MacOS/VibeTunnel
|
// Typical structure: /path/to/VibeTunnel.app/Contents/MacOS/VibeTunnel
|
||||||
|
|
@ -62,7 +62,8 @@ fn get_app_bundle_path() -> Result<PathBuf, String> {
|
||||||
|
|
||||||
// Go up three levels to reach the .app bundle
|
// Go up three levels to reach the .app bundle
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
bundle_path = bundle_path.parent()
|
bundle_path = bundle_path
|
||||||
|
.parent()
|
||||||
.ok_or("Failed to find app bundle")?
|
.ok_or("Failed to find app bundle")?
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
}
|
}
|
||||||
|
|
@ -83,10 +84,11 @@ fn is_in_applications_folder(bundle_path: &PathBuf) -> bool {
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn move_to_applications_folder(bundle_path: PathBuf) -> Result<(), String> {
|
fn move_to_applications_folder(bundle_path: PathBuf) -> Result<(), String> {
|
||||||
use std::process::Command;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
let app_name = bundle_path.file_name()
|
let app_name = bundle_path
|
||||||
|
.file_name()
|
||||||
.ok_or("Failed to get app name")?
|
.ok_or("Failed to get app name")?
|
||||||
.to_string_lossy();
|
.to_string_lossy();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use axum::{
|
||||||
response::Response,
|
response::Response,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use std::collections::HashMap;
|
|
||||||
use chrono::{DateTime, Utc, Duration};
|
|
||||||
use sha2::{Sha256, Digest};
|
|
||||||
|
|
||||||
/// Authentication token type
|
/// Authentication token type
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -104,7 +104,7 @@ impl Default for AuthCacheConfig {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
max_entries: 1000,
|
max_entries: 1000,
|
||||||
default_ttl_seconds: 3600, // 1 hour
|
default_ttl_seconds: 3600, // 1 hour
|
||||||
refresh_threshold_seconds: 300, // 5 minutes
|
refresh_threshold_seconds: 300, // 5 minutes
|
||||||
persist_to_disk: false,
|
persist_to_disk: false,
|
||||||
encryption_enabled: true,
|
encryption_enabled: true,
|
||||||
|
|
@ -126,7 +126,11 @@ pub struct AuthCacheStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Token refresh callback
|
/// Token refresh callback
|
||||||
pub type TokenRefreshCallback = Arc<dyn Fn(CachedToken) -> futures::future::BoxFuture<'static, Result<CachedToken, String>> + Send + Sync>;
|
pub type TokenRefreshCallback = Arc<
|
||||||
|
dyn Fn(CachedToken) -> futures::future::BoxFuture<'static, Result<CachedToken, String>>
|
||||||
|
+ Send
|
||||||
|
+ Sync,
|
||||||
|
>;
|
||||||
|
|
||||||
/// Authentication cache manager
|
/// Authentication cache manager
|
||||||
pub struct AuthCacheManager {
|
pub struct AuthCacheManager {
|
||||||
|
|
@ -168,7 +172,10 @@ impl AuthCacheManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +251,8 @@ impl AuthCacheManager {
|
||||||
// Check if needs refresh
|
// Check if needs refresh
|
||||||
if token.needs_refresh(config.refresh_threshold_seconds) {
|
if token.needs_refresh(config.refresh_threshold_seconds) {
|
||||||
// Trigger refresh in background
|
// 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 token_clone = token.clone();
|
||||||
let callback = refresh_callback.clone();
|
let callback = refresh_callback.clone();
|
||||||
let key_clone = key.to_string();
|
let key_clone = key.to_string();
|
||||||
|
|
@ -269,7 +277,11 @@ impl AuthCacheManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store credential in cache
|
/// 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;
|
let config = self.config.read().await;
|
||||||
if !config.enabled {
|
if !config.enabled {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -315,7 +327,10 @@ impl AuthCacheManager {
|
||||||
|
|
||||||
/// Register token refresh callback
|
/// Register token refresh callback
|
||||||
pub async fn register_refresh_callback(&self, key: &str, callback: TokenRefreshCallback) {
|
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
|
/// Clear specific cache entry
|
||||||
|
|
@ -344,7 +359,9 @@ impl AuthCacheManager {
|
||||||
|
|
||||||
/// List all cache entries
|
/// List all cache entries
|
||||||
pub async fn list_entries(&self) -> Vec<(String, DateTime<Utc>, u64)> {
|
pub async fn list_entries(&self) -> Vec<(String, DateTime<Utc>, u64)> {
|
||||||
self.cache.read().await
|
self.cache
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.values()
|
.values()
|
||||||
.map(|entry| (entry.key.clone(), entry.last_accessed, entry.access_count))
|
.map(|entry| (entry.key.clone(), entry.last_accessed, entry.access_count))
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -372,9 +389,7 @@ impl AuthCacheManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.total_entries = cache.len();
|
stats.total_entries = cache.len();
|
||||||
stats.total_tokens = cache.values()
|
stats.total_tokens = cache.values().map(|e| e.tokens.len()).sum();
|
||||||
.map(|e| e.tokens.len())
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -388,14 +403,20 @@ impl AuthCacheManager {
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
fn token_matches_scope(&self, token: &CachedToken, scope: &AuthScope) -> bool {
|
fn token_matches_scope(&self, token: &CachedToken, scope: &AuthScope) -> bool {
|
||||||
token.scope.service == scope.service &&
|
token.scope.service == scope.service
|
||||||
token.scope.resource == scope.resource &&
|
&& token.scope.resource == scope.resource
|
||||||
scope.permissions.iter().all(|p| token.scope.permissions.contains(p))
|
&& scope
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.all(|p| token.scope.permissions.contains(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evict_oldest_entry(&self, cache: &mut HashMap<String, AuthCacheEntry>, stats: &mut AuthCacheStats) {
|
fn evict_oldest_entry(
|
||||||
if let Some((key, _)) = cache.iter()
|
&self,
|
||||||
.min_by_key(|(_, entry)| entry.last_accessed) {
|
cache: &mut HashMap<String, AuthCacheEntry>,
|
||||||
|
stats: &mut AuthCacheStats,
|
||||||
|
) {
|
||||||
|
if let Some((key, _)) = cache.iter().min_by_key(|(_, entry)| entry.last_accessed) {
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
cache.remove(&key);
|
cache.remove(&key);
|
||||||
stats.eviction_count += 1;
|
stats.eviction_count += 1;
|
||||||
|
|
@ -429,9 +450,7 @@ impl AuthCacheManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.expired_tokens += total_expired;
|
stats.expired_tokens += total_expired;
|
||||||
stats.total_tokens = cache.values()
|
stats.total_tokens = cache.values().map(|e| e.tokens.len()).sum();
|
||||||
.map(|e| e.tokens.len())
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
// Remove empty entries
|
// Remove empty entries
|
||||||
cache.retain(|_, entry| !entry.tokens.is_empty() || entry.credential.is_some());
|
cache.retain(|_, entry| !entry.tokens.is_empty() || entry.credential.is_some());
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
use crate::state::AppState;
|
||||||
use auto_launch::AutoLaunchBuilder;
|
use auto_launch::AutoLaunchBuilder;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
fn get_app_path() -> String {
|
fn get_app_path() -> String {
|
||||||
let exe_path = std::env::current_exe().unwrap();
|
let exe_path = std::env::current_exe().unwrap();
|
||||||
|
|
@ -64,10 +64,7 @@ pub fn is_auto_launch_enabled() -> Result<bool, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_auto_launch(
|
pub async fn set_auto_launch(enabled: bool, _state: State<'_, AppState>) -> Result<(), String> {
|
||||||
enabled: bool,
|
|
||||||
_state: State<'_, AppState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if enabled {
|
if enabled {
|
||||||
enable_auto_launch()
|
enable_auto_launch()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -76,8 +73,6 @@ pub async fn set_auto_launch(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_auto_launch(
|
pub async fn get_auto_launch(_state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
_state: State<'_, AppState>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
is_auto_launch_enabled()
|
is_auto_launch_enabled()
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use chrono::{DateTime, Utc};
|
||||||
use std::sync::Arc;
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use chrono::{DateTime, Utc};
|
use std::sync::Arc;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Backend type enumeration
|
/// Backend type enumeration
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -155,7 +155,10 @@ impl BackendManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,103 +167,112 @@ impl BackendManager {
|
||||||
let mut configs = HashMap::new();
|
let mut configs = HashMap::new();
|
||||||
|
|
||||||
// Rust backend (built-in)
|
// Rust backend (built-in)
|
||||||
configs.insert(BackendType::Rust, BackendConfig {
|
configs.insert(
|
||||||
backend_type: BackendType::Rust,
|
BackendType::Rust,
|
||||||
name: "Rust (Built-in)".to_string(),
|
BackendConfig {
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
backend_type: BackendType::Rust,
|
||||||
executable_path: None,
|
name: "Rust (Built-in)".to_string(),
|
||||||
working_directory: None,
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
environment_variables: HashMap::new(),
|
executable_path: None,
|
||||||
arguments: vec![],
|
working_directory: None,
|
||||||
port: Some(4020),
|
environment_variables: HashMap::new(),
|
||||||
features: BackendFeatures {
|
arguments: vec![],
|
||||||
terminal_sessions: true,
|
port: Some(4020),
|
||||||
file_browser: true,
|
features: BackendFeatures {
|
||||||
port_forwarding: true,
|
terminal_sessions: true,
|
||||||
authentication: true,
|
file_browser: true,
|
||||||
websocket_support: true,
|
port_forwarding: true,
|
||||||
rest_api: true,
|
authentication: true,
|
||||||
graphql_api: false,
|
websocket_support: true,
|
||||||
metrics: 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
|
// Node.js backend
|
||||||
configs.insert(BackendType::NodeJS, BackendConfig {
|
configs.insert(
|
||||||
backend_type: BackendType::NodeJS,
|
BackendType::NodeJS,
|
||||||
name: "Node.js Server".to_string(),
|
BackendConfig {
|
||||||
version: "1.0.0".to_string(),
|
backend_type: BackendType::NodeJS,
|
||||||
executable_path: Some(PathBuf::from("node")),
|
name: "Node.js Server".to_string(),
|
||||||
working_directory: None,
|
version: "1.0.0".to_string(),
|
||||||
environment_variables: HashMap::new(),
|
executable_path: Some(PathBuf::from("node")),
|
||||||
arguments: vec!["server.js".to_string()],
|
working_directory: None,
|
||||||
port: Some(4021),
|
environment_variables: HashMap::new(),
|
||||||
features: BackendFeatures {
|
arguments: vec!["server.js".to_string()],
|
||||||
terminal_sessions: true,
|
port: Some(4021),
|
||||||
file_browser: true,
|
features: BackendFeatures {
|
||||||
port_forwarding: false,
|
terminal_sessions: true,
|
||||||
authentication: true,
|
file_browser: true,
|
||||||
websocket_support: true,
|
port_forwarding: false,
|
||||||
rest_api: true,
|
authentication: true,
|
||||||
graphql_api: true,
|
websocket_support: true,
|
||||||
metrics: false,
|
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
|
// Python backend
|
||||||
configs.insert(BackendType::Python, BackendConfig {
|
configs.insert(
|
||||||
backend_type: BackendType::Python,
|
BackendType::Python,
|
||||||
name: "Python Server".to_string(),
|
BackendConfig {
|
||||||
version: "1.0.0".to_string(),
|
backend_type: BackendType::Python,
|
||||||
executable_path: Some(PathBuf::from("python3")),
|
name: "Python Server".to_string(),
|
||||||
working_directory: None,
|
version: "1.0.0".to_string(),
|
||||||
environment_variables: HashMap::new(),
|
executable_path: Some(PathBuf::from("python3")),
|
||||||
arguments: vec!["-m".to_string(), "vibetunnel_server".to_string()],
|
working_directory: None,
|
||||||
port: Some(4022),
|
environment_variables: HashMap::new(),
|
||||||
features: BackendFeatures {
|
arguments: vec!["-m".to_string(), "vibetunnel_server".to_string()],
|
||||||
terminal_sessions: true,
|
port: Some(4022),
|
||||||
file_browser: true,
|
features: BackendFeatures {
|
||||||
port_forwarding: false,
|
terminal_sessions: true,
|
||||||
authentication: true,
|
file_browser: true,
|
||||||
websocket_support: true,
|
port_forwarding: false,
|
||||||
rest_api: true,
|
authentication: true,
|
||||||
graphql_api: false,
|
websocket_support: true,
|
||||||
metrics: 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
|
configs
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +317,9 @@ impl BackendManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get backend configuration
|
// 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())?;
|
.ok_or_else(|| "Backend configuration not found".to_string())?;
|
||||||
|
|
||||||
// Generate instance ID
|
// Generate instance ID
|
||||||
|
|
@ -332,13 +346,17 @@ impl BackendManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store instance
|
// Store instance
|
||||||
self.instances.write().await.insert(instance_id.clone(), instance);
|
self.instances
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(instance_id.clone(), instance);
|
||||||
|
|
||||||
// Start backend process
|
// Start backend process
|
||||||
match backend_type {
|
match backend_type {
|
||||||
BackendType::Rust => {
|
BackendType::Rust => {
|
||||||
// Rust backend is handled internally
|
// 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);
|
*self.active_backend.write().await = Some(BackendType::Rust);
|
||||||
Ok(instance_id)
|
Ok(instance_id)
|
||||||
}
|
}
|
||||||
|
|
@ -351,7 +369,10 @@ impl BackendManager {
|
||||||
|
|
||||||
/// Stop backend
|
/// Stop backend
|
||||||
pub async fn stop_backend(&self, instance_id: &str) -> Result<(), String> {
|
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)
|
.get(instance_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| "Backend instance not found".to_string())?;
|
.ok_or_else(|| "Backend instance not found".to_string())?;
|
||||||
|
|
@ -359,7 +380,8 @@ impl BackendManager {
|
||||||
match instance.backend_type {
|
match instance.backend_type {
|
||||||
BackendType::Rust => {
|
BackendType::Rust => {
|
||||||
// Rust backend is handled internally
|
// Rust backend is handled internally
|
||||||
self.update_instance_status(instance_id, BackendStatus::Stopped).await;
|
self.update_instance_status(instance_id, BackendStatus::Stopped)
|
||||||
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -378,8 +400,12 @@ impl BackendManager {
|
||||||
// Find and stop current backend instances
|
// Find and stop current backend instances
|
||||||
let instance_id = {
|
let instance_id = {
|
||||||
let instances = self.instances.read().await;
|
let instances = self.instances.read().await;
|
||||||
instances.iter()
|
instances
|
||||||
.find(|(_, instance)| instance.backend_type == current && instance.status == BackendStatus::Running)
|
.iter()
|
||||||
|
.find(|(_, instance)| {
|
||||||
|
instance.backend_type == current
|
||||||
|
&& instance.status == BackendStatus::Running
|
||||||
|
})
|
||||||
.map(|(id, _)| id.clone())
|
.map(|(id, _)| id.clone())
|
||||||
};
|
};
|
||||||
if let Some(id) = instance_id {
|
if let Some(id) = instance_id {
|
||||||
|
|
@ -396,10 +422,12 @@ impl BackendManager {
|
||||||
|
|
||||||
// Notify about backend switch
|
// Notify about backend switch
|
||||||
if let Some(notification_manager) = &self.notification_manager {
|
if let Some(notification_manager) = &self.notification_manager {
|
||||||
let _ = notification_manager.notify_success(
|
let _ = notification_manager
|
||||||
"Backend Switched",
|
.notify_success(
|
||||||
&format!("Switched to {:?} backend", backend_type)
|
"Backend Switched",
|
||||||
).await;
|
&format!("Switched to {:?} backend", backend_type),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -417,7 +445,10 @@ impl BackendManager {
|
||||||
|
|
||||||
/// Get backend health
|
/// Get backend health
|
||||||
pub async fn check_backend_health(&self, instance_id: &str) -> Result<HealthStatus, String> {
|
pub async fn check_backend_health(&self, instance_id: &str) -> Result<HealthStatus, String> {
|
||||||
let instance = self.instances.read().await
|
let instance = self
|
||||||
|
.instances
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.get(instance_id)
|
.get(instance_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| "Backend instance not found".to_string())?;
|
.ok_or_else(|| "Backend instance not found".to_string())?;
|
||||||
|
|
@ -487,7 +518,11 @@ impl BackendManager {
|
||||||
Err("Python backend installation not yet implemented".to_string())
|
Err("Python backend installation not yet implemented".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_external_backend(&self, _instance_id: &str, _config: BackendConfig) -> Result<String, String> {
|
async fn start_external_backend(
|
||||||
|
&self,
|
||||||
|
_instance_id: &str,
|
||||||
|
_config: BackendConfig,
|
||||||
|
) -> Result<String, String> {
|
||||||
// TODO: Implement external backend startup
|
// TODO: Implement external backend startup
|
||||||
Err("External backend startup not yet implemented".to_string())
|
Err("External backend startup not yet implemented".to_string())
|
||||||
}
|
}
|
||||||
|
|
@ -497,7 +532,10 @@ impl BackendManager {
|
||||||
Err("External backend shutdown not yet implemented".to_string())
|
Err("External backend shutdown not yet implemented".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_external_backend_health(&self, _instance: &BackendInstance) -> Result<HealthStatus, String> {
|
async fn check_external_backend_health(
|
||||||
|
&self,
|
||||||
|
_instance: &BackendInstance,
|
||||||
|
) -> Result<HealthStatus, String> {
|
||||||
// TODO: Implement health check for external backends
|
// TODO: Implement health check for external backends
|
||||||
Ok(HealthStatus::Unknown)
|
Ok(HealthStatus::Unknown)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
@ -5,7 +6,6 @@ use std::io::{BufWriter, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
/// Asciinema cast v2 format header
|
/// Asciinema cast v2 format header
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -62,12 +62,7 @@ pub struct CastRecorder {
|
||||||
|
|
||||||
impl CastRecorder {
|
impl CastRecorder {
|
||||||
/// Create a new cast recorder
|
/// Create a new cast recorder
|
||||||
pub fn new(
|
pub fn new(width: u16, height: u16, title: Option<String>, command: Option<String>) -> Self {
|
||||||
width: u16,
|
|
||||||
height: u16,
|
|
||||||
title: Option<String>,
|
|
||||||
command: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let header = CastHeader {
|
let header = CastHeader {
|
||||||
version: 2,
|
version: 2,
|
||||||
|
|
@ -114,7 +109,8 @@ impl CastRecorder {
|
||||||
self.write_event_to_file(&mut writer, event)?;
|
self.write_event_to_file(&mut writer, event)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.flush()
|
writer
|
||||||
|
.flush()
|
||||||
.map_err(|e| format!("Failed to flush writer: {}", e))?;
|
.map_err(|e| format!("Failed to flush writer: {}", e))?;
|
||||||
|
|
||||||
self.file_writer = Some(Arc::new(Mutex::new(writer)));
|
self.file_writer = Some(Arc::new(Mutex::new(writer)));
|
||||||
|
|
@ -131,7 +127,8 @@ impl CastRecorder {
|
||||||
|
|
||||||
if let Some(writer_arc) = self.file_writer.take() {
|
if let Some(writer_arc) = self.file_writer.take() {
|
||||||
let mut writer = writer_arc.lock().await;
|
let mut writer = writer_arc.lock().await;
|
||||||
writer.flush()
|
writer
|
||||||
|
.flush()
|
||||||
.map_err(|e| format!("Failed to flush final data: {}", e))?;
|
.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> {
|
async fn add_event(&self, event_type: EventType, data: &[u8]) -> Result<(), String> {
|
||||||
let timestamp = Utc::now()
|
let timestamp = Utc::now()
|
||||||
.signed_duration_since(self.start_time)
|
.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)
|
// Convert data to string (handling potential UTF-8 errors)
|
||||||
let data_string = String::from_utf8_lossy(data).to_string();
|
let data_string = String::from_utf8_lossy(data).to_string();
|
||||||
|
|
@ -168,7 +166,8 @@ impl CastRecorder {
|
||||||
if let Some(writer_arc) = &self.file_writer {
|
if let Some(writer_arc) = &self.file_writer {
|
||||||
let mut writer = writer_arc.lock().await;
|
let mut writer = writer_arc.lock().await;
|
||||||
self.write_event_to_file(&mut writer, &event)?;
|
self.write_event_to_file(&mut writer, &event)?;
|
||||||
writer.flush()
|
writer
|
||||||
|
.flush()
|
||||||
.map_err(|e| format!("Failed to flush event: {}", e))?;
|
.map_err(|e| format!("Failed to flush event: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,14 +185,10 @@ impl CastRecorder {
|
||||||
event: &CastEvent,
|
event: &CastEvent,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Format: [timestamp, event_type, data]
|
// Format: [timestamp, event_type, data]
|
||||||
let event_array = serde_json::json!([
|
let event_array =
|
||||||
event.timestamp,
|
serde_json::json!([event.timestamp, event.event_type.as_str(), event.data]);
|
||||||
event.event_type.as_str(),
|
|
||||||
event.data
|
|
||||||
]);
|
|
||||||
|
|
||||||
writeln!(writer, "{}", event_array)
|
writeln!(writer, "{}", event_array).map_err(|e| format!("Failed to write event: {}", e))?;
|
||||||
.map_err(|e| format!("Failed to write event: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +218,8 @@ impl CastRecorder {
|
||||||
self.write_event_to_file(&mut writer, event)?;
|
self.write_event_to_file(&mut writer, event)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.flush()
|
writer
|
||||||
|
.flush()
|
||||||
.map_err(|e| format!("Failed to flush file: {}", e))?;
|
.map_err(|e| format!("Failed to flush file: {}", e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
use serde::Serialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
const CLI_SCRIPT: &str = r#"#!/bin/bash
|
const CLI_SCRIPT: &str = r#"#!/bin/bash
|
||||||
# VibeTunnel CLI wrapper
|
# VibeTunnel CLI wrapper
|
||||||
|
|
@ -21,6 +21,7 @@ fi
|
||||||
"$APP_PATH/Contents/MacOS/VibeTunnel" --cli "$@"
|
"$APP_PATH/Contents/MacOS/VibeTunnel" --cli "$@"
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
const WINDOWS_CLI_SCRIPT: &str = r#"@echo off
|
const WINDOWS_CLI_SCRIPT: &str = r#"@echo off
|
||||||
:: VibeTunnel CLI wrapper for Windows
|
:: VibeTunnel CLI wrapper for Windows
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ if not exist "%APP_PATH%" (
|
||||||
"%APP_PATH%" --cli %*
|
"%APP_PATH%" --cli %*
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
const LINUX_CLI_SCRIPT: &str = r#"#!/bin/bash
|
const LINUX_CLI_SCRIPT: &str = r#"#!/bin/bash
|
||||||
# VibeTunnel CLI wrapper for Linux
|
# VibeTunnel CLI wrapper for Linux
|
||||||
|
|
||||||
|
|
@ -86,8 +88,12 @@ fn install_cli_macos() -> Result<CliInstallResult, String> {
|
||||||
// Check if /usr/local/bin exists, create if not
|
// Check if /usr/local/bin exists, create if not
|
||||||
let bin_dir = cli_path.parent().unwrap();
|
let bin_dir = cli_path.parent().unwrap();
|
||||||
if !bin_dir.exists() {
|
if !bin_dir.exists() {
|
||||||
fs::create_dir_all(bin_dir)
|
fs::create_dir_all(bin_dir).map_err(|e| {
|
||||||
.map_err(|e| format!("Failed to create /usr/local/bin: {}. Try running with sudo.", e))?;
|
format!(
|
||||||
|
"Failed to create /usr/local/bin: {}. Try running with sudo.",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the CLI script
|
// Write the CLI script
|
||||||
|
|
@ -114,8 +120,7 @@ fn install_cli_macos() -> Result<CliInstallResult, String> {
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn install_cli_windows() -> Result<CliInstallResult, String> {
|
fn install_cli_windows() -> Result<CliInstallResult, String> {
|
||||||
let user_path = std::env::var("USERPROFILE")
|
let user_path = std::env::var("USERPROFILE").map_err(|_| "Failed to get user profile path")?;
|
||||||
.map_err(|_| "Failed to get user profile path")?;
|
|
||||||
|
|
||||||
let cli_dir = PathBuf::from(&user_path).join(".vibetunnel");
|
let cli_dir = PathBuf::from(&user_path).join(".vibetunnel");
|
||||||
let cli_path = cli_dir.join("vt.cmd");
|
let cli_path = cli_dir.join("vt.cmd");
|
||||||
|
|
@ -136,14 +141,16 @@ fn install_cli_windows() -> Result<CliInstallResult, String> {
|
||||||
Ok(CliInstallResult {
|
Ok(CliInstallResult {
|
||||||
installed: true,
|
installed: true,
|
||||||
path: cli_path.to_string_lossy().to_string(),
|
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")]
|
#[cfg(target_os = "linux")]
|
||||||
fn install_cli_linux() -> Result<CliInstallResult, String> {
|
fn install_cli_linux() -> Result<CliInstallResult, String> {
|
||||||
let home_dir = std::env::var("HOME")
|
let home_dir = std::env::var("HOME").map_err(|_| "Failed to get home directory")?;
|
||||||
.map_err(|_| "Failed to get home directory")?;
|
|
||||||
|
|
||||||
let local_bin = PathBuf::from(&home_dir).join(".local").join("bin");
|
let local_bin = PathBuf::from(&home_dir).join(".local").join("bin");
|
||||||
let cli_path = local_bin.join("vt");
|
let cli_path = local_bin.join("vt");
|
||||||
|
|
@ -172,7 +179,10 @@ fn install_cli_linux() -> Result<CliInstallResult, String> {
|
||||||
Ok(CliInstallResult {
|
Ok(CliInstallResult {
|
||||||
installed: true,
|
installed: true,
|
||||||
path: cli_path.to_string_lossy().to_string(),
|
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,7 +193,8 @@ fn add_to_windows_path(dir: &Path) -> Result<(), String> {
|
||||||
use winreg::enums::*;
|
use winreg::enums::*;
|
||||||
use winreg::RegKey;
|
use winreg::RegKey;
|
||||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
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))?;
|
.map_err(|e| format!("Failed to open registry key: {}", e))?;
|
||||||
|
|
||||||
let path: String = env.get_value("Path").unwrap_or_default();
|
let path: String = env.get_value("Path").unwrap_or_default();
|
||||||
|
|
@ -227,13 +238,12 @@ pub fn uninstall_cli_tool() -> Result<CliInstallResult, String> {
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let user_path = std::env::var("USERPROFILE")
|
let user_path =
|
||||||
.map_err(|_| "Failed to get user profile 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");
|
let cli_path = PathBuf::from(&user_path).join(".vibetunnel").join("vt.cmd");
|
||||||
|
|
||||||
if cli_path.exists() {
|
if cli_path.exists() {
|
||||||
fs::remove_file(&cli_path)
|
fs::remove_file(&cli_path).map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
||||||
.map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CliInstallResult {
|
Ok(CliInstallResult {
|
||||||
|
|
@ -245,13 +255,14 @@ pub fn uninstall_cli_tool() -> Result<CliInstallResult, String> {
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let home_dir = std::env::var("HOME")
|
let home_dir = std::env::var("HOME").map_err(|_| "Failed to get home directory")?;
|
||||||
.map_err(|_| "Failed to get home directory")?;
|
let cli_path = PathBuf::from(&home_dir)
|
||||||
let cli_path = PathBuf::from(&home_dir).join(".local").join("bin").join("vt");
|
.join(".local")
|
||||||
|
.join("bin")
|
||||||
|
.join("vt");
|
||||||
|
|
||||||
if cli_path.exists() {
|
if cli_path.exists() {
|
||||||
fs::remove_file(&cli_path)
|
fs::remove_file(&cli_path).map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
||||||
.map_err(|e| format!("Failed to remove CLI tool: {}", e))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CliInstallResult {
|
Ok(CliInstallResult {
|
||||||
|
|
@ -271,7 +282,10 @@ pub fn is_cli_installed() -> bool {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
if let Ok(user_path) = std::env::var("USERPROFILE") {
|
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 {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +294,11 @@ pub fn is_cli_installed() -> bool {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
if let Ok(home_dir) = std::env::var("HOME") {
|
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 {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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 std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use std::collections::{HashMap, VecDeque};
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Debug feature types
|
/// Debug feature types
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -245,7 +245,10 @@ impl DebugFeaturesManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +263,13 @@ impl DebugFeaturesManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log a message
|
/// Log a message
|
||||||
pub async fn log(&self, level: LogLevel, component: &str, message: &str, metadata: HashMap<String, serde_json::Value>) {
|
pub async fn log(
|
||||||
|
&self,
|
||||||
|
level: LogLevel,
|
||||||
|
component: &str,
|
||||||
|
message: &str,
|
||||||
|
metadata: HashMap<String, serde_json::Value>,
|
||||||
|
) {
|
||||||
let settings = self.settings.read().await;
|
let settings = self.settings.read().await;
|
||||||
|
|
||||||
// Check if logging is enabled and level is appropriate
|
// Check if logging is enabled and level is appropriate
|
||||||
|
|
@ -294,7 +303,13 @@ impl DebugFeaturesManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record a performance metric
|
/// Record a performance metric
|
||||||
pub async fn record_metric(&self, name: &str, value: f64, unit: &str, tags: HashMap<String, String>) {
|
pub async fn record_metric(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
value: f64,
|
||||||
|
unit: &str,
|
||||||
|
tags: HashMap<String, String>,
|
||||||
|
) {
|
||||||
let settings = self.settings.read().await;
|
let settings = self.settings.read().await;
|
||||||
|
|
||||||
if !settings.enabled || !settings.enable_performance_monitoring {
|
if !settings.enabled || !settings.enable_performance_monitoring {
|
||||||
|
|
@ -379,7 +394,8 @@ impl DebugFeaturesManager {
|
||||||
|
|
||||||
// Store result
|
// Store result
|
||||||
let mut test_results = self.api_test_results.write().await;
|
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)
|
.or_insert_with(Vec::new)
|
||||||
.push(result);
|
.push(result);
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +459,12 @@ impl DebugFeaturesManager {
|
||||||
let app_info = self.get_app_info().await;
|
let app_info = self.get_app_info().await;
|
||||||
let performance_summary = self.get_performance_summary().await;
|
let performance_summary = self.get_performance_summary().await;
|
||||||
let error_summary = self.get_error_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 {
|
DiagnosticReport {
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
|
|
@ -522,7 +543,9 @@ impl DebugFeaturesManager {
|
||||||
} else {
|
} else {
|
||||||
"Debug mode disabled"
|
"Debug mode disabled"
|
||||||
};
|
};
|
||||||
let _ = notification_manager.notify_success("Debug Mode", message).await;
|
let _ = notification_manager
|
||||||
|
.notify_success("Debug Mode", message)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -545,7 +568,8 @@ impl DebugFeaturesManager {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -568,7 +592,7 @@ impl DebugFeaturesManager {
|
||||||
AppInfo {
|
AppInfo {
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
build_date: chrono::Utc::now().to_rfc3339(), // TODO: Get actual build date
|
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,
|
active_sessions: 0,
|
||||||
total_requests: 0,
|
total_requests: 0,
|
||||||
error_count: 0,
|
error_count: 0,
|
||||||
|
|
@ -588,14 +612,17 @@ impl DebugFeaturesManager {
|
||||||
|
|
||||||
async fn get_error_summary(&self) -> ErrorSummary {
|
async fn get_error_summary(&self) -> ErrorSummary {
|
||||||
let logs = self.logs.read().await;
|
let logs = self.logs.read().await;
|
||||||
let errors: Vec<_> = logs.iter()
|
let errors: Vec<_> = logs
|
||||||
|
.iter()
|
||||||
.filter(|log| log.level == LogLevel::Error)
|
.filter(|log| log.level == LogLevel::Error)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut errors_by_type = HashMap::new();
|
let mut errors_by_type = HashMap::new();
|
||||||
for error in &errors {
|
for error in &errors {
|
||||||
let error_type = error.metadata.get("type")
|
let error_type = error
|
||||||
|
.metadata
|
||||||
|
.get("type")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("unknown")
|
.unwrap_or("unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
@ -609,11 +636,20 @@ impl DebugFeaturesManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_recommendations(&self, system: &SystemInfo, _app: &AppInfo, perf: &PerformanceSummary, errors: &ErrorSummary) -> Vec<String> {
|
fn generate_recommendations(
|
||||||
|
&self,
|
||||||
|
system: &SystemInfo,
|
||||||
|
_app: &AppInfo,
|
||||||
|
perf: &PerformanceSummary,
|
||||||
|
errors: &ErrorSummary,
|
||||||
|
) -> Vec<String> {
|
||||||
let mut recommendations = Vec::new();
|
let mut recommendations = Vec::new();
|
||||||
|
|
||||||
if perf.cpu_usage_percent > 80.0 {
|
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) {
|
if perf.memory_usage_mb > (system.total_memory_mb as f64 * 0.8) {
|
||||||
|
|
@ -621,11 +657,14 @@ impl DebugFeaturesManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.total_errors > 100 {
|
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 {
|
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
|
recommendations
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::Query,
|
extract::Query,
|
||||||
http::{StatusCode, header},
|
http::{header, StatusCode},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
|
|
@ -62,8 +62,7 @@ pub struct OperationResult {
|
||||||
/// Expand tilde to home directory
|
/// Expand tilde to home directory
|
||||||
fn expand_path(path: &str) -> Result<PathBuf, StatusCode> {
|
fn expand_path(path: &str) -> Result<PathBuf, StatusCode> {
|
||||||
if path.starts_with('~') {
|
if path.starts_with('~') {
|
||||||
let home = dirs::home_dir()
|
let home = dirs::home_dir().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
Ok(home.join(path.strip_prefix("~/").unwrap_or("")))
|
Ok(home.join(path.strip_prefix("~/").unwrap_or("")))
|
||||||
} else {
|
} else {
|
||||||
Ok(PathBuf::from(path))
|
Ok(PathBuf::from(path))
|
||||||
|
|
@ -76,34 +75,40 @@ pub async fn get_file_info(
|
||||||
) -> Result<Json<FileMetadata>, StatusCode> {
|
) -> Result<Json<FileMetadata>, StatusCode> {
|
||||||
let path = expand_path(¶ms.path)?;
|
let path = expand_path(¶ms.path)?;
|
||||||
|
|
||||||
let metadata = fs::metadata(&path).await
|
let metadata = fs::metadata(&path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
let name = path.file_name()
|
let name = path
|
||||||
|
.file_name()
|
||||||
.map(|n| n.to_string_lossy().to_string())
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| path.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())
|
.map(|m| m.file_type().is_symlink())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let hidden = name.starts_with('.');
|
let hidden = name.starts_with('.');
|
||||||
|
|
||||||
let created = metadata.created()
|
let created = metadata
|
||||||
|
.created()
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||||
datetime.to_rfc3339()
|
datetime.to_rfc3339()
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let modified = metadata.modified()
|
let modified = metadata
|
||||||
|
.modified()
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||||
datetime.to_rfc3339()
|
datetime.to_rfc3339()
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let accessed = metadata.accessed()
|
let accessed = metadata
|
||||||
|
.accessed()
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||||
datetime.to_rfc3339()
|
datetime.to_rfc3339()
|
||||||
|
|
@ -118,23 +123,24 @@ pub async fn get_file_info(
|
||||||
|
|
||||||
let mime_type = if metadata.is_file() {
|
let mime_type = if metadata.is_file() {
|
||||||
// Simple MIME type detection based on extension
|
// Simple MIME type detection based on extension
|
||||||
let ext = path.extension()
|
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||||
.and_then(|e| e.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
Some(match ext {
|
Some(
|
||||||
"txt" => "text/plain",
|
match ext {
|
||||||
"html" | "htm" => "text/html",
|
"txt" => "text/plain",
|
||||||
"css" => "text/css",
|
"html" | "htm" => "text/html",
|
||||||
"js" => "application/javascript",
|
"css" => "text/css",
|
||||||
"json" => "application/json",
|
"js" => "application/javascript",
|
||||||
"png" => "image/png",
|
"json" => "application/json",
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"png" => "image/png",
|
||||||
"gif" => "image/gif",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
"pdf" => "application/pdf",
|
"gif" => "image/gif",
|
||||||
"zip" => "application/zip",
|
"pdf" => "application/pdf",
|
||||||
_ => "application/octet-stream",
|
"zip" => "application/zip",
|
||||||
}.to_string())
|
_ => "application/octet-stream",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -160,13 +166,12 @@ pub async fn get_file_info(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read file contents
|
/// Read file contents
|
||||||
pub async fn read_file(
|
pub async fn read_file(Query(params): Query<FileQuery>) -> Result<Response, StatusCode> {
|
||||||
Query(params): Query<FileQuery>,
|
|
||||||
) -> Result<Response, StatusCode> {
|
|
||||||
let path = expand_path(¶ms.path)?;
|
let path = expand_path(¶ms.path)?;
|
||||||
|
|
||||||
// Check if file exists and is a file
|
// 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)?;
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
if !metadata.is_file() {
|
if !metadata.is_file() {
|
||||||
|
|
@ -174,15 +179,18 @@ pub async fn read_file(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read file contents
|
// 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)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
let mut contents = Vec::new();
|
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)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
// Determine content type
|
// Determine content type
|
||||||
let content_type = path.extension()
|
let content_type = path
|
||||||
|
.extension()
|
||||||
.and_then(|e| e.to_str())
|
.and_then(|e| e.to_str())
|
||||||
.and_then(|ext| match ext {
|
.and_then(|ext| match ext {
|
||||||
"txt" => Some("text/plain"),
|
"txt" => Some("text/plain"),
|
||||||
|
|
@ -198,10 +206,7 @@ pub async fn read_file(
|
||||||
})
|
})
|
||||||
.unwrap_or("application/octet-stream");
|
.unwrap_or("application/octet-stream");
|
||||||
|
|
||||||
Ok((
|
Ok(([(header::CONTENT_TYPE, content_type)], contents).into_response())
|
||||||
[(header::CONTENT_TYPE, content_type)],
|
|
||||||
contents,
|
|
||||||
).into_response())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write file contents
|
/// Write file contents
|
||||||
|
|
@ -212,19 +217,22 @@ pub async fn write_file(
|
||||||
|
|
||||||
// Ensure parent directory exists
|
// Ensure parent directory exists
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent).await
|
fs::create_dir_all(parent)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
let content = if req.encoding.as_deref() == Some("base64") {
|
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)?
|
.map_err(|_| StatusCode::BAD_REQUEST)?
|
||||||
} else {
|
} else {
|
||||||
req.content.into_bytes()
|
req.content.into_bytes()
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::write(&path, content).await
|
fs::write(&path, content)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
Ok(Json(OperationResult {
|
Ok(Json(OperationResult {
|
||||||
|
|
@ -240,15 +248,18 @@ pub async fn delete_file(
|
||||||
let path = expand_path(¶ms.path)?;
|
let path = expand_path(¶ms.path)?;
|
||||||
|
|
||||||
// Check if path exists
|
// Check if path exists
|
||||||
let metadata = fs::metadata(&path).await
|
let metadata = fs::metadata(&path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
// Delete based on type
|
// Delete based on type
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
fs::remove_dir_all(&path).await
|
fs::remove_dir_all(&path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
} else {
|
} else {
|
||||||
fs::remove_file(&path).await
|
fs::remove_file(&path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,9 +270,7 @@ pub async fn delete_file(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move/rename file or directory
|
/// Move/rename file or directory
|
||||||
pub async fn move_file(
|
pub async fn move_file(Json(req): Json<MoveRequest>) -> Result<Json<OperationResult>, StatusCode> {
|
||||||
Json(req): Json<MoveRequest>,
|
|
||||||
) -> Result<Json<OperationResult>, StatusCode> {
|
|
||||||
let from_path = expand_path(&req.from)?;
|
let from_path = expand_path(&req.from)?;
|
||||||
let to_path = expand_path(&req.to)?;
|
let to_path = expand_path(&req.to)?;
|
||||||
|
|
||||||
|
|
@ -277,29 +286,34 @@ pub async fn move_file(
|
||||||
|
|
||||||
// Ensure destination parent directory exists
|
// Ensure destination parent directory exists
|
||||||
if let Some(parent) = to_path.parent() {
|
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)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the file/directory
|
// Move the file/directory
|
||||||
fs::rename(&from_path, &to_path).await
|
fs::rename(&from_path, &to_path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
Ok(Json(OperationResult {
|
Ok(Json(OperationResult {
|
||||||
success: true,
|
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
|
/// Copy file or directory
|
||||||
pub async fn copy_file(
|
pub async fn copy_file(Json(req): Json<CopyRequest>) -> Result<Json<OperationResult>, StatusCode> {
|
||||||
Json(req): Json<CopyRequest>,
|
|
||||||
) -> Result<Json<OperationResult>, StatusCode> {
|
|
||||||
let from_path = expand_path(&req.from)?;
|
let from_path = expand_path(&req.from)?;
|
||||||
let to_path = expand_path(&req.to)?;
|
let to_path = expand_path(&req.to)?;
|
||||||
|
|
||||||
// Check if source exists
|
// Check if source exists
|
||||||
let metadata = fs::metadata(&from_path).await
|
let metadata = fs::metadata(&from_path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
// Check if destination already exists
|
// Check if destination already exists
|
||||||
|
|
@ -309,13 +323,15 @@ pub async fn copy_file(
|
||||||
|
|
||||||
// Ensure destination parent directory exists
|
// Ensure destination parent directory exists
|
||||||
if let Some(parent) = to_path.parent() {
|
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)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy based on type
|
// Copy based on type
|
||||||
if metadata.is_file() {
|
if metadata.is_file() {
|
||||||
fs::copy(&from_path, &to_path).await
|
fs::copy(&from_path, &to_path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
} else if metadata.is_dir() {
|
} else if metadata.is_dir() {
|
||||||
// Recursive directory copy
|
// Recursive directory copy
|
||||||
|
|
@ -324,29 +340,40 @@ pub async fn copy_file(
|
||||||
|
|
||||||
Ok(Json(OperationResult {
|
Ok(Json(OperationResult {
|
||||||
success: true,
|
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
|
/// Recursively copy a directory
|
||||||
async fn copy_dir_recursive(from: &PathBuf, to: &PathBuf) -> Result<(), StatusCode> {
|
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)?;
|
.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)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
while let Some(entry) = entries.next_entry().await
|
while let Some(entry) = entries
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
{
|
||||||
let from_path = entry.path();
|
let from_path = entry.path();
|
||||||
let to_path = to.join(entry.file_name());
|
let to_path = to.join(entry.file_name());
|
||||||
|
|
||||||
let metadata = entry.metadata().await
|
let metadata = entry
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
if metadata.is_file() {
|
if metadata.is_file() {
|
||||||
fs::copy(&from_path, &to_path).await
|
fs::copy(&from_path, &to_path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
} else if metadata.is_dir() {
|
} else if metadata.is_dir() {
|
||||||
Box::pin(copy_dir_recursive(&from_path, &to_path)).await?;
|
Box::pin(copy_dir_recursive(&from_path, &to_path)).await?;
|
||||||
|
|
@ -396,17 +423,22 @@ async fn search_recursive(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut entries = fs::read_dir(path).await
|
let mut entries = fs::read_dir(path)
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
while let Some(entry) = entries.next_entry().await
|
while let Some(entry) = entries
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
{
|
||||||
let entry_path = entry.path();
|
let entry_path = entry.path();
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
if file_name.to_lowercase().contains(pattern) {
|
if file_name.to_lowercase().contains(pattern) {
|
||||||
let metadata = entry.metadata().await
|
let metadata = entry
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
results.push(SearchResult {
|
results.push(SearchResult {
|
||||||
|
|
@ -418,10 +450,15 @@ async fn search_recursive(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse into directories
|
// Recurse into directories
|
||||||
if entry.file_type().await
|
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
|
||||||
.map(|t| t.is_dir())
|
Box::pin(search_recursive(
|
||||||
.unwrap_or(false) {
|
&entry_path,
|
||||||
Box::pin(search_recursive(&entry_path, pattern, depth + 1, max_depth, results)).await?;
|
pattern,
|
||||||
|
depth + 1,
|
||||||
|
max_depth,
|
||||||
|
results,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
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 api_testing;
|
||||||
pub mod auth_cache;
|
|
||||||
pub mod terminal_integrations;
|
|
||||||
pub mod app_mover;
|
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 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)]
|
#[cfg(mobile)]
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
|
|
|
||||||
|
|
@ -3,44 +3,44 @@
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager, Emitter, WindowEvent};
|
|
||||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
|
||||||
use tauri::menu::Menu;
|
use tauri::menu::Menu;
|
||||||
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||||
|
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
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 api_testing;
|
||||||
mod auth_cache;
|
|
||||||
mod terminal_integrations;
|
|
||||||
mod app_mover;
|
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 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::ServerStatus;
|
||||||
|
use commands::*;
|
||||||
|
use server::HttpServer;
|
||||||
|
use state::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
||||||
|
|
@ -53,7 +53,7 @@ fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
||||||
tauri::WebviewWindowBuilder::new(
|
tauri::WebviewWindowBuilder::new(
|
||||||
&app,
|
&app,
|
||||||
"settings",
|
"settings",
|
||||||
tauri::WebviewUrl::App("settings.html".into())
|
tauri::WebviewUrl::App("settings.html".into()),
|
||||||
)
|
)
|
||||||
.title("VibeTunnel Settings")
|
.title("VibeTunnel Settings")
|
||||||
.inner_size(800.0, 600.0)
|
.inner_size(800.0, 600.0)
|
||||||
|
|
@ -66,10 +66,13 @@ fn open_settings_window(app: AppHandle) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_tray_menu_status(_app: &AppHandle, port: u16, _session_count: usize) {
|
fn update_tray_menu_status(app: &AppHandle, port: u16, session_count: usize) {
|
||||||
// For now, just log the status update
|
// Update tray menu status using the tray menu manager
|
||||||
// TODO: In Tauri v2, dynamic menu updates require rebuilding the menu
|
let app_handle = app.clone();
|
||||||
tracing::info!("Server status updated: port {}", port);
|
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() {
|
fn main() {
|
||||||
|
|
@ -317,7 +320,11 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create system tray icon using menu-bar-icon.png with template mode
|
// 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) {
|
let tray_icon = if let Ok(icon_data) = std::fs::read(&icon_path) {
|
||||||
tauri::image::Image::from_bytes(&icon_data).ok()
|
tauri::image::Image::from_bytes(&icon_data).ok()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -362,7 +369,8 @@ fn main() {
|
||||||
let settings = settings::Settings::load().unwrap_or_default();
|
let settings = settings::Settings::load().unwrap_or_default();
|
||||||
|
|
||||||
// Check if launched at startup (auto-launch)
|
// 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();
|
let window = app.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
|
|
@ -399,7 +407,9 @@ fn main() {
|
||||||
{
|
{
|
||||||
if let Ok(settings) = settings::Settings::load() {
|
if let Ok(settings) = settings::Settings::load() {
|
||||||
if !settings.general.show_dock_icon {
|
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,6 +430,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
#[allow(dead_code)]
|
||||||
fn create_app_menu(app: &tauri::App) -> Result<Menu<tauri::Wry>, tauri::Error> {
|
fn create_app_menu(app: &tauri::App) -> Result<Menu<tauri::Wry>, tauri::Error> {
|
||||||
// Create the menu using the builder pattern
|
// Create the menu using the builder pattern
|
||||||
let menu = Menu::new(app)?;
|
let menu = Menu::new(app)?;
|
||||||
|
|
@ -560,7 +571,16 @@ fn show_main_window(app: AppHandle) -> Result<(), String> {
|
||||||
fn quit_app(app: AppHandle) {
|
fn quit_app(app: AppHandle) {
|
||||||
// Stop monitoring before exit
|
// Stop monitoring before exit
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
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);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,14 +598,20 @@ async fn start_server_with_monitoring(app_handle: AppHandle) {
|
||||||
update_tray_menu_status(&app_handle, status.port, 0);
|
update_tray_menu_status(&app_handle, status.port, 0);
|
||||||
|
|
||||||
// Show notification
|
// 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) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to start server: {}", e);
|
tracing::error!("Failed to start server: {}", e);
|
||||||
let _ = state.notification_manager.notify_error(
|
let _ = state
|
||||||
"Server Start Failed",
|
.notification_manager
|
||||||
&format!("Failed to start server: {}", e)
|
.notify_error(
|
||||||
).await;
|
"Server Start Failed",
|
||||||
|
&format!("Failed to start server: {}", e),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,7 +622,10 @@ async fn start_server_with_monitoring(app_handle: AppHandle) {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut check_interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
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_interval.tick().await;
|
||||||
|
|
||||||
// Check if server is still running
|
// Check if server is still running
|
||||||
|
|
@ -633,14 +662,20 @@ async fn start_server_with_monitoring(app_handle: AppHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification
|
// 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) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to restart server: {}", e);
|
tracing::error!("Failed to restart server: {}", e);
|
||||||
let _ = monitoring_state.notification_manager.notify_error(
|
let _ = monitoring_state
|
||||||
"Server Restart Failed",
|
.notification_manager
|
||||||
&format!("Failed to restart server: {}", e)
|
.notify_error(
|
||||||
).await;
|
"Server Restart Failed",
|
||||||
|
&format!("Failed to restart server: {}", e),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -660,14 +695,20 @@ async fn start_server_with_monitoring(app_handle: AppHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification
|
// 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) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to start server: {}", e);
|
tracing::error!("Failed to start server: {}", e);
|
||||||
let _ = monitoring_state.notification_manager.notify_error(
|
let _ = monitoring_state
|
||||||
"Server Start Failed",
|
.notification_manager
|
||||||
&format!("Failed to start server: {}", e)
|
.notify_error(
|
||||||
).await;
|
"Server Start Failed",
|
||||||
|
&format!("Failed to start server: {}", e),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -726,19 +767,27 @@ async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String>
|
||||||
let settings = crate::settings::Settings::load().unwrap_or_default();
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
||||||
|
|
||||||
// Start HTTP server with auth if configured
|
// Start HTTP server with auth if configured
|
||||||
let mut http_server = if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() {
|
let mut http_server =
|
||||||
let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password));
|
if settings.dashboard.enable_password && !settings.dashboard.password.is_empty() {
|
||||||
HttpServer::with_auth(state.terminal_manager.clone(), state.session_monitor.clone(), auth_config)
|
let auth_config = crate::auth::AuthConfig::new(true, Some(settings.dashboard.password));
|
||||||
} else {
|
HttpServer::with_auth(
|
||||||
HttpServer::new(state.terminal_manager.clone(), state.session_monitor.clone())
|
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
|
// Start server with appropriate access mode
|
||||||
let (port, url) = match settings.dashboard.access_mode.as_str() {
|
let (port, url) = match settings.dashboard.access_mode.as_str() {
|
||||||
"network" => {
|
"network" => {
|
||||||
let port = http_server.start_with_mode("network").await?;
|
let port = http_server.start_with_mode("network").await?;
|
||||||
(port, format!("http://0.0.0.0:{}", port))
|
(port, format!("http://0.0.0.0:{}", port))
|
||||||
},
|
}
|
||||||
"ngrok" => {
|
"ngrok" => {
|
||||||
// For ngrok mode, start in localhost and let ngrok handle the tunneling
|
// For ngrok mode, start in localhost and let ngrok handle the tunneling
|
||||||
let port = http_server.start_with_mode("localhost").await?;
|
let port = http_server.start_with_mode("localhost").await?;
|
||||||
|
|
@ -746,7 +795,11 @@ async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String>
|
||||||
// Try to start ngrok tunnel if auth token is configured
|
// Try to start ngrok tunnel if auth token is configured
|
||||||
let url = if let Some(auth_token) = settings.advanced.ngrok_auth_token {
|
let url = if let Some(auth_token) = settings.advanced.ngrok_auth_token {
|
||||||
if !auth_token.is_empty() {
|
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,
|
Ok(tunnel) => tunnel.url,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to start ngrok tunnel: {}", e);
|
tracing::error!("Failed to start ngrok tunnel: {}", e);
|
||||||
|
|
@ -761,7 +814,7 @@ async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String>
|
||||||
};
|
};
|
||||||
|
|
||||||
(port, url)
|
(port, url)
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let port = http_server.start_with_mode("localhost").await?;
|
let port = http_server.start_with_mode("localhost").await?;
|
||||||
(port, format!("http://localhost:{}", port))
|
(port, format!("http://localhost:{}", port))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
|
@ -106,12 +106,16 @@ impl NetworkUtils {
|
||||||
let name = ifaddr.interface_name.clone();
|
let name = ifaddr.interface_name.clone();
|
||||||
let flags = ifaddr.flags;
|
let flags = ifaddr.flags;
|
||||||
|
|
||||||
let interface = interfaces.entry(name.clone()).or_insert_with(|| NetworkInterface {
|
let interface =
|
||||||
name,
|
interfaces
|
||||||
addresses: Vec::new(),
|
.entry(name.clone())
|
||||||
is_up: flags.contains(nix::net::if_::InterfaceFlags::IFF_UP),
|
.or_insert_with(|| NetworkInterface {
|
||||||
is_loopback: flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK),
|
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(address) = ifaddr.address {
|
||||||
if let Some(sockaddr) = address.as_sockaddr_in() {
|
if let Some(sockaddr) = address.as_sockaddr_in() {
|
||||||
|
|
@ -244,9 +248,9 @@ impl NetworkUtils {
|
||||||
|
|
||||||
/// Test network connectivity to a host
|
/// Test network connectivity to a host
|
||||||
pub async fn test_connectivity(host: &str, port: u16) -> bool {
|
pub async fn test_connectivity(host: &str, port: u16) -> bool {
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
let addr = format!("{}:{}", host, port);
|
let addr = format!("{}:{}", host, port);
|
||||||
match timeout(Duration::from_secs(3), TcpStream::connect(&addr)).await {
|
match timeout(Duration::from_secs(3), TcpStream::connect(&addr)).await {
|
||||||
|
|
@ -282,8 +286,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_private_ipv4() {
|
fn test_private_ipv4() {
|
||||||
assert!(NetworkUtils::is_private_ipv4(&"10.0.0.1".parse().unwrap()));
|
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(
|
||||||
assert!(NetworkUtils::is_private_ipv4(&"192.168.1.1".parse().unwrap()));
|
&"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()));
|
assert!(!NetworkUtils::is_private_ipv4(&"8.8.8.8".parse().unwrap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
use crate::state::AppState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::process::{Command, Child};
|
use std::process::{Child, Command};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::state::AppState;
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -25,7 +25,11 @@ impl NgrokManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_tunnel(&self, port: u16, auth_token: Option<String>) -> Result<NgrokTunnel, String> {
|
pub async fn start_tunnel(
|
||||||
|
&self,
|
||||||
|
port: u16,
|
||||||
|
auth_token: Option<String>,
|
||||||
|
) -> Result<NgrokTunnel, String> {
|
||||||
// Check if ngrok is installed
|
// Check if ngrok is installed
|
||||||
let ngrok_path = which::which("ngrok")
|
let ngrok_path = which::which("ngrok")
|
||||||
.map_err(|_| "ngrok not found. Please install ngrok first.".to_string())?;
|
.map_err(|_| "ngrok not found. Please install ngrok first.".to_string())?;
|
||||||
|
|
@ -61,7 +65,8 @@ impl NgrokManager {
|
||||||
|
|
||||||
pub async fn stop_tunnel(&self) -> Result<(), String> {
|
pub async fn stop_tunnel(&self) -> Result<(), String> {
|
||||||
if let Some(mut child) = self.process.lock().unwrap().take() {
|
if let Some(mut child) = self.process.lock().unwrap().take() {
|
||||||
child.kill()
|
child
|
||||||
|
.kill()
|
||||||
.map_err(|e| format!("Failed to stop ngrok: {}", e))?;
|
.map_err(|e| format!("Failed to stop ngrok: {}", e))?;
|
||||||
|
|
||||||
info!("ngrok tunnel stopped");
|
info!("ngrok tunnel stopped");
|
||||||
|
|
@ -82,23 +87,28 @@ impl NgrokManager {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to query ngrok API: {}", e))?;
|
.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
|
.await
|
||||||
.map_err(|e| format!("Failed to parse ngrok API response: {}", e))?;
|
.map_err(|e| format!("Failed to parse ngrok API response: {}", e))?;
|
||||||
|
|
||||||
// Extract tunnel URL
|
// Extract tunnel URL
|
||||||
let tunnels = data["tunnels"].as_array()
|
let tunnels = data["tunnels"]
|
||||||
|
.as_array()
|
||||||
.ok_or_else(|| "No tunnels found".to_string())?;
|
.ok_or_else(|| "No tunnels found".to_string())?;
|
||||||
|
|
||||||
let tunnel = tunnels.iter()
|
let tunnel = tunnels
|
||||||
|
.iter()
|
||||||
.find(|t| t["proto"].as_str() == Some("https"))
|
.find(|t| t["proto"].as_str() == Some("https"))
|
||||||
.or_else(|| tunnels.first())
|
.or_else(|| tunnels.first())
|
||||||
.ok_or_else(|| "No tunnel found".to_string())?;
|
.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())?;
|
.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(|addr| addr.split(':').last())
|
||||||
.and_then(|p| p.parse::<u16>().ok())
|
.and_then(|p| p.parse::<u16>().ok())
|
||||||
.unwrap_or(3000);
|
.unwrap_or(3000);
|
||||||
|
|
@ -121,15 +131,11 @@ pub async fn start_ngrok_tunnel(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn stop_ngrok_tunnel(
|
pub async fn stop_ngrok_tunnel(state: State<'_, AppState>) -> Result<(), String> {
|
||||||
state: State<'_, AppState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
state.ngrok_manager.stop_tunnel().await
|
state.ngrok_manager.stop_tunnel().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_ngrok_status(
|
pub async fn get_ngrok_status(state: State<'_, AppState>) -> Result<Option<NgrokTunnel>, String> {
|
||||||
state: State<'_, AppState>,
|
|
||||||
) -> Result<Option<NgrokTunnel>, String> {
|
|
||||||
Ok(state.ngrok_manager.get_tunnel_status())
|
Ok(state.ngrok_manager.get_tunnel_status())
|
||||||
}
|
}
|
||||||
|
|
@ -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::{AppHandle, Emitter};
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use std::collections::HashMap;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
/// Notification type enumeration
|
/// Notification type enumeration
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -154,7 +154,10 @@ impl NotificationManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store notification
|
// 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
|
// Add to history
|
||||||
let mut history = self.notification_history.write().await;
|
let mut history = self.notification_history.write().await;
|
||||||
|
|
@ -168,8 +171,11 @@ impl NotificationManager {
|
||||||
|
|
||||||
// Show system notification if enabled
|
// Show system notification if enabled
|
||||||
if settings.show_in_system {
|
if settings.show_in_system {
|
||||||
match self.show_system_notification(&title, &body, notification_type).await {
|
match self
|
||||||
Ok(_) => {},
|
.show_system_notification(&title, &body, notification_type)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to show system notification: {}", e);
|
tracing::error!("Failed to show system notification: {}", e);
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +184,8 @@ impl NotificationManager {
|
||||||
|
|
||||||
// Emit notification event to frontend
|
// Emit notification event to frontend
|
||||||
if let Some(app_handle) = self.app_handle.read().await.as_ref() {
|
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))?;
|
.map_err(|e| format!("Failed to emit notification event: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,13 +200,11 @@ impl NotificationManager {
|
||||||
notification_type: NotificationType,
|
notification_type: NotificationType,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let app_handle_guard = self.app_handle.read().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())?;
|
.ok_or_else(|| "App handle not set".to_string())?;
|
||||||
|
|
||||||
let mut builder = app_handle.notification()
|
let mut builder = app_handle.notification().builder().title(title).body(body);
|
||||||
.builder()
|
|
||||||
.title(title)
|
|
||||||
.body(body);
|
|
||||||
|
|
||||||
// Set icon based on notification type
|
// Set icon based on notification type
|
||||||
let icon = match notification_type {
|
let icon = match notification_type {
|
||||||
|
|
@ -217,7 +222,8 @@ impl NotificationManager {
|
||||||
builder = builder.icon(icon_str);
|
builder = builder.icon(icon_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.show()
|
builder
|
||||||
|
.show()
|
||||||
.map_err(|e| format!("Failed to show notification: {}", e))?;
|
.map_err(|e| format!("Failed to show notification: {}", e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -263,7 +269,9 @@ impl NotificationManager {
|
||||||
|
|
||||||
/// Get unread notification count
|
/// Get unread notification count
|
||||||
pub async fn get_unread_count(&self) -> usize {
|
pub async fn get_unread_count(&self) -> usize {
|
||||||
self.notifications.read().await
|
self.notifications
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.values()
|
.values()
|
||||||
.filter(|n| !n.read)
|
.filter(|n| !n.read)
|
||||||
.count()
|
.count()
|
||||||
|
|
@ -311,50 +319,69 @@ impl NotificationManager {
|
||||||
body,
|
body,
|
||||||
vec![],
|
vec![],
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show update available notification
|
/// Show update available notification
|
||||||
pub async fn notify_update_available(&self, version: &str, download_url: &str) -> Result<String, String> {
|
pub async fn notify_update_available(
|
||||||
|
&self,
|
||||||
|
version: &str,
|
||||||
|
download_url: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
let mut metadata = HashMap::new();
|
let mut metadata = HashMap::new();
|
||||||
metadata.insert("version".to_string(), serde_json::Value::String(version.to_string()));
|
metadata.insert(
|
||||||
metadata.insert("download_url".to_string(), serde_json::Value::String(download_url.to_string()));
|
"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(
|
self.show_notification(
|
||||||
NotificationType::UpdateAvailable,
|
NotificationType::UpdateAvailable,
|
||||||
NotificationPriority::High,
|
NotificationPriority::High,
|
||||||
"Update Available".to_string(),
|
"Update Available".to_string(),
|
||||||
format!("VibeTunnel {} is now available. Click to download.", version),
|
format!(
|
||||||
vec![
|
"VibeTunnel {} is now available. Click to download.",
|
||||||
NotificationAction {
|
version
|
||||||
id: "download".to_string(),
|
),
|
||||||
label: "Download".to_string(),
|
vec![NotificationAction {
|
||||||
action_type: "open_url".to_string(),
|
id: "download".to_string(),
|
||||||
}
|
label: "Download".to_string(),
|
||||||
],
|
action_type: "open_url".to_string(),
|
||||||
|
}],
|
||||||
metadata,
|
metadata,
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show permission required notification
|
/// Show permission required notification
|
||||||
pub async fn notify_permission_required(&self, permission: &str, reason: &str) -> Result<String, String> {
|
pub async fn notify_permission_required(
|
||||||
|
&self,
|
||||||
|
permission: &str,
|
||||||
|
reason: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
let mut metadata = HashMap::new();
|
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(
|
self.show_notification(
|
||||||
NotificationType::PermissionRequired,
|
NotificationType::PermissionRequired,
|
||||||
NotificationPriority::High,
|
NotificationPriority::High,
|
||||||
"Permission Required".to_string(),
|
"Permission Required".to_string(),
|
||||||
format!("{} permission is required: {}", permission, reason),
|
format!("{} permission is required: {}", permission, reason),
|
||||||
vec![
|
vec![NotificationAction {
|
||||||
NotificationAction {
|
id: "grant".to_string(),
|
||||||
id: "grant".to_string(),
|
label: "Grant Permission".to_string(),
|
||||||
label: "Grant Permission".to_string(),
|
action_type: "request_permission".to_string(),
|
||||||
action_type: "request_permission".to_string(),
|
}],
|
||||||
}
|
|
||||||
],
|
|
||||||
metadata,
|
metadata,
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show error notification
|
/// Show error notification
|
||||||
|
|
@ -366,7 +393,8 @@ impl NotificationManager {
|
||||||
error_message.to_string(),
|
error_message.to_string(),
|
||||||
vec![],
|
vec![],
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show success notification
|
/// Show success notification
|
||||||
|
|
@ -378,6 +406,7 @@ impl NotificationManager {
|
||||||
message.to_string(),
|
message.to_string(),
|
||||||
vec![],
|
vec![],
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Permission type enumeration
|
/// Permission type enumeration
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -94,7 +94,10 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,91 +110,118 @@ impl PermissionsManager {
|
||||||
|
|
||||||
match platform {
|
match platform {
|
||||||
"macos" => {
|
"macos" => {
|
||||||
permissions.insert(PermissionType::ScreenRecording, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::ScreenRecording,
|
PermissionType::ScreenRecording,
|
||||||
status: PermissionStatus::NotDetermined,
|
PermissionInfo {
|
||||||
required: false,
|
permission_type: PermissionType::ScreenRecording,
|
||||||
platform_specific: true,
|
status: PermissionStatus::NotDetermined,
|
||||||
description: "Required for recording terminal sessions with system UI".to_string(),
|
required: false,
|
||||||
last_checked: None,
|
platform_specific: true,
|
||||||
request_count: 0,
|
description: "Required for recording terminal sessions with system UI"
|
||||||
});
|
.to_string(),
|
||||||
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
permissions.insert(PermissionType::Accessibility, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::Accessibility,
|
PermissionType::Accessibility,
|
||||||
status: PermissionStatus::NotDetermined,
|
PermissionInfo {
|
||||||
required: false,
|
permission_type: PermissionType::Accessibility,
|
||||||
platform_specific: true,
|
status: PermissionStatus::NotDetermined,
|
||||||
description: "Required for advanced terminal integration features".to_string(),
|
required: false,
|
||||||
last_checked: None,
|
platform_specific: true,
|
||||||
request_count: 0,
|
description: "Required for advanced terminal integration features"
|
||||||
});
|
.to_string(),
|
||||||
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
permissions.insert(PermissionType::NotificationAccess, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::NotificationAccess,
|
PermissionType::NotificationAccess,
|
||||||
status: PermissionStatus::NotDetermined,
|
PermissionInfo {
|
||||||
required: false,
|
permission_type: PermissionType::NotificationAccess,
|
||||||
platform_specific: true,
|
status: PermissionStatus::NotDetermined,
|
||||||
description: "Required to show system notifications".to_string(),
|
required: false,
|
||||||
last_checked: None,
|
platform_specific: true,
|
||||||
request_count: 0,
|
description: "Required to show system notifications".to_string(),
|
||||||
});
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
"windows" => {
|
"windows" => {
|
||||||
permissions.insert(PermissionType::TerminalAccess, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::TerminalAccess,
|
PermissionType::TerminalAccess,
|
||||||
status: PermissionStatus::NotDetermined,
|
PermissionInfo {
|
||||||
required: true,
|
permission_type: PermissionType::TerminalAccess,
|
||||||
platform_specific: true,
|
status: PermissionStatus::NotDetermined,
|
||||||
description: "Required to create and manage terminal sessions".to_string(),
|
required: true,
|
||||||
last_checked: None,
|
platform_specific: true,
|
||||||
request_count: 0,
|
description: "Required to create and manage terminal sessions".to_string(),
|
||||||
});
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
permissions.insert(PermissionType::AutoStart, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::AutoStart,
|
PermissionType::AutoStart,
|
||||||
status: PermissionStatus::NotDetermined,
|
PermissionInfo {
|
||||||
required: false,
|
permission_type: PermissionType::AutoStart,
|
||||||
platform_specific: true,
|
status: PermissionStatus::NotDetermined,
|
||||||
description: "Required to start VibeTunnel with Windows".to_string(),
|
required: false,
|
||||||
last_checked: None,
|
platform_specific: true,
|
||||||
request_count: 0,
|
description: "Required to start VibeTunnel with Windows".to_string(),
|
||||||
});
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
"linux" => {
|
"linux" => {
|
||||||
permissions.insert(PermissionType::FileSystemFull, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::FileSystemFull,
|
PermissionType::FileSystemFull,
|
||||||
status: PermissionStatus::Granted,
|
PermissionInfo {
|
||||||
required: true,
|
permission_type: PermissionType::FileSystemFull,
|
||||||
platform_specific: false,
|
status: PermissionStatus::Granted,
|
||||||
description: "Required for saving recordings and configurations".to_string(),
|
required: true,
|
||||||
last_checked: None,
|
platform_specific: false,
|
||||||
request_count: 0,
|
description: "Required for saving recordings and configurations"
|
||||||
});
|
.to_string(),
|
||||||
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add common permissions
|
// Add common permissions
|
||||||
permissions.insert(PermissionType::NetworkAccess, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::NetworkAccess,
|
PermissionType::NetworkAccess,
|
||||||
status: PermissionStatus::Granted,
|
PermissionInfo {
|
||||||
required: true,
|
permission_type: PermissionType::NetworkAccess,
|
||||||
platform_specific: false,
|
status: PermissionStatus::Granted,
|
||||||
description: "Required for web server and remote access features".to_string(),
|
required: true,
|
||||||
last_checked: None,
|
platform_specific: false,
|
||||||
request_count: 0,
|
description: "Required for web server and remote access features".to_string(),
|
||||||
});
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
permissions.insert(PermissionType::FileSystemRestricted, PermissionInfo {
|
permissions.insert(
|
||||||
permission_type: PermissionType::FileSystemRestricted,
|
PermissionType::FileSystemRestricted,
|
||||||
status: PermissionStatus::Granted,
|
PermissionInfo {
|
||||||
required: true,
|
permission_type: PermissionType::FileSystemRestricted,
|
||||||
platform_specific: false,
|
status: PermissionStatus::Granted,
|
||||||
description: "Required for basic application functionality".to_string(),
|
required: true,
|
||||||
last_checked: None,
|
platform_specific: false,
|
||||||
request_count: 0,
|
description: "Required for basic application functionality".to_string(),
|
||||||
});
|
last_checked: None,
|
||||||
|
request_count: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
permissions
|
permissions
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +281,10 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request permission
|
/// Request permission
|
||||||
pub async fn request_permission(&self, permission_type: PermissionType) -> Result<PermissionRequestResult, String> {
|
pub async fn request_permission(
|
||||||
|
&self,
|
||||||
|
permission_type: PermissionType,
|
||||||
|
) -> Result<PermissionRequestResult, String> {
|
||||||
// Update request count
|
// Update request count
|
||||||
if let Some(info) = self.permissions.write().await.get_mut(&permission_type) {
|
if let Some(info) = self.permissions.write().await.get_mut(&permission_type) {
|
||||||
info.request_count += 1;
|
info.request_count += 1;
|
||||||
|
|
@ -283,7 +316,10 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get permission info
|
/// Get permission info
|
||||||
pub async fn get_permission_info(&self, permission_type: PermissionType) -> Option<PermissionInfo> {
|
pub async fn get_permission_info(
|
||||||
|
&self,
|
||||||
|
permission_type: PermissionType,
|
||||||
|
) -> Option<PermissionInfo> {
|
||||||
self.permissions.read().await.get(&permission_type).cloned()
|
self.permissions.read().await.get(&permission_type).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +330,9 @@ impl PermissionsManager {
|
||||||
|
|
||||||
/// Get required permissions
|
/// Get required permissions
|
||||||
pub async fn get_required_permissions(&self) -> Vec<PermissionInfo> {
|
pub async fn get_required_permissions(&self) -> Vec<PermissionInfo> {
|
||||||
self.permissions.read().await
|
self.permissions
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.values()
|
.values()
|
||||||
.filter(|info| info.required)
|
.filter(|info| info.required)
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
@ -303,7 +341,9 @@ impl PermissionsManager {
|
||||||
|
|
||||||
/// Get missing required permissions
|
/// Get missing required permissions
|
||||||
pub async fn get_missing_required_permissions(&self) -> Vec<PermissionInfo> {
|
pub async fn get_missing_required_permissions(&self) -> Vec<PermissionInfo> {
|
||||||
self.permissions.read().await
|
self.permissions
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.values()
|
.values()
|
||||||
.filter(|info| info.required && info.status != PermissionStatus::Granted)
|
.filter(|info| info.required && info.status != PermissionStatus::Granted)
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
@ -312,13 +352,19 @@ impl PermissionsManager {
|
||||||
|
|
||||||
/// Check if all required permissions are granted
|
/// Check if all required permissions are granted
|
||||||
pub async fn all_required_permissions_granted(&self) -> bool {
|
pub async fn all_required_permissions_granted(&self) -> bool {
|
||||||
!self.permissions.read().await
|
!self
|
||||||
|
.permissions
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.values()
|
.values()
|
||||||
.any(|info| info.required && info.status != PermissionStatus::Granted)
|
.any(|info| info.required && info.status != PermissionStatus::Granted)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open system settings for permission
|
/// 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;
|
let platform = std::env::consts::OS;
|
||||||
|
|
||||||
match (platform, permission_type) {
|
match (platform, permission_type) {
|
||||||
|
|
@ -335,9 +381,7 @@ impl PermissionsManager {
|
||||||
self.open_notification_settings_macos().await
|
self.open_notification_settings_macos().await
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
("windows", PermissionType::AutoStart) => {
|
("windows", PermissionType::AutoStart) => self.open_startup_settings_windows().await,
|
||||||
self.open_startup_settings_windows().await
|
|
||||||
}
|
|
||||||
_ => Err("No system settings available for this permission".to_string()),
|
_ => Err("No system settings available for this permission".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -360,13 +404,17 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn request_screen_recording_permission_macos(&self) -> Result<PermissionRequestResult, String> {
|
async fn request_screen_recording_permission_macos(
|
||||||
|
&self,
|
||||||
|
) -> Result<PermissionRequestResult, String> {
|
||||||
// Show notification about needing to grant permission
|
// Show notification about needing to grant permission
|
||||||
if let Some(notification_manager) = &self.notification_manager {
|
if let Some(notification_manager) = &self.notification_manager {
|
||||||
let _ = notification_manager.notify_permission_required(
|
let _ = notification_manager
|
||||||
"Screen Recording",
|
.notify_permission_required(
|
||||||
"VibeTunnel needs screen recording permission to capture terminal sessions"
|
"Screen Recording",
|
||||||
).await;
|
"VibeTunnel needs screen recording permission to capture terminal sessions",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open system preferences
|
// Open system preferences
|
||||||
|
|
@ -375,7 +423,9 @@ impl PermissionsManager {
|
||||||
Ok(PermissionRequestResult {
|
Ok(PermissionRequestResult {
|
||||||
permission_type: PermissionType::ScreenRecording,
|
permission_type: PermissionType::ScreenRecording,
|
||||||
status: PermissionStatus::NotDetermined,
|
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_restart: true,
|
||||||
requires_system_settings: true,
|
requires_system_settings: true,
|
||||||
})
|
})
|
||||||
|
|
@ -416,7 +466,9 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn request_accessibility_permission_macos(&self) -> Result<PermissionRequestResult, String> {
|
async fn request_accessibility_permission_macos(
|
||||||
|
&self,
|
||||||
|
) -> Result<PermissionRequestResult, String> {
|
||||||
let _ = self.open_accessibility_settings_macos().await;
|
let _ = self.open_accessibility_settings_macos().await;
|
||||||
|
|
||||||
Ok(PermissionRequestResult {
|
Ok(PermissionRequestResult {
|
||||||
|
|
@ -447,7 +499,9 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn request_notification_permission_macos(&self) -> Result<PermissionRequestResult, String> {
|
async fn request_notification_permission_macos(
|
||||||
|
&self,
|
||||||
|
) -> Result<PermissionRequestResult, String> {
|
||||||
Ok(PermissionRequestResult {
|
Ok(PermissionRequestResult {
|
||||||
permission_type: PermissionType::NotificationAccess,
|
permission_type: PermissionType::NotificationAccess,
|
||||||
status: PermissionStatus::Granted,
|
status: PermissionStatus::Granted,
|
||||||
|
|
@ -505,12 +559,17 @@ impl PermissionsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show permission required notification
|
/// 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 {
|
if let Some(notification_manager) = &self.notification_manager {
|
||||||
notification_manager.notify_permission_required(
|
notification_manager
|
||||||
&format!("{:?}", permission_info.permission_type),
|
.notify_permission_required(
|
||||||
&permission_info.description
|
&format!("{:?}", permission_info.permission_type),
|
||||||
).await?;
|
&permission_info.description,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::process::Command;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use serde::{Serialize, Deserialize};
|
use std::process::Command;
|
||||||
use tracing::{info, error};
|
use tracing::{error, info};
|
||||||
|
|
||||||
/// Information about a process using a port
|
/// Information about a process using a port
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -23,8 +23,13 @@ impl ProcessDetails {
|
||||||
|
|
||||||
/// Check if this is one of our managed servers
|
/// Check if this is one of our managed servers
|
||||||
pub fn is_managed_server(&self) -> bool {
|
pub fn is_managed_server(&self) -> bool {
|
||||||
self.name == "vibetunnel" ||
|
self.name == "vibetunnel"
|
||||||
self.name.contains("node") && self.path.as_ref().map(|p| p.contains("VibeTunnel")).unwrap_or(false)
|
|| self.name.contains("node")
|
||||||
|
&& self
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.contains("VibeTunnel"))
|
||||||
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,10 +138,7 @@ impl PortConflictResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to netstat
|
// Fallback to netstat
|
||||||
if let Ok(output) = Command::new("netstat")
|
if let Ok(output) = Command::new("netstat").args(&["-tulpn"]).output() {
|
||||||
.args(&["-tulpn"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
// Parse netstat output (simplified)
|
// Parse netstat output (simplified)
|
||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
|
|
@ -145,7 +147,8 @@ impl PortConflictResolver {
|
||||||
if let Some(pid_part) = line.split_whitespace().last() {
|
if let Some(pid_part) = line.split_whitespace().last() {
|
||||||
if let Some(pid_str) = pid_part.split('/').next() {
|
if let Some(pid_str) = pid_part.split('/').next() {
|
||||||
if let Ok(pid) = pid_str.parse::<u32>() {
|
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||||
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 {
|
let process_info = ProcessDetails {
|
||||||
pid,
|
pid,
|
||||||
name,
|
name,
|
||||||
|
|
@ -267,9 +270,7 @@ impl PortConflictResolver {
|
||||||
.args(&["-p", &pid.to_string(), "-o", "comm="])
|
.args(&["-p", &pid.to_string(), "-o", "comm="])
|
||||||
.output()
|
.output()
|
||||||
{
|
{
|
||||||
let path = String::from_utf8_lossy(&output.stdout)
|
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
if !path.is_empty() {
|
if !path.is_empty() {
|
||||||
return Some(path);
|
return Some(path);
|
||||||
}
|
}
|
||||||
|
|
@ -360,7 +361,10 @@ impl PortConflictResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine action for conflict resolution
|
/// Determine action for conflict resolution
|
||||||
fn determine_action(process: &ProcessDetails, root_process: &Option<ProcessDetails>) -> ConflictAction {
|
fn determine_action(
|
||||||
|
process: &ProcessDetails,
|
||||||
|
root_process: &Option<ProcessDetails>,
|
||||||
|
) -> ConflictAction {
|
||||||
// If it's our managed server, kill it
|
// If it's our managed server, kill it
|
||||||
if process.is_managed_server() {
|
if process.is_managed_server() {
|
||||||
return ConflictAction::KillOurInstance {
|
return ConflictAction::KillOurInstance {
|
||||||
|
|
@ -397,7 +401,10 @@ impl PortConflictResolver {
|
||||||
pub async fn resolve_conflict(conflict: &PortConflict) -> Result<(), String> {
|
pub async fn resolve_conflict(conflict: &PortConflict) -> Result<(), String> {
|
||||||
match &conflict.suggested_action {
|
match &conflict.suggested_action {
|
||||||
ConflictAction::KillOurInstance { pid, process_name } => {
|
ConflictAction::KillOurInstance { pid, process_name } => {
|
||||||
info!("Killing conflicting process: {} (PID: {})", process_name, pid);
|
info!(
|
||||||
|
"Killing conflicting process: {} (PID: {})",
|
||||||
|
process_name, pid
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
|
@ -436,7 +443,10 @@ impl PortConflictResolver {
|
||||||
|
|
||||||
/// Force kill a process
|
/// Force kill a process
|
||||||
pub async fn force_kill_process(conflict: &PortConflict) -> Result<(), String> {
|
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)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
use axum::{
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
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::response::sse::{Event, KeepAlive, Sse};
|
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 futures::stream::{Stream, StreamExt as FuturesStreamExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use tokio::time::{interval, Duration};
|
use std::fs;
|
||||||
use tower_http::cors::{CorsLayer, Any};
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use futures::sink::SinkExt;
|
use tokio::time::{interval, Duration};
|
||||||
use serde::{Deserialize, Serialize};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
use tracing::{info, error, debug};
|
use tracing::{debug, error, info};
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use crate::terminal::TerminalManager;
|
use crate::auth::{auth_middleware, check_auth, login, AuthConfig};
|
||||||
use crate::auth::{AuthConfig, auth_middleware, check_auth, login};
|
|
||||||
use crate::session_monitor::SessionMonitor;
|
use crate::session_monitor::SessionMonitor;
|
||||||
|
use crate::terminal::TerminalManager;
|
||||||
|
|
||||||
// Combined app state for Axum
|
// Combined app state for Axum
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -100,7 +99,10 @@ impl HttpServer {
|
||||||
self.port
|
self.port
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(terminal_manager: Arc<TerminalManager>, session_monitor: Arc<SessionMonitor>) -> Self {
|
pub fn new(
|
||||||
|
terminal_manager: Arc<TerminalManager>,
|
||||||
|
session_monitor: Arc<SessionMonitor>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
terminal_manager,
|
terminal_manager,
|
||||||
auth_config: Arc::new(AuthConfig::new(false, None)),
|
auth_config: Arc::new(AuthConfig::new(false, None)),
|
||||||
|
|
@ -111,7 +113,11 @@ impl HttpServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_auth(terminal_manager: Arc<TerminalManager>, session_monitor: Arc<SessionMonitor>, auth_config: AuthConfig) -> Self {
|
pub fn with_auth(
|
||||||
|
terminal_manager: Arc<TerminalManager>,
|
||||||
|
session_monitor: Arc<SessionMonitor>,
|
||||||
|
auth_config: AuthConfig,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
terminal_manager,
|
terminal_manager,
|
||||||
auth_config: Arc::new(auth_config),
|
auth_config: Arc::new(auth_config),
|
||||||
|
|
@ -122,6 +128,7 @@ impl HttpServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn start(&mut self) -> Result<u16, String> {
|
pub async fn start(&mut self) -> Result<u16, String> {
|
||||||
self.start_with_mode("localhost").await
|
self.start_with_mode("localhost").await
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +137,7 @@ impl HttpServer {
|
||||||
// Determine bind address based on mode
|
// Determine bind address based on mode
|
||||||
let bind_addr = match mode {
|
let bind_addr = match mode {
|
||||||
"localhost" => "127.0.0.1:0",
|
"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",
|
_ => "127.0.0.1:0",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -139,7 +146,8 @@ impl HttpServer {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to bind to {}: {}", bind_addr, e))?;
|
.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))?;
|
.map_err(|e| format!("Failed to get local address: {}", e))?;
|
||||||
|
|
||||||
self.port = addr.port();
|
self.port = addr.port();
|
||||||
|
|
@ -155,11 +163,10 @@ impl HttpServer {
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let server = axum::serve(listener, app)
|
let server = axum::serve(listener, app).with_graceful_shutdown(async {
|
||||||
.with_graceful_shutdown(async {
|
let _ = shutdown_rx.await;
|
||||||
let _ = shutdown_rx.await;
|
info!("Graceful shutdown initiated");
|
||||||
info!("Graceful shutdown initiated");
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(e) = server.await {
|
if let Err(e) = server.await {
|
||||||
error!("Server error: {}", e);
|
error!("Server error: {}", e);
|
||||||
|
|
@ -183,10 +190,7 @@ impl HttpServer {
|
||||||
|
|
||||||
// Wait for server task to complete
|
// Wait for server task to complete
|
||||||
if let Some(handle) = self.handle.take() {
|
if let Some(handle) = self.handle.take() {
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(tokio::time::Duration::from_secs(10), handle).await {
|
||||||
tokio::time::Duration::from_secs(10),
|
|
||||||
handle
|
|
||||||
).await {
|
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
info!("HTTP server stopped gracefully");
|
info!("HTTP server stopped gracefully");
|
||||||
}
|
}
|
||||||
|
|
@ -243,17 +247,19 @@ impl HttpServer {
|
||||||
.route("/api/cleanup-exited", post(cleanup_exited))
|
.route("/api/cleanup-exited", post(cleanup_exited))
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
app_state.auth_config.clone(),
|
app_state.auth_config.clone(),
|
||||||
auth_middleware
|
auth_middleware,
|
||||||
))
|
))
|
||||||
.with_state(app_state);
|
.with_state(app_state);
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(auth_routes)
|
.merge(auth_routes)
|
||||||
.merge(protected_routes)
|
.merge(protected_routes)
|
||||||
.layer(CorsLayer::new()
|
.layer(
|
||||||
.allow_origin(Any)
|
CorsLayer::new()
|
||||||
.allow_methods(Any)
|
.allow_origin(Any)
|
||||||
.allow_headers(Any))
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,12 +269,15 @@ async fn list_sessions(
|
||||||
) -> Result<Json<Vec<SessionInfo>>, StatusCode> {
|
) -> Result<Json<Vec<SessionInfo>>, StatusCode> {
|
||||||
let sessions = state.terminal_manager.list_sessions().await;
|
let sessions = state.terminal_manager.list_sessions().await;
|
||||||
|
|
||||||
let session_infos: Vec<SessionInfo> = sessions.into_iter().map(|s| SessionInfo {
|
let session_infos: Vec<SessionInfo> = sessions
|
||||||
id: s.id,
|
.into_iter()
|
||||||
name: s.name,
|
.map(|s| SessionInfo {
|
||||||
status: "running".to_string(),
|
id: s.id,
|
||||||
created_at: s.created_at,
|
name: s.name,
|
||||||
}).collect();
|
status: "running".to_string(),
|
||||||
|
created_at: s.created_at,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(session_infos))
|
Ok(Json(session_infos))
|
||||||
}
|
}
|
||||||
|
|
@ -277,17 +286,21 @@ async fn create_session(
|
||||||
AxumState(state): AxumState<AppState>,
|
AxumState(state): AxumState<AppState>,
|
||||||
Json(req): Json<CreateSessionRequest>,
|
Json(req): Json<CreateSessionRequest>,
|
||||||
) -> Result<Json<SessionInfo>, StatusCode> {
|
) -> Result<Json<SessionInfo>, StatusCode> {
|
||||||
let session = state.terminal_manager.create_session(
|
let session = state
|
||||||
req.name.unwrap_or_else(|| "Terminal".to_string()),
|
.terminal_manager
|
||||||
req.rows.unwrap_or(24),
|
.create_session(
|
||||||
req.cols.unwrap_or(80),
|
req.name.unwrap_or_else(|| "Terminal".to_string()),
|
||||||
req.cwd,
|
req.rows.unwrap_or(24),
|
||||||
None,
|
req.cols.unwrap_or(80),
|
||||||
None,
|
req.cwd,
|
||||||
).await.map_err(|e| {
|
None,
|
||||||
error!("Failed to create session: {}", e);
|
None,
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
)
|
||||||
})?;
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to create session: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Json(SessionInfo {
|
Ok(Json(SessionInfo {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
|
|
@ -303,14 +316,17 @@ async fn get_session(
|
||||||
) -> Result<Json<SessionInfo>, StatusCode> {
|
) -> Result<Json<SessionInfo>, StatusCode> {
|
||||||
let sessions = state.terminal_manager.list_sessions().await;
|
let sessions = state.terminal_manager.list_sessions().await;
|
||||||
|
|
||||||
sessions.into_iter()
|
sessions
|
||||||
|
.into_iter()
|
||||||
.find(|s| s.id == id)
|
.find(|s| s.id == id)
|
||||||
.map(|s| Json(SessionInfo {
|
.map(|s| {
|
||||||
id: s.id,
|
Json(SessionInfo {
|
||||||
name: s.name,
|
id: s.id,
|
||||||
status: "running".to_string(),
|
name: s.name,
|
||||||
created_at: s.created_at,
|
status: "running".to_string(),
|
||||||
}))
|
created_at: s.created_at,
|
||||||
|
})
|
||||||
|
})
|
||||||
.ok_or(StatusCode::NOT_FOUND)
|
.ok_or(StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,7 +334,10 @@ async fn delete_session(
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
AxumState(state): AxumState<AppState>,
|
AxumState(state): AxumState<AppState>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
state.terminal_manager.close_session(&id).await
|
state
|
||||||
|
.terminal_manager
|
||||||
|
.close_session(&id)
|
||||||
|
.await
|
||||||
.map(|_| StatusCode::NO_CONTENT)
|
.map(|_| StatusCode::NO_CONTENT)
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)
|
.map_err(|_| StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +353,10 @@ async fn resize_session(
|
||||||
AxumState(state): AxumState<AppState>,
|
AxumState(state): AxumState<AppState>,
|
||||||
Json(req): Json<ResizeRequest>,
|
Json(req): Json<ResizeRequest>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
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(|_| StatusCode::NO_CONTENT)
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)
|
.map_err(|_| StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
@ -358,7 +380,9 @@ async fn handle_terminal_websocket(
|
||||||
let _session = match terminal_manager.get_session(&session_id).await {
|
let _session = match terminal_manager.get_session(&session_id).await {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
let _ = sender.send(Message::Text("Session not found".to_string())).await;
|
let _ = sender
|
||||||
|
.send(Message::Text("Session not found".to_string()))
|
||||||
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -368,7 +392,10 @@ async fn handle_terminal_websocket(
|
||||||
let terminal_manager_clone = terminal_manager.clone();
|
let terminal_manager_clone = terminal_manager.clone();
|
||||||
let read_task = tokio::spawn(async move {
|
let read_task = tokio::spawn(async move {
|
||||||
loop {
|
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() => {
|
Ok(data) if !data.is_empty() => {
|
||||||
if sender.send(Message::Binary(data)).await.is_err() {
|
if sender.send(Message::Binary(data)).await.is_err() {
|
||||||
break;
|
break;
|
||||||
|
|
@ -399,15 +426,12 @@ async fn handle_terminal_websocket(
|
||||||
// Handle text messages (e.g., resize commands)
|
// Handle text messages (e.g., resize commands)
|
||||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
if json["type"] == "resize" {
|
if json["type"] == "resize" {
|
||||||
if let (Some(rows), Some(cols)) = (
|
if let (Some(rows), Some(cols)) =
|
||||||
json["rows"].as_u64(),
|
(json["rows"].as_u64(), json["cols"].as_u64())
|
||||||
json["cols"].as_u64()
|
{
|
||||||
) {
|
let _ = terminal_manager
|
||||||
let _ = terminal_manager.resize_session(
|
.resize_session(&session_id, rows as u16, cols as u16)
|
||||||
&session_id,
|
.await;
|
||||||
rows as u16,
|
|
||||||
cols as u16
|
|
||||||
).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +461,10 @@ async fn send_input(
|
||||||
AxumState(state): AxumState<AppState>,
|
AxumState(state): AxumState<AppState>,
|
||||||
Json(req): Json<InputRequest>,
|
Json(req): Json<InputRequest>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
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(|_| StatusCode::NO_CONTENT)
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)
|
.map_err(|_| StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
@ -448,7 +475,8 @@ async fn terminal_stream(
|
||||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
|
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
|
||||||
// Check if session exists
|
// Check if session exists
|
||||||
let sessions = state.terminal_manager.list_sessions().await;
|
let sessions = state.terminal_manager.list_sessions().await;
|
||||||
let session = sessions.into_iter()
|
let session = sessions
|
||||||
|
.into_iter()
|
||||||
.find(|s| s.id == id)
|
.find(|s| s.id == id)
|
||||||
.ok_or(StatusCode::NOT_FOUND)?;
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
|
@ -530,11 +558,10 @@ async fn session_events_stream(
|
||||||
session_monitor.start_monitoring().await;
|
session_monitor.start_monitoring().await;
|
||||||
|
|
||||||
// Create SSE stream from session monitor
|
// Create SSE stream from session monitor
|
||||||
let stream = session_monitor.create_sse_stream()
|
let stream = session_monitor.create_sse_stream().map(|data| {
|
||||||
.map(|data| {
|
data.map(|json| Event::default().data(json))
|
||||||
data.map(|json| Event::default().data(json))
|
.map_err(|_| unreachable!())
|
||||||
.map_err(|_| unreachable!())
|
});
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
||||||
}
|
}
|
||||||
|
|
@ -547,8 +574,7 @@ async fn browse_directory(
|
||||||
|
|
||||||
// Expand tilde to home directory
|
// Expand tilde to home directory
|
||||||
let path = if path_str.starts_with('~') {
|
let path = if path_str.starts_with('~') {
|
||||||
let home = dirs::home_dir()
|
let home = dirs::home_dir().ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
.ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
home.join(path_str.strip_prefix("~/").unwrap_or(""))
|
home.join(path_str.strip_prefix("~/").unwrap_or(""))
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from(&path_str)
|
PathBuf::from(&path_str)
|
||||||
|
|
@ -565,22 +591,24 @@ async fn browse_directory(
|
||||||
|
|
||||||
// Read directory entries
|
// Read directory entries
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
let entries = fs::read_dir(&path)
|
let entries = fs::read_dir(&path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let entry = entry.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
let entry = entry.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
let metadata = entry.metadata()
|
let metadata = entry
|
||||||
|
.metadata()
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
let created = metadata.created()
|
let created = metadata
|
||||||
|
.created()
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||||
datetime.to_rfc3339()
|
datetime.to_rfc3339()
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| String::new());
|
.unwrap_or_else(|_| String::new());
|
||||||
|
|
||||||
let modified = metadata.modified()
|
let modified = metadata
|
||||||
|
.modified()
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
||||||
datetime.to_rfc3339()
|
datetime.to_rfc3339()
|
||||||
|
|
@ -597,12 +625,10 @@ async fn browse_directory(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort directories first, then files, alphabetically
|
// Sort directories first, then files, alphabetically
|
||||||
files.sort_by(|a, b| {
|
files.sort_by(|a, b| match (a.is_dir, b.is_dir) {
|
||||||
match (a.is_dir, b.is_dir) {
|
(true, false) => std::cmp::Ordering::Less,
|
||||||
(true, false) => std::cmp::Ordering::Less,
|
(false, true) => std::cmp::Ordering::Greater,
|
||||||
(false, true) => std::cmp::Ordering::Greater,
|
_ => a.name.cmp(&b.name),
|
||||||
_ => a.name.cmp(&b.name),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Json(DirectoryListing {
|
Ok(Json(DirectoryListing {
|
||||||
|
|
@ -611,21 +637,19 @@ async fn browse_directory(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_directory(
|
async fn create_directory(Json(req): Json<CreateDirRequest>) -> Result<StatusCode, StatusCode> {
|
||||||
Json(req): Json<CreateDirRequest>,
|
|
||||||
) -> Result<StatusCode, StatusCode> {
|
|
||||||
// Validate directory name
|
// Validate directory name
|
||||||
if req.name.is_empty() ||
|
if req.name.is_empty()
|
||||||
req.name.contains('/') ||
|
|| req.name.contains('/')
|
||||||
req.name.contains('\\') ||
|
|| req.name.contains('\\')
|
||||||
req.name.starts_with('.') {
|
|| req.name.starts_with('.')
|
||||||
|
{
|
||||||
return Err(StatusCode::BAD_REQUEST);
|
return Err(StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand path
|
// Expand path
|
||||||
let base_path = if req.path.starts_with('~') {
|
let base_path = if req.path.starts_with('~') {
|
||||||
let home = dirs::home_dir()
|
let home = dirs::home_dir().ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
.ok_or_else(|| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
home.join(req.path.strip_prefix("~/").unwrap_or(""))
|
home.join(req.path.strip_prefix("~/").unwrap_or(""))
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from(&req.path)
|
PathBuf::from(&req.path)
|
||||||
|
|
@ -640,8 +664,7 @@ async fn create_directory(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directory
|
// Create directory
|
||||||
fs::create_dir(&full_path)
|
fs::create_dir(&full_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
|
||||||
Ok(StatusCode::CREATED)
|
Ok(StatusCode::CREATED)
|
||||||
}
|
}
|
||||||
|
|
@ -656,9 +679,19 @@ async fn cleanup_exited(
|
||||||
// Check each session and close if the process has exited
|
// Check each session and close if the process has exited
|
||||||
for session in sessions {
|
for session in sessions {
|
||||||
// Try to write empty data to check if session is alive
|
// 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
|
// 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);
|
cleaned_sessions.push(session.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -679,7 +712,8 @@ async fn get_snapshot(
|
||||||
) -> Result<String, StatusCode> {
|
) -> Result<String, StatusCode> {
|
||||||
// Check if session exists
|
// Check if session exists
|
||||||
let sessions = state.terminal_manager.list_sessions().await;
|
let sessions = state.terminal_manager.list_sessions().await;
|
||||||
let session = sessions.into_iter()
|
let session = sessions
|
||||||
|
.into_iter()
|
||||||
.find(|s| s.id == id)
|
.find(|s| s.id == id)
|
||||||
.ok_or(StatusCode::NOT_FOUND)?;
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{RwLock, mpsc};
|
use tokio::sync::{mpsc, RwLock};
|
||||||
use tokio::time::{interval, Duration};
|
use tokio::time::{interval, Duration};
|
||||||
use chrono::Utc;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Information about a terminal session
|
/// Information about a terminal session
|
||||||
|
|
@ -25,19 +25,10 @@ pub struct SessionInfo {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum SessionEvent {
|
pub enum SessionEvent {
|
||||||
SessionCreated {
|
SessionCreated { session: SessionInfo },
|
||||||
session: SessionInfo,
|
SessionUpdated { session: SessionInfo },
|
||||||
},
|
SessionClosed { id: String },
|
||||||
SessionUpdated {
|
SessionActivity { id: String, timestamp: String },
|
||||||
session: SessionInfo,
|
|
||||||
},
|
|
||||||
SessionClosed {
|
|
||||||
id: String,
|
|
||||||
},
|
|
||||||
SessionActivity {
|
|
||||||
id: String,
|
|
||||||
timestamp: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Session monitoring service
|
/// Session monitoring service
|
||||||
|
|
@ -93,21 +84,24 @@ impl SessionMonitor {
|
||||||
Self::broadcast_event(
|
Self::broadcast_event(
|
||||||
&subscribers,
|
&subscribers,
|
||||||
SessionEvent::SessionCreated {
|
SessionEvent::SessionCreated {
|
||||||
session: session_info.clone()
|
session: session_info.clone(),
|
||||||
}
|
},
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
} else {
|
} else {
|
||||||
// Check if session was updated
|
// Check if session was updated
|
||||||
if let Some(existing) = sessions_map.get(&session.id) {
|
if let Some(existing) = sessions_map.get(&session.id) {
|
||||||
if existing.rows != session_info.rows ||
|
if existing.rows != session_info.rows
|
||||||
existing.cols != session_info.cols {
|
|| existing.cols != session_info.cols
|
||||||
|
{
|
||||||
// Broadcast session updated event
|
// Broadcast session updated event
|
||||||
Self::broadcast_event(
|
Self::broadcast_event(
|
||||||
&subscribers,
|
&subscribers,
|
||||||
SessionEvent::SessionUpdated {
|
SessionEvent::SessionUpdated {
|
||||||
session: session_info.clone()
|
session: session_info.clone(),
|
||||||
}
|
},
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +110,8 @@ impl SessionMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for closed sessions
|
// Check for closed sessions
|
||||||
let closed_sessions: Vec<String> = sessions_map.keys()
|
let closed_sessions: Vec<String> = sessions_map
|
||||||
|
.keys()
|
||||||
.filter(|id| !updated_sessions.contains_key(*id))
|
.filter(|id| !updated_sessions.contains_key(*id))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -126,9 +121,10 @@ impl SessionMonitor {
|
||||||
Self::broadcast_event(
|
Self::broadcast_event(
|
||||||
&subscribers,
|
&subscribers,
|
||||||
SessionEvent::SessionClosed {
|
SessionEvent::SessionClosed {
|
||||||
id: session_id.clone()
|
id: session_id.clone(),
|
||||||
}
|
},
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the sessions map
|
// Update the sessions map
|
||||||
|
|
@ -138,21 +134,27 @@ impl SessionMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to session events
|
/// Subscribe to session events
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn subscribe(&self) -> mpsc::UnboundedReceiver<SessionEvent> {
|
pub async fn subscribe(&self) -> mpsc::UnboundedReceiver<SessionEvent> {
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
let subscriber_id = Uuid::new_v4().to_string();
|
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
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unsubscribe from session events
|
/// Unsubscribe from session events
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn unsubscribe(&self, subscriber_id: &str) {
|
pub async fn unsubscribe(&self, subscriber_id: &str) {
|
||||||
self.event_subscribers.write().await.remove(subscriber_id);
|
self.event_subscribers.write().await.remove(subscriber_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current session count
|
/// Get current session count
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn get_session_count(&self) -> usize {
|
pub async fn get_session_count(&self) -> usize {
|
||||||
self.sessions.read().await.len()
|
self.sessions.read().await.len()
|
||||||
}
|
}
|
||||||
|
|
@ -163,11 +165,13 @@ impl SessionMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a specific session
|
/// Get a specific session
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn get_session(&self, id: &str) -> Option<SessionInfo> {
|
pub async fn get_session(&self, id: &str) -> Option<SessionInfo> {
|
||||||
self.sessions.read().await.get(id).cloned()
|
self.sessions.read().await.get(id).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Notify activity for a session
|
/// Notify activity for a session
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn notify_activity(&self, session_id: &str) {
|
pub async fn notify_activity(&self, session_id: &str) {
|
||||||
if let Some(session) = self.sessions.write().await.get_mut(session_id) {
|
if let Some(session) = self.sessions.write().await.get_mut(session_id) {
|
||||||
session.last_activity = Utc::now().to_rfc3339();
|
session.last_activity = Utc::now().to_rfc3339();
|
||||||
|
|
@ -178,8 +182,9 @@ impl SessionMonitor {
|
||||||
SessionEvent::SessionActivity {
|
SessionEvent::SessionActivity {
|
||||||
id: session_id.to_string(),
|
id: session_id.to_string(),
|
||||||
timestamp: session.last_activity.clone(),
|
timestamp: session.last_activity.clone(),
|
||||||
}
|
},
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +213,10 @@ impl SessionMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an SSE stream for session events
|
/// Create an SSE stream for session events
|
||||||
pub fn create_sse_stream(self: Arc<Self>) -> impl futures::Stream<Item = Result<String, std::convert::Infallible>> + Send + 'static {
|
pub fn create_sse_stream(
|
||||||
|
self: Arc<Self>,
|
||||||
|
) -> impl futures::Stream<Item = Result<String, std::convert::Infallible>> + Send + 'static
|
||||||
|
{
|
||||||
async_stream::stream! {
|
async_stream::stream! {
|
||||||
// Subscribe to events
|
// Subscribe to events
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
|
@ -260,7 +268,7 @@ impl SessionMonitor {
|
||||||
total_sessions: sessions.len(),
|
total_sessions: sessions.len(),
|
||||||
active_sessions,
|
active_sessions,
|
||||||
total_clients,
|
total_clients,
|
||||||
uptime_seconds: 0, // TODO: Track uptime
|
uptime_seconds: 0, // TODO: Track uptime
|
||||||
sessions_created_today: 0, // TODO: Track daily stats
|
sessions_created_today: 0, // TODO: Track daily stats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use directories::ProjectDirs;
|
|
||||||
use tauri::{Manager, State};
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{Manager, State};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct GeneralSettings {
|
pub struct GeneralSettings {
|
||||||
|
|
@ -336,8 +336,7 @@ impl Settings {
|
||||||
let contents = std::fs::read_to_string(&config_path)
|
let contents = std::fs::read_to_string(&config_path)
|
||||||
.map_err(|e| format!("Failed to read settings: {}", e))?;
|
.map_err(|e| format!("Failed to read settings: {}", e))?;
|
||||||
|
|
||||||
toml::from_str(&contents)
|
toml::from_str(&contents).map_err(|e| format!("Failed to parse settings: {}", e))
|
||||||
.map_err(|e| format!("Failed to parse settings: {}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), String> {
|
pub fn save(&self) -> Result<(), String> {
|
||||||
|
|
@ -367,9 +366,7 @@ impl Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_settings(
|
pub async fn get_settings(_state: State<'_, AppState>) -> Result<Settings, String> {
|
||||||
_state: State<'_, AppState>,
|
|
||||||
) -> Result<Settings, String> {
|
|
||||||
Settings::load()
|
Settings::load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,7 +389,10 @@ pub async fn save_settings(
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
// Check if any windows are visible
|
// 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 {
|
if !has_visible_windows && !settings.general.show_dock_icon {
|
||||||
// Hide dock icon if no windows are visible and setting is disabled
|
// Hide dock icon if no windows are visible and setting is disabled
|
||||||
|
|
|
||||||
|
|
@ -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::api_testing::APITestingManager;
|
||||||
use crate::auth_cache::AuthCacheManager;
|
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_integrations::TerminalIntegrationsManager;
|
||||||
use crate::terminal_spawn_service::TerminalSpawnService;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
|
@ -75,7 +75,7 @@ impl AppState {
|
||||||
|
|
||||||
let terminal_integrations_manager = Arc::new(terminal_integrations_manager);
|
let terminal_integrations_manager = Arc::new(terminal_integrations_manager);
|
||||||
let terminal_spawn_service = Arc::new(TerminalSpawnService::new(
|
let terminal_spawn_service = Arc::new(TerminalSpawnService::new(
|
||||||
terminal_integrations_manager.clone()
|
terminal_integrations_manager.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -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 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)]
|
#[derive(Clone)]
|
||||||
pub struct TerminalManager {
|
pub struct TerminalManager {
|
||||||
|
|
@ -24,9 +24,12 @@ pub struct TerminalSession {
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub cwd: String,
|
pub cwd: String,
|
||||||
pty_pair: PtyPair,
|
pty_pair: PtyPair,
|
||||||
|
#[allow(dead_code)]
|
||||||
child: Box<dyn Child + Send + Sync>,
|
child: Box<dyn Child + Send + Sync>,
|
||||||
writer: Box<dyn Write + Send>,
|
writer: Box<dyn Write + Send>,
|
||||||
|
#[allow(dead_code)]
|
||||||
reader_thread: Option<std::thread::JoinHandle<()>>,
|
reader_thread: Option<std::thread::JoinHandle<()>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
output_tx: mpsc::UnboundedSender<Bytes>,
|
output_tx: mpsc::UnboundedSender<Bytes>,
|
||||||
pub output_rx: Arc<Mutex<mpsc::UnboundedReceiver<Bytes>>>,
|
pub output_rx: Arc<Mutex<mpsc::UnboundedReceiver<Bytes>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +162,12 @@ impl TerminalManager {
|
||||||
rows,
|
rows,
|
||||||
cols,
|
cols,
|
||||||
created_at: Utc::now().to_rfc3339(),
|
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,
|
pty_pair,
|
||||||
child,
|
child,
|
||||||
writer,
|
writer,
|
||||||
|
|
@ -169,7 +177,10 @@ impl TerminalManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store session
|
// 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);
|
info!("Created terminal session: {} ({})", name, id);
|
||||||
|
|
||||||
|
|
@ -240,7 +251,8 @@ impl TerminalManager {
|
||||||
if let Some(session_arc) = self.get_session(id).await {
|
if let Some(session_arc) = self.get_session(id).await {
|
||||||
let mut session = session_arc.write().await;
|
let mut session = session_arc.write().await;
|
||||||
|
|
||||||
session.pty_pair
|
session
|
||||||
|
.pty_pair
|
||||||
.master
|
.master
|
||||||
.resize(PtySize {
|
.resize(PtySize {
|
||||||
rows,
|
rows,
|
||||||
|
|
@ -277,11 +289,13 @@ impl TerminalManager {
|
||||||
let _ = cast_manager.add_input(id, data).await;
|
let _ = cast_manager.add_input(id, data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
session.writer
|
session
|
||||||
|
.writer
|
||||||
.write_all(data)
|
.write_all(data)
|
||||||
.map_err(|e| format!("Failed to write to PTY: {}", e))?;
|
.map_err(|e| format!("Failed to write to PTY: {}", e))?;
|
||||||
|
|
||||||
session.writer
|
session
|
||||||
|
.writer
|
||||||
.flush()
|
.flush()
|
||||||
.map_err(|e| format!("Failed to flush PTY: {}", e))?;
|
.map_err(|e| format!("Failed to flush PTY: {}", e))?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
// Check for Terminal.app
|
// Check for Terminal.app
|
||||||
if let Ok(_) = Command::new("open")
|
if let Ok(_) = Command::new("open").args(&["-Ra", "Terminal.app"]).output() {
|
||||||
.args(&["-Ra", "Terminal.app"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "Terminal".to_string(),
|
name: "Terminal".to_string(),
|
||||||
path: "/System/Applications/Utilities/Terminal.app".to_string(),
|
path: "/System/Applications/Utilities/Terminal.app".to_string(),
|
||||||
|
|
@ -33,10 +30,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for iTerm2
|
// Check for iTerm2
|
||||||
if let Ok(_) = Command::new("open")
|
if let Ok(_) = Command::new("open").args(&["-Ra", "iTerm.app"]).output() {
|
||||||
.args(&["-Ra", "iTerm.app"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "iTerm2".to_string(),
|
name: "iTerm2".to_string(),
|
||||||
path: "/Applications/iTerm.app".to_string(),
|
path: "/Applications/iTerm.app".to_string(),
|
||||||
|
|
@ -45,10 +39,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Warp
|
// Check for Warp
|
||||||
if let Ok(output) = Command::new("which")
|
if let Ok(output) = Command::new("which").arg("warp").output() {
|
||||||
.arg("warp")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "Warp".to_string(),
|
name: "Warp".to_string(),
|
||||||
|
|
@ -59,10 +50,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Hyper
|
// Check for Hyper
|
||||||
if let Ok(_) = Command::new("open")
|
if let Ok(_) = Command::new("open").args(&["-Ra", "Hyper.app"]).output() {
|
||||||
.args(&["-Ra", "Hyper.app"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "Hyper".to_string(),
|
name: "Hyper".to_string(),
|
||||||
path: "/Applications/Hyper.app".to_string(),
|
path: "/Applications/Hyper.app".to_string(),
|
||||||
|
|
@ -71,10 +59,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Alacritty
|
// Check for Alacritty
|
||||||
if let Ok(output) = Command::new("which")
|
if let Ok(output) = Command::new("which").arg("alacritty").output() {
|
||||||
.arg("alacritty")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "Alacritty".to_string(),
|
name: "Alacritty".to_string(),
|
||||||
|
|
@ -114,10 +99,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
// Check for Windows Terminal
|
// Check for Windows Terminal
|
||||||
if let Ok(output) = Command::new("where")
|
if let Ok(output) = Command::new("where").arg("wt.exe").output() {
|
||||||
.arg("wt.exe")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
|
|
@ -134,10 +116,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for PowerShell
|
// Check for PowerShell
|
||||||
if let Ok(output) = Command::new("where")
|
if let Ok(output) = Command::new("where").arg("powershell.exe").output() {
|
||||||
.arg("powershell.exe")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "PowerShell".to_string(),
|
name: "PowerShell".to_string(),
|
||||||
|
|
@ -148,10 +127,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Command Prompt
|
// Check for Command Prompt
|
||||||
if let Ok(output) = Command::new("where")
|
if let Ok(output) = Command::new("where").arg("cmd.exe").output() {
|
||||||
.arg("cmd.exe")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: "Command Prompt".to_string(),
|
name: "Command Prompt".to_string(),
|
||||||
|
|
@ -187,10 +163,7 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (cmd, name) in terminals {
|
for (cmd, name) in terminals {
|
||||||
if let Ok(output) = Command::new("which")
|
if let Ok(output) = Command::new("which").arg(cmd).output() {
|
||||||
.arg(cmd)
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
available_terminals.push(TerminalInfo {
|
available_terminals.push(TerminalInfo {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
|
@ -205,17 +178,20 @@ pub fn detect_terminals() -> Result<DetectedTerminals, String> {
|
||||||
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
|
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||||
match desktop.to_lowercase().as_str() {
|
match desktop.to_lowercase().as_str() {
|
||||||
"gnome" | "ubuntu" => {
|
"gnome" | "ubuntu" => {
|
||||||
default_terminal = available_terminals.iter()
|
default_terminal = available_terminals
|
||||||
|
.iter()
|
||||||
.find(|t| t.name == "GNOME Terminal")
|
.find(|t| t.name == "GNOME Terminal")
|
||||||
.cloned();
|
.cloned();
|
||||||
}
|
}
|
||||||
"kde" => {
|
"kde" => {
|
||||||
default_terminal = available_terminals.iter()
|
default_terminal = available_terminals
|
||||||
|
.iter()
|
||||||
.find(|t| t.name == "Konsole")
|
.find(|t| t.name == "Konsole")
|
||||||
.cloned();
|
.cloned();
|
||||||
}
|
}
|
||||||
"xfce" => {
|
"xfce" => {
|
||||||
default_terminal = available_terminals.iter()
|
default_terminal = available_terminals
|
||||||
|
.iter()
|
||||||
.find(|t| t.name == "XFCE Terminal")
|
.find(|t| t.name == "XFCE Terminal")
|
||||||
.cloned();
|
.cloned();
|
||||||
}
|
}
|
||||||
|
|
@ -260,10 +236,7 @@ pub async fn get_default_shell() -> Result<String, String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
// On Windows, default to PowerShell
|
// On Windows, default to PowerShell
|
||||||
if let Ok(output) = Command::new("where")
|
if let Ok(output) = Command::new("where").arg("powershell.exe").output() {
|
||||||
.arg("powershell.exe")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
|
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Terminal emulator type
|
/// Terminal emulator type
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub enum TerminalEmulator {
|
pub enum TerminalEmulator {
|
||||||
SystemDefault,
|
SystemDefault,
|
||||||
Terminal, // macOS Terminal.app
|
Terminal, // macOS Terminal.app
|
||||||
ITerm2, // iTerm2
|
ITerm2, // iTerm2
|
||||||
Hyper, // Hyper
|
Hyper, // Hyper
|
||||||
Alacritty, // Alacritty
|
Alacritty, // Alacritty
|
||||||
Kitty, // Kitty
|
Kitty, // Kitty
|
||||||
WezTerm, // WezTerm
|
WezTerm, // WezTerm
|
||||||
Ghostty, // Ghostty
|
Ghostty, // Ghostty
|
||||||
Warp, // Warp
|
Warp, // Warp
|
||||||
WindowsTerminal, // Windows Terminal
|
WindowsTerminal, // Windows Terminal
|
||||||
ConEmu, // ConEmu
|
ConEmu, // ConEmu
|
||||||
Cmder, // Cmder
|
Cmder, // Cmder
|
||||||
Gnome, // GNOME Terminal
|
Gnome, // GNOME Terminal
|
||||||
Konsole, // KDE Konsole
|
Konsole, // KDE Konsole
|
||||||
Xterm, // XTerm
|
Xterm, // XTerm
|
||||||
Custom, // Custom terminal
|
Custom, // Custom terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalEmulator {
|
impl TerminalEmulator {
|
||||||
|
|
@ -151,7 +151,10 @@ impl TerminalIntegrationsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,123 +163,148 @@ impl TerminalIntegrationsManager {
|
||||||
let mut configs = HashMap::new();
|
let mut configs = HashMap::new();
|
||||||
|
|
||||||
// WezTerm configuration
|
// WezTerm configuration
|
||||||
configs.insert(TerminalEmulator::WezTerm, TerminalConfig {
|
configs.insert(
|
||||||
emulator: TerminalEmulator::WezTerm,
|
TerminalEmulator::WezTerm,
|
||||||
name: "WezTerm".to_string(),
|
TerminalConfig {
|
||||||
executable_path: PathBuf::from("/Applications/WezTerm.app/Contents/MacOS/wezterm"),
|
emulator: TerminalEmulator::WezTerm,
|
||||||
args_template: vec![
|
name: "WezTerm".to_string(),
|
||||||
"start".to_string(),
|
executable_path: PathBuf::from("/Applications/WezTerm.app/Contents/MacOS/wezterm"),
|
||||||
"--cwd".to_string(),
|
args_template: vec![
|
||||||
"{working_directory}".to_string(),
|
"start".to_string(),
|
||||||
"--".to_string(),
|
"--cwd".to_string(),
|
||||||
"{command}".to_string(),
|
"{working_directory}".to_string(),
|
||||||
"{args}".to_string(),
|
"--".to_string(),
|
||||||
],
|
"{command}".to_string(),
|
||||||
env_vars: HashMap::new(),
|
"{args}".to_string(),
|
||||||
features: TerminalFeatures {
|
],
|
||||||
supports_tabs: true,
|
env_vars: HashMap::new(),
|
||||||
supports_splits: true,
|
features: TerminalFeatures {
|
||||||
supports_profiles: true,
|
supports_tabs: true,
|
||||||
supports_themes: true,
|
supports_splits: true,
|
||||||
supports_scripting: true,
|
supports_profiles: true,
|
||||||
supports_url_scheme: false,
|
supports_themes: true,
|
||||||
supports_remote_control: 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
|
// Ghostty configuration
|
||||||
configs.insert(TerminalEmulator::Ghostty, TerminalConfig {
|
configs.insert(
|
||||||
emulator: TerminalEmulator::Ghostty,
|
TerminalEmulator::Ghostty,
|
||||||
name: "Ghostty".to_string(),
|
TerminalConfig {
|
||||||
executable_path: PathBuf::from("/Applications/Ghostty.app/Contents/MacOS/ghostty"),
|
emulator: TerminalEmulator::Ghostty,
|
||||||
args_template: vec![
|
name: "Ghostty".to_string(),
|
||||||
"--working-directory".to_string(),
|
executable_path: PathBuf::from("/Applications/Ghostty.app/Contents/MacOS/ghostty"),
|
||||||
"{working_directory}".to_string(),
|
args_template: vec![
|
||||||
"--command".to_string(),
|
"--working-directory".to_string(),
|
||||||
"{command}".to_string(),
|
"{working_directory}".to_string(),
|
||||||
"{args}".to_string(),
|
"--command".to_string(),
|
||||||
],
|
"{command}".to_string(),
|
||||||
env_vars: HashMap::new(),
|
"{args}".to_string(),
|
||||||
features: TerminalFeatures {
|
],
|
||||||
supports_tabs: true,
|
env_vars: HashMap::new(),
|
||||||
supports_splits: true,
|
features: TerminalFeatures {
|
||||||
supports_profiles: true,
|
supports_tabs: true,
|
||||||
supports_themes: true,
|
supports_splits: true,
|
||||||
supports_scripting: false,
|
supports_profiles: true,
|
||||||
supports_url_scheme: false,
|
supports_themes: true,
|
||||||
supports_remote_control: false,
|
supports_scripting: false,
|
||||||
|
supports_url_scheme: false,
|
||||||
|
supports_remote_control: false,
|
||||||
|
},
|
||||||
|
platform: vec!["macos".to_string()],
|
||||||
},
|
},
|
||||||
platform: vec!["macos".to_string()],
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// iTerm2 configuration
|
// iTerm2 configuration
|
||||||
configs.insert(TerminalEmulator::ITerm2, TerminalConfig {
|
configs.insert(
|
||||||
emulator: TerminalEmulator::ITerm2,
|
TerminalEmulator::ITerm2,
|
||||||
name: "iTerm2".to_string(),
|
TerminalConfig {
|
||||||
executable_path: PathBuf::from("/Applications/iTerm.app/Contents/MacOS/iTerm2"),
|
emulator: TerminalEmulator::ITerm2,
|
||||||
args_template: vec![],
|
name: "iTerm2".to_string(),
|
||||||
env_vars: HashMap::new(),
|
executable_path: PathBuf::from("/Applications/iTerm.app/Contents/MacOS/iTerm2"),
|
||||||
features: TerminalFeatures {
|
args_template: vec![],
|
||||||
supports_tabs: true,
|
env_vars: HashMap::new(),
|
||||||
supports_splits: true,
|
features: TerminalFeatures {
|
||||||
supports_profiles: true,
|
supports_tabs: true,
|
||||||
supports_themes: true,
|
supports_splits: true,
|
||||||
supports_scripting: true,
|
supports_profiles: true,
|
||||||
supports_url_scheme: true,
|
supports_themes: true,
|
||||||
supports_remote_control: true,
|
supports_scripting: true,
|
||||||
|
supports_url_scheme: true,
|
||||||
|
supports_remote_control: true,
|
||||||
|
},
|
||||||
|
platform: vec!["macos".to_string()],
|
||||||
},
|
},
|
||||||
platform: vec!["macos".to_string()],
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// Alacritty configuration
|
// Alacritty configuration
|
||||||
configs.insert(TerminalEmulator::Alacritty, TerminalConfig {
|
configs.insert(
|
||||||
emulator: TerminalEmulator::Alacritty,
|
TerminalEmulator::Alacritty,
|
||||||
name: "Alacritty".to_string(),
|
TerminalConfig {
|
||||||
executable_path: PathBuf::from("/Applications/Alacritty.app/Contents/MacOS/alacritty"),
|
emulator: TerminalEmulator::Alacritty,
|
||||||
args_template: vec![
|
name: "Alacritty".to_string(),
|
||||||
"--working-directory".to_string(),
|
executable_path: PathBuf::from(
|
||||||
"{working_directory}".to_string(),
|
"/Applications/Alacritty.app/Contents/MacOS/alacritty",
|
||||||
"-e".to_string(),
|
),
|
||||||
"{command}".to_string(),
|
args_template: vec![
|
||||||
"{args}".to_string(),
|
"--working-directory".to_string(),
|
||||||
],
|
"{working_directory}".to_string(),
|
||||||
env_vars: HashMap::new(),
|
"-e".to_string(),
|
||||||
features: TerminalFeatures {
|
"{command}".to_string(),
|
||||||
supports_tabs: false,
|
"{args}".to_string(),
|
||||||
supports_splits: false,
|
],
|
||||||
supports_profiles: true,
|
env_vars: HashMap::new(),
|
||||||
supports_themes: true,
|
features: TerminalFeatures {
|
||||||
supports_scripting: false,
|
supports_tabs: false,
|
||||||
supports_url_scheme: false,
|
supports_splits: false,
|
||||||
supports_remote_control: 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
|
// Kitty configuration
|
||||||
configs.insert(TerminalEmulator::Kitty, TerminalConfig {
|
configs.insert(
|
||||||
emulator: TerminalEmulator::Kitty,
|
TerminalEmulator::Kitty,
|
||||||
name: "Kitty".to_string(),
|
TerminalConfig {
|
||||||
executable_path: PathBuf::from("/Applications/kitty.app/Contents/MacOS/kitty"),
|
emulator: TerminalEmulator::Kitty,
|
||||||
args_template: vec![
|
name: "Kitty".to_string(),
|
||||||
"--directory".to_string(),
|
executable_path: PathBuf::from("/Applications/kitty.app/Contents/MacOS/kitty"),
|
||||||
"{working_directory}".to_string(),
|
args_template: vec![
|
||||||
"{command}".to_string(),
|
"--directory".to_string(),
|
||||||
"{args}".to_string(),
|
"{working_directory}".to_string(),
|
||||||
],
|
"{command}".to_string(),
|
||||||
env_vars: HashMap::new(),
|
"{args}".to_string(),
|
||||||
features: TerminalFeatures {
|
],
|
||||||
supports_tabs: true,
|
env_vars: HashMap::new(),
|
||||||
supports_splits: true,
|
features: TerminalFeatures {
|
||||||
supports_profiles: true,
|
supports_tabs: true,
|
||||||
supports_themes: true,
|
supports_splits: true,
|
||||||
supports_scripting: true,
|
supports_profiles: true,
|
||||||
supports_url_scheme: false,
|
supports_themes: true,
|
||||||
supports_remote_control: 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
|
configs
|
||||||
}
|
}
|
||||||
|
|
@ -285,12 +313,15 @@ impl TerminalIntegrationsManager {
|
||||||
fn initialize_url_schemes() -> HashMap<TerminalEmulator, TerminalURLScheme> {
|
fn initialize_url_schemes() -> HashMap<TerminalEmulator, TerminalURLScheme> {
|
||||||
let mut schemes = HashMap::new();
|
let mut schemes = HashMap::new();
|
||||||
|
|
||||||
schemes.insert(TerminalEmulator::ITerm2, TerminalURLScheme {
|
schemes.insert(
|
||||||
scheme: "iterm2".to_string(),
|
TerminalEmulator::ITerm2,
|
||||||
supports_ssh: true,
|
TerminalURLScheme {
|
||||||
supports_local: true,
|
scheme: "iterm2".to_string(),
|
||||||
template: "iterm2://ssh/{user}@{host}:{port}".to_string(),
|
supports_ssh: true,
|
||||||
});
|
supports_local: true,
|
||||||
|
template: "iterm2://ssh/{user}@{host}:{port}".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
schemes
|
schemes
|
||||||
}
|
}
|
||||||
|
|
@ -304,7 +335,10 @@ impl TerminalIntegrationsManager {
|
||||||
let info = self.check_terminal_installation(emulator, config).await;
|
let info = self.check_terminal_installation(emulator, config).await;
|
||||||
if info.installed {
|
if info.installed {
|
||||||
detected.push(info.clone());
|
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
|
/// 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 installed = config.executable_path.exists();
|
||||||
let version = if installed {
|
let version = if installed {
|
||||||
self.get_terminal_version(emulator, &config.executable_path).await
|
self.get_terminal_version(emulator, &config.executable_path)
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -328,31 +367,39 @@ impl TerminalIntegrationsManager {
|
||||||
emulator: *emulator,
|
emulator: *emulator,
|
||||||
installed,
|
installed,
|
||||||
version,
|
version,
|
||||||
path: if installed { Some(config.executable_path.clone()) } else { None },
|
path: if installed {
|
||||||
|
Some(config.executable_path.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
is_default: false,
|
is_default: false,
|
||||||
config: if installed { Some(config.clone()) } else { None },
|
config: if installed {
|
||||||
|
Some(config.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get terminal version
|
/// Get terminal version
|
||||||
async fn get_terminal_version(&self, emulator: &TerminalEmulator, path: &PathBuf) -> Option<String> {
|
async fn get_terminal_version(
|
||||||
|
&self,
|
||||||
|
emulator: &TerminalEmulator,
|
||||||
|
path: &PathBuf,
|
||||||
|
) -> Option<String> {
|
||||||
match emulator {
|
match emulator {
|
||||||
TerminalEmulator::WezTerm => {
|
TerminalEmulator::WezTerm => Command::new(path)
|
||||||
Command::new(path)
|
.arg("--version")
|
||||||
.arg("--version")
|
.output()
|
||||||
.output()
|
.ok()
|
||||||
.ok()
|
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
.map(|v| v.trim().to_string()),
|
||||||
.map(|v| v.trim().to_string())
|
TerminalEmulator::Alacritty => Command::new(path)
|
||||||
}
|
.arg("--version")
|
||||||
TerminalEmulator::Alacritty => {
|
.output()
|
||||||
Command::new(path)
|
.ok()
|
||||||
.arg("--version")
|
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||||
.output()
|
.map(|v| v.trim().to_string()),
|
||||||
.ok()
|
|
||||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
|
||||||
.map(|v| v.trim().to_string())
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -413,10 +460,12 @@ impl TerminalIntegrationsManager {
|
||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
if let Some(notification_manager) = &self.notification_manager {
|
if let Some(notification_manager) = &self.notification_manager {
|
||||||
let _ = notification_manager.notify_success(
|
let _ = notification_manager
|
||||||
"Default Terminal Changed",
|
.notify_success(
|
||||||
&format!("Default terminal set to {}", emulator.display_name())
|
"Default Terminal Changed",
|
||||||
).await;
|
&format!("Default terminal set to {}", emulator.display_name()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -461,7 +510,8 @@ impl TerminalIntegrationsManager {
|
||||||
options: TerminalLaunchOptions,
|
options: TerminalLaunchOptions,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let configs = self.configs.read().await;
|
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())?;
|
.ok_or_else(|| "Terminal configuration not found".to_string())?;
|
||||||
|
|
||||||
let mut command = Command::new(&config.executable_path);
|
let mut command = Command::new(&config.executable_path);
|
||||||
|
|
@ -488,7 +538,8 @@ impl TerminalIntegrationsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch terminal
|
// Launch terminal
|
||||||
command.spawn()
|
command
|
||||||
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to launch terminal: {}", e))?;
|
.map_err(|e| format!("Failed to launch terminal: {}", e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -503,11 +554,16 @@ impl TerminalIntegrationsManager {
|
||||||
script.push_str(" activate\n");
|
script.push_str(" activate\n");
|
||||||
|
|
||||||
if options.tab {
|
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 {
|
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 {
|
if let Some(command) = options.command {
|
||||||
|
|
@ -516,7 +572,10 @@ impl TerminalIntegrationsManager {
|
||||||
} else {
|
} else {
|
||||||
format!("{} {}", command, options.args.join(" "))
|
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");
|
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))?;
|
.map_err(|e| format!("Failed to launch Windows Terminal: {}", e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -600,7 +660,8 @@ impl TerminalIntegrationsManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.spawn()
|
return command
|
||||||
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to launch terminal: {}", e))
|
.map_err(|e| format!("Failed to launch terminal: {}", e))
|
||||||
.map(|_| ());
|
.map(|_| ());
|
||||||
}
|
}
|
||||||
|
|
@ -620,7 +681,8 @@ impl TerminalIntegrationsManager {
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let schemes = self.url_schemes.read().await;
|
let schemes = self.url_schemes.read().await;
|
||||||
schemes.get(&emulator).map(|scheme| {
|
schemes.get(&emulator).map(|scheme| {
|
||||||
scheme.template
|
scheme
|
||||||
|
.template
|
||||||
.replace("{user}", user)
|
.replace("{user}", user)
|
||||||
.replace("{host}", host)
|
.replace("{host}", host)
|
||||||
.replace("{port}", &port.to_string())
|
.replace("{port}", &port.to_string())
|
||||||
|
|
@ -639,11 +701,20 @@ impl TerminalIntegrationsManager {
|
||||||
|
|
||||||
/// List detected terminals
|
/// List detected terminals
|
||||||
pub async fn list_detected_terminals(&self) -> Vec<TerminalIntegrationInfo> {
|
pub async fn list_detected_terminals(&self) -> Vec<TerminalIntegrationInfo> {
|
||||||
self.detected_terminals.read().await.values().cloned().collect()
|
self.detected_terminals
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// 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();
|
let mut result = template.to_string();
|
||||||
|
|
||||||
if let Some(cwd) = &options.working_directory {
|
if let Some(cwd) = &options.working_directory {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
/// Request to spawn a terminal
|
/// Request to spawn a terminal
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -23,12 +23,15 @@ pub struct TerminalSpawnResponse {
|
||||||
/// Terminal Spawn Service - manages background terminal spawning
|
/// Terminal Spawn Service - manages background terminal spawning
|
||||||
pub struct TerminalSpawnService {
|
pub struct TerminalSpawnService {
|
||||||
request_tx: mpsc::Sender<TerminalSpawnRequest>,
|
request_tx: mpsc::Sender<TerminalSpawnRequest>,
|
||||||
|
#[allow(dead_code)]
|
||||||
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalSpawnService {
|
impl TerminalSpawnService {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
terminal_integrations_manager: Arc<
|
||||||
|
crate::terminal_integrations::TerminalIntegrationsManager,
|
||||||
|
>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (tx, mut rx) = mpsc::channel::<TerminalSpawnRequest>(100);
|
let (tx, mut rx) = mpsc::channel::<TerminalSpawnRequest>(100);
|
||||||
|
|
||||||
|
|
@ -52,14 +55,18 @@ impl TerminalSpawnService {
|
||||||
|
|
||||||
/// Queue a terminal spawn request
|
/// Queue a terminal spawn request
|
||||||
pub async fn spawn_terminal(&self, request: TerminalSpawnRequest) -> Result<(), String> {
|
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))
|
.map_err(|e| format!("Failed to queue terminal spawn: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a spawn request
|
/// Handle a spawn request
|
||||||
async fn handle_spawn_request(
|
async fn handle_spawn_request(
|
||||||
request: TerminalSpawnRequest,
|
request: TerminalSpawnRequest,
|
||||||
terminal_integrations_manager: Arc<crate::terminal_integrations::TerminalIntegrationsManager>,
|
terminal_integrations_manager: Arc<
|
||||||
|
crate::terminal_integrations::TerminalIntegrationsManager,
|
||||||
|
>,
|
||||||
) -> Result<TerminalSpawnResponse, String> {
|
) -> Result<TerminalSpawnResponse, String> {
|
||||||
// Determine which terminal to use
|
// Determine which terminal to use
|
||||||
let terminal_type = if let Some(terminal) = &request.terminal_type {
|
let terminal_type = if let Some(terminal) = &request.terminal_type {
|
||||||
|
|
@ -82,7 +89,9 @@ impl TerminalSpawnService {
|
||||||
// Build launch options
|
// Build launch options
|
||||||
let mut launch_options = crate::terminal_integrations::TerminalLaunchOptions {
|
let mut launch_options = crate::terminal_integrations::TerminalLaunchOptions {
|
||||||
command: request.command,
|
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![],
|
args: vec![],
|
||||||
env_vars: request.environment.unwrap_or_default(),
|
env_vars: request.environment.unwrap_or_default(),
|
||||||
title: Some(format!("VibeTunnel Session {}", request.session_id)),
|
title: Some(format!("VibeTunnel Session {}", request.session_id)),
|
||||||
|
|
@ -96,11 +105,17 @@ impl TerminalSpawnService {
|
||||||
if launch_options.command.is_none() {
|
if launch_options.command.is_none() {
|
||||||
// Get server status to build the correct URL
|
// Get server status to build the correct URL
|
||||||
let port = 4020; // Default port, should get from settings
|
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
|
// 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 {
|
Ok(_) => Ok(TerminalSpawnResponse {
|
||||||
success: true,
|
success: true,
|
||||||
error: None,
|
error: None,
|
||||||
|
|
@ -158,7 +173,9 @@ pub async fn spawn_terminal_for_session(
|
||||||
state: tauri::State<'_, crate::state::AppState>,
|
state: tauri::State<'_, crate::state::AppState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let spawn_service = &state.terminal_spawn_service;
|
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]
|
#[tauri::command]
|
||||||
|
|
@ -169,7 +186,9 @@ pub async fn spawn_terminal_with_command(
|
||||||
state: tauri::State<'_, crate::state::AppState>,
|
state: tauri::State<'_, crate::state::AppState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let spawn_service = &state.terminal_spawn_service;
|
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]
|
#[tauri::command]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder, Menu};
|
|
||||||
|
|
||||||
pub struct TrayMenuManager;
|
pub struct TrayMenuManager;
|
||||||
|
|
||||||
|
|
@ -27,9 +27,7 @@ impl TrayMenuManager {
|
||||||
.id("show_tutorial")
|
.id("show_tutorial")
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
let website = MenuItemBuilder::new("Website")
|
let website = MenuItemBuilder::new("Website").id("website").build(app)?;
|
||||||
.id("website")
|
|
||||||
.build(app)?;
|
|
||||||
|
|
||||||
let report_issue = MenuItemBuilder::new("Report Issue")
|
let report_issue = MenuItemBuilder::new("Report Issue")
|
||||||
.id("report_issue")
|
.id("report_issue")
|
||||||
|
|
@ -70,9 +68,7 @@ impl TrayMenuManager {
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
// Quit
|
// Quit
|
||||||
let quit = MenuItemBuilder::new("Quit")
|
let quit = MenuItemBuilder::new("Quit").id("quit").build(app)?;
|
||||||
.id("quit")
|
|
||||||
.build(app)?;
|
|
||||||
|
|
||||||
// Build the complete menu - matching Mac app exactly
|
// Build the complete menu - matching Mac app exactly
|
||||||
let menu = MenuBuilder::new(app)
|
let menu = MenuBuilder::new(app)
|
||||||
|
|
|
||||||
|
|
@ -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 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
|
/// Represents a forwarded TTY session
|
||||||
pub struct ForwardedSession {
|
pub struct ForwardedSession {
|
||||||
|
|
@ -48,7 +48,8 @@ impl TTYForwardManager {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to bind to port {}: {}", local_port, e))?;
|
.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))?
|
.map_err(|e| format!("Failed to get local address: {}", e))?
|
||||||
.port();
|
.port();
|
||||||
|
|
||||||
|
|
@ -91,7 +92,8 @@ impl TTYForwardManager {
|
||||||
remote_port,
|
remote_port,
|
||||||
shell,
|
shell,
|
||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("Started TTY forward on port {} (ID: {})", actual_port, id);
|
info!("Started TTY forward on port {} (ID: {})", actual_port, id);
|
||||||
|
|
@ -305,7 +307,9 @@ impl TTYForwardManager {
|
||||||
|
|
||||||
/// List all active forwarding sessions
|
/// List all active forwarding sessions
|
||||||
pub async fn list_forwards(&self) -> Vec<ForwardedSession> {
|
pub async fn list_forwards(&self) -> Vec<ForwardedSession> {
|
||||||
self.sessions.read().await
|
self.sessions
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.values()
|
.values()
|
||||||
.map(|s| ForwardedSession {
|
.map(|s| ForwardedSession {
|
||||||
id: s.id.clone(),
|
id: s.id.clone(),
|
||||||
|
|
@ -320,22 +324,23 @@ impl TTYForwardManager {
|
||||||
|
|
||||||
/// Get a specific forwarding session
|
/// Get a specific forwarding session
|
||||||
pub async fn get_forward(&self, id: &str) -> Option<ForwardedSession> {
|
pub async fn get_forward(&self, id: &str) -> Option<ForwardedSession> {
|
||||||
self.sessions.read().await.get(id).map(|s| ForwardedSession {
|
self.sessions
|
||||||
id: s.id.clone(),
|
.read()
|
||||||
local_port: s.local_port,
|
.await
|
||||||
remote_host: s.remote_host.clone(),
|
.get(id)
|
||||||
remote_port: s.remote_port,
|
.map(|s| ForwardedSession {
|
||||||
connected: s.connected,
|
id: s.id.clone(),
|
||||||
client_count: s.client_count,
|
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
|
/// HTTP endpoint handler for terminal spawn requests
|
||||||
pub async fn handle_terminal_spawn(
|
pub async fn handle_terminal_spawn(port: u16, _shell: Option<String>) -> Result<(), String> {
|
||||||
port: u16,
|
|
||||||
_shell: Option<String>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// Listen for HTTP requests on the specified port
|
// Listen for HTTP requests on the specified port
|
||||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
||||||
.await
|
.await
|
||||||
|
|
@ -344,7 +349,8 @@ pub async fn handle_terminal_spawn(
|
||||||
info!("Terminal spawn service listening on port {}", port);
|
info!("Terminal spawn service listening on port {}", port);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (stream, addr) = listener.accept()
|
let (stream, addr) = listener
|
||||||
|
.accept()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to accept spawn connection: {}", e))?;
|
.map_err(|e| format!("Failed to accept spawn connection: {}", e))?;
|
||||||
|
|
||||||
|
|
@ -360,13 +366,11 @@ pub async fn handle_terminal_spawn(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a single terminal spawn request
|
/// Handle a single terminal spawn request
|
||||||
async fn handle_spawn_request(
|
async fn handle_spawn_request(mut stream: TcpStream, _shell: Option<String>) -> Result<(), String> {
|
||||||
mut stream: TcpStream,
|
|
||||||
_shell: Option<String>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// Simple HTTP response
|
// Simple HTTP response
|
||||||
let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nTerminal spawned\r\n";
|
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
|
.await
|
||||||
.map_err(|e| format!("Failed to write response: {}", e))?;
|
.map_err(|e| format!("Failed to write response: {}", e))?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use chrono::{DateTime, Utc, TimeZone};
|
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use tauri_plugin_updater::UpdaterExt;
|
use tauri_plugin_updater::UpdaterExt;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// Update channel type
|
/// Update channel type
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -155,7 +155,10 @@ impl UpdateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the notification manager
|
/// Set the notification manager
|
||||||
pub fn set_notification_manager(&mut self, notification_manager: Arc<crate::notification_manager::NotificationManager>) {
|
pub fn set_notification_manager(
|
||||||
|
&mut self,
|
||||||
|
notification_manager: Arc<crate::notification_manager::NotificationManager>,
|
||||||
|
) {
|
||||||
self.notification_manager = Some(notification_manager);
|
self.notification_manager = Some(notification_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,12 +169,13 @@ impl UpdateManager {
|
||||||
let mut updater_settings = self.settings.write().await;
|
let mut updater_settings = self.settings.write().await;
|
||||||
updater_settings.channel = UpdateChannel::from_str(&update_settings.channel);
|
updater_settings.channel = UpdateChannel::from_str(&update_settings.channel);
|
||||||
updater_settings.check_on_startup = true;
|
updater_settings.check_on_startup = true;
|
||||||
updater_settings.check_interval_hours = match update_settings.check_frequency.as_str() {
|
updater_settings.check_interval_hours =
|
||||||
"daily" => 24,
|
match update_settings.check_frequency.as_str() {
|
||||||
"weekly" => 168,
|
"daily" => 24,
|
||||||
"monthly" => 720,
|
"weekly" => 168,
|
||||||
_ => 24,
|
"monthly" => 720,
|
||||||
};
|
_ => 24,
|
||||||
|
};
|
||||||
updater_settings.auto_download = update_settings.auto_download;
|
updater_settings.auto_download = update_settings.auto_download;
|
||||||
updater_settings.auto_install = update_settings.auto_install;
|
updater_settings.auto_install = update_settings.auto_install;
|
||||||
updater_settings.show_release_notes = update_settings.show_release_notes;
|
updater_settings.show_release_notes = update_settings.show_release_notes;
|
||||||
|
|
@ -229,7 +233,8 @@ impl UpdateManager {
|
||||||
self.emit_update_event("checking", None).await;
|
self.emit_update_event("checking", None).await;
|
||||||
|
|
||||||
let app_handle_guard = self.app_handle.read().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())?;
|
.ok_or_else(|| "App handle not set".to_string())?;
|
||||||
|
|
||||||
// Get the updater instance
|
// Get the updater instance
|
||||||
|
|
@ -241,13 +246,19 @@ impl UpdateManager {
|
||||||
// Build updater with channel-specific endpoint
|
// Build updater with channel-specific endpoint
|
||||||
let updater_result = match settings.channel {
|
let updater_result = match settings.channel {
|
||||||
UpdateChannel::Stable => updater.endpoints(vec![
|
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![
|
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![
|
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 => {
|
UpdateChannel::Custom => {
|
||||||
if let Some(endpoint) = &settings.custom_endpoint {
|
if let Some(endpoint) = &settings.custom_endpoint {
|
||||||
|
|
@ -265,77 +276,84 @@ impl UpdateManager {
|
||||||
match updater_result {
|
match updater_result {
|
||||||
Ok(updater_builder) => match updater_builder.build() {
|
Ok(updater_builder) => match updater_builder.build() {
|
||||||
Ok(updater) => {
|
Ok(updater) => {
|
||||||
match updater.check().await {
|
match updater.check().await {
|
||||||
Ok(Some(update)) => {
|
Ok(Some(update)) => {
|
||||||
let update_info = UpdateInfo {
|
let update_info = UpdateInfo {
|
||||||
version: update.version.clone(),
|
version: update.version.clone(),
|
||||||
notes: update.body.clone().unwrap_or_default(),
|
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())),
|
pub_date: update.date.map(|d| {
|
||||||
download_size: None, // TODO: Get from update
|
Utc.timestamp_opt(d.unix_timestamp(), 0)
|
||||||
signature: None,
|
.single()
|
||||||
download_url: String::new(), // Will be set by updater
|
.unwrap_or(Utc::now())
|
||||||
channel: settings.channel,
|
}),
|
||||||
};
|
download_size: None, // TODO: Get from update
|
||||||
|
signature: None,
|
||||||
|
download_url: String::new(), // Will be set by updater
|
||||||
|
channel: settings.channel,
|
||||||
|
};
|
||||||
|
|
||||||
// Update state
|
// 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;
|
let mut state = self.state.write().await;
|
||||||
state.status = UpdateStatus::Available;
|
state.status = UpdateStatus::NoUpdate;
|
||||||
state.available_update = Some(update_info.clone());
|
|
||||||
state.last_check = Some(Utc::now());
|
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);
|
||||||
|
|
||||||
// Emit available event
|
let mut state = self.state.write().await;
|
||||||
self.emit_update_event("available", Some(&update_info)).await;
|
state.status = UpdateStatus::Error;
|
||||||
|
state.last_error = Some(error_msg.clone());
|
||||||
|
state.last_check = Some(Utc::now());
|
||||||
|
|
||||||
// Show notification
|
self.emit_update_event("error", None).await;
|
||||||
if let Some(notification_manager) = &self.notification_manager {
|
|
||||||
let _ = notification_manager.notify_update_available(
|
Err(error_msg)
|
||||||
&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::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) => {
|
||||||
Err(e) => {
|
let error_msg = format!("Failed to build updater: {}", e);
|
||||||
let error_msg = format!("Failed to build updater: {}", e);
|
|
||||||
|
|
||||||
let mut state = self.state.write().await;
|
let mut state = self.state.write().await;
|
||||||
state.status = UpdateStatus::Error;
|
state.status = UpdateStatus::Error;
|
||||||
state.last_error = Some(error_msg.clone());
|
state.last_error = Some(error_msg.clone());
|
||||||
|
|
||||||
Err(error_msg)
|
Err(error_msg)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = format!("Failed to configure updater endpoints: {}", e);
|
let error_msg = format!("Failed to configure updater endpoints: {}", e);
|
||||||
|
|
@ -487,7 +505,8 @@ impl UpdateManager {
|
||||||
return;
|
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);
|
drop(settings);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
|
||||||
|
|
@ -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 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
|
/// Tutorial step structure
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -113,7 +113,8 @@ impl WelcomeManager {
|
||||||
|
|
||||||
// Update settings to reflect welcome state
|
// Update settings to reflect welcome state
|
||||||
if let Ok(mut settings) = crate::settings::Settings::load() {
|
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())?;
|
settings.save().map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +139,9 @@ impl WelcomeManager {
|
||||||
|
|
||||||
/// Get specific tutorial category
|
/// Get specific tutorial category
|
||||||
pub async fn get_tutorial_category(&self, category_id: &str) -> Option<TutorialCategory> {
|
pub async fn get_tutorial_category(&self, category_id: &str) -> Option<TutorialCategory> {
|
||||||
self.tutorials.read().await
|
self.tutorials
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.id == category_id)
|
.find(|c| c.id == category_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
@ -153,9 +156,7 @@ impl WelcomeManager {
|
||||||
|
|
||||||
// Check if all steps are completed
|
// Check if all steps are completed
|
||||||
let tutorials = self.tutorials.read().await;
|
let tutorials = self.tutorials.read().await;
|
||||||
let total_steps: usize = tutorials.iter()
|
let total_steps: usize = tutorials.iter().map(|c| c.steps.len()).sum();
|
||||||
.map(|c| c.steps.len())
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
if state.completed_steps.len() >= total_steps {
|
if state.completed_steps.len() >= total_steps {
|
||||||
state.tutorial_completed = true;
|
state.tutorial_completed = true;
|
||||||
|
|
@ -212,7 +213,7 @@ impl WelcomeManager {
|
||||||
tauri::WebviewWindowBuilder::new(
|
tauri::WebviewWindowBuilder::new(
|
||||||
app_handle,
|
app_handle,
|
||||||
"welcome",
|
"welcome",
|
||||||
tauri::WebviewUrl::App("welcome.html".into())
|
tauri::WebviewUrl::App("welcome.html".into()),
|
||||||
)
|
)
|
||||||
.title("Welcome to VibeTunnel")
|
.title("Welcome to VibeTunnel")
|
||||||
.inner_size(800.0, 600.0)
|
.inner_size(800.0, 600.0)
|
||||||
|
|
@ -409,9 +410,7 @@ VibeTunnel will be ready whenever you need it."#.to_string(),
|
||||||
let state = self.state.read().await;
|
let state = self.state.read().await;
|
||||||
let tutorials = self.tutorials.read().await;
|
let tutorials = self.tutorials.read().await;
|
||||||
|
|
||||||
let total_steps: usize = tutorials.iter()
|
let total_steps: usize = tutorials.iter().map(|c| c.steps.len()).sum();
|
||||||
.map(|c| c.steps.len())
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
let completed_steps = state.completed_steps.len();
|
let completed_steps = state.completed_steps.len();
|
||||||
let percentage = if total_steps > 0 {
|
let percentage = if total_steps > 0 {
|
||||||
|
|
@ -424,18 +423,23 @@ VibeTunnel will be ready whenever you need it."#.to_string(),
|
||||||
total_steps,
|
total_steps,
|
||||||
completed_steps,
|
completed_steps,
|
||||||
percentage,
|
percentage,
|
||||||
categories: tutorials.iter().map(|category| {
|
categories: tutorials
|
||||||
let category_completed = category.steps.iter()
|
.iter()
|
||||||
.filter(|s| state.completed_steps.contains(&s.id))
|
.map(|category| {
|
||||||
.count();
|
let category_completed = category
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.filter(|s| state.completed_steps.contains(&s.id))
|
||||||
|
.count();
|
||||||
|
|
||||||
CategoryProgress {
|
CategoryProgress {
|
||||||
category_id: category.id.clone(),
|
category_id: category.id.clone(),
|
||||||
category_name: category.name.clone(),
|
category_name: category.name.clone(),
|
||||||
total_steps: category.steps.len(),
|
total_steps: category.steps.len(),
|
||||||
completed_steps: category_completed,
|
completed_steps: category_completed,
|
||||||
}
|
}
|
||||||
}).collect(),
|
})
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue