mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-18 13:25:52 +00:00
1041 lines
38 KiB
Rust
1041 lines
38 KiB
Rust
#![cfg_attr(
|
|
all(not(debug_assertions), target_os = "windows"),
|
|
windows_subsystem = "windows"
|
|
)]
|
|
|
|
use tauri::menu::Menu;
|
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
|
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
|
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|
|
|
mod api_client;
|
|
mod api_testing;
|
|
mod app_mover;
|
|
mod auth_cache;
|
|
mod auto_launch;
|
|
mod backend_manager;
|
|
mod cli_installer;
|
|
mod commands;
|
|
mod debug_features;
|
|
mod errors;
|
|
mod fs_api;
|
|
mod keychain;
|
|
mod network_utils;
|
|
mod ngrok;
|
|
mod notification_manager;
|
|
mod permissions;
|
|
mod port_conflict;
|
|
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;
|
|
#[cfg(unix)]
|
|
mod unix_socket_server;
|
|
mod updater;
|
|
mod welcome;
|
|
|
|
use commands::ServerStatus;
|
|
use commands::*;
|
|
use state::AppState;
|
|
|
|
#[tauri::command]
|
|
fn open_settings_window(app: AppHandle, tab: Option<String>) -> Result<(), String> {
|
|
tracing::info!("Opening settings window");
|
|
|
|
// Build URL with optional tab parameter
|
|
let url = if let Some(tab_name) = tab {
|
|
format!("settings.html?tab={}", tab_name)
|
|
} else {
|
|
"settings.html".to_string()
|
|
};
|
|
|
|
// Check if settings window already exists
|
|
if let Some(window) = app.get_webview_window("settings") {
|
|
// Navigate to the URL with the tab parameter if window exists
|
|
window
|
|
.eval(&format!("window.location.href = '{}'", url))
|
|
.map_err(|e| e.to_string())?;
|
|
window.show().map_err(|e| e.to_string())?;
|
|
window.set_focus().map_err(|e| e.to_string())?;
|
|
} else {
|
|
// Create new settings window
|
|
tracing::info!("Creating new settings window with URL: {}", url);
|
|
let window =
|
|
tauri::WebviewWindowBuilder::new(&app, "settings", tauri::WebviewUrl::App(url.into()))
|
|
.title("VibeTunnel Settings")
|
|
.inner_size(1200.0, 800.0)
|
|
.resizable(true)
|
|
.decorations(true)
|
|
.center()
|
|
.build()
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to create settings window: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
|
|
tracing::info!("Settings window created successfully");
|
|
|
|
// Handle close event to destroy the window
|
|
let window_clone = window.clone();
|
|
window.on_window_event(move |event| {
|
|
if let WindowEvent::CloseRequested { .. } = event {
|
|
let _ = window_clone.close();
|
|
}
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn focus_terminal_window(session_id: String) -> Result<(), String> {
|
|
// Focus the terminal window for the given session
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use std::process::Command;
|
|
|
|
// Use AppleScript to focus the terminal window
|
|
let script = format!(
|
|
r#"tell application "System Events"
|
|
set allProcesses to name of every process
|
|
if "Terminal" is in allProcesses then
|
|
tell application "Terminal"
|
|
activate
|
|
repeat with w in windows
|
|
if name of w contains "{}" then
|
|
set index of w to 1
|
|
return
|
|
end if
|
|
end repeat
|
|
end tell
|
|
end if
|
|
end tell"#,
|
|
session_id
|
|
);
|
|
|
|
let output = Command::new("osascript")
|
|
.arg("-e")
|
|
.arg(&script)
|
|
.output()
|
|
.map_err(|e| format!("Failed to execute AppleScript: {}", e))?;
|
|
|
|
if !output.status.success() {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!("AppleScript failed: {}", error));
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
// On other platforms, we can try to use wmctrl or similar tools
|
|
// For now, just return an error
|
|
return Err("Terminal window focus not implemented for this platform".to_string());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn open_session_detail_window(app: AppHandle, session_id: String) -> Result<(), String> {
|
|
// Build URL with session ID parameter
|
|
let url = format!("session-detail.html?id={}", session_id);
|
|
let window_id = format!("session-detail-{}", session_id);
|
|
|
|
// Check if session detail window already exists for this session
|
|
if let Some(window) = app.get_webview_window(&window_id) {
|
|
window.show().map_err(|e| e.to_string())?;
|
|
window.set_focus().map_err(|e| e.to_string())?;
|
|
} else {
|
|
// Create new session detail window
|
|
let window =
|
|
tauri::WebviewWindowBuilder::new(&app, window_id, tauri::WebviewUrl::App(url.into()))
|
|
.title("Session Details")
|
|
.inner_size(600.0, 450.0)
|
|
.resizable(true)
|
|
.decorations(true)
|
|
.center()
|
|
.build()
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
// Handle close event to destroy the window
|
|
let window_clone = window.clone();
|
|
window.on_window_event(move |event| {
|
|
if let WindowEvent::CloseRequested { .. } = event {
|
|
let _ = window_clone.close();
|
|
}
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn update_tray_menu_status(app: &AppHandle, port: u16, session_count: usize) {
|
|
// Update tray menu status using the tray menu manager
|
|
let app_handle = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
tray_menu::TrayMenuManager::update_server_status(&app_handle, port, true).await;
|
|
tray_menu::TrayMenuManager::update_session_count(&app_handle, session_count).await;
|
|
});
|
|
}
|
|
|
|
fn main() {
|
|
// Initialize tracing
|
|
tracing_subscriber::registry()
|
|
.with(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| "vibetunnel=debug,tower_http=debug".into()),
|
|
)
|
|
.with(tracing_subscriber::fmt::layer())
|
|
.init();
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_http::init())
|
|
.plugin(tauri_plugin_notification::init())
|
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
|
.plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| {
|
|
// Don't show main window on startup - app runs in system tray
|
|
// let _ = show_main_window(app.app_handle().clone());
|
|
}))
|
|
.manage(AppState::new())
|
|
.invoke_handler(tauri::generate_handler![
|
|
create_terminal,
|
|
list_terminals,
|
|
close_terminal,
|
|
resize_terminal,
|
|
write_to_terminal,
|
|
read_from_terminal,
|
|
start_server,
|
|
stop_server,
|
|
get_server_status,
|
|
get_app_version,
|
|
get_os,
|
|
restart_server,
|
|
show_server_console,
|
|
show_welcome_screen,
|
|
purge_all_settings,
|
|
update_dock_icon_visibility,
|
|
show_main_window,
|
|
open_settings_window,
|
|
open_session_detail_window,
|
|
focus_terminal_window,
|
|
quit_app,
|
|
settings::get_settings,
|
|
settings::save_settings,
|
|
auto_launch::set_auto_launch,
|
|
auto_launch::get_auto_launch,
|
|
ngrok::start_ngrok_tunnel,
|
|
ngrok::stop_ngrok_tunnel,
|
|
ngrok::get_ngrok_status,
|
|
terminal_detector::detect_system_terminals,
|
|
terminal_detector::get_default_shell,
|
|
cli_installer::install_cli,
|
|
cli_installer::uninstall_cli,
|
|
cli_installer::check_cli_installed,
|
|
start_tty_forward,
|
|
stop_tty_forward,
|
|
list_tty_forwards,
|
|
get_tty_forward,
|
|
get_session_stats,
|
|
get_monitored_sessions,
|
|
start_session_monitoring,
|
|
check_port_availability,
|
|
detect_port_conflict,
|
|
resolve_port_conflict,
|
|
force_kill_process,
|
|
find_available_ports,
|
|
get_local_ip_address,
|
|
get_all_ip_addresses,
|
|
get_network_interfaces,
|
|
get_hostname,
|
|
test_network_connectivity,
|
|
get_network_stats,
|
|
show_notification,
|
|
get_notifications,
|
|
get_notification_history,
|
|
mark_notification_as_read,
|
|
mark_all_notifications_as_read,
|
|
clear_notification,
|
|
clear_all_notifications,
|
|
get_unread_notification_count,
|
|
update_notification_settings,
|
|
get_notification_settings,
|
|
get_welcome_state,
|
|
should_show_welcome,
|
|
get_tutorials,
|
|
get_tutorial_category,
|
|
complete_tutorial_step,
|
|
skip_tutorial,
|
|
reset_tutorial,
|
|
get_tutorial_progress,
|
|
show_welcome_window,
|
|
get_all_advanced_settings,
|
|
update_advanced_settings,
|
|
reset_settings_section,
|
|
export_settings,
|
|
import_settings,
|
|
check_all_permissions,
|
|
check_permission,
|
|
check_permission_silent,
|
|
request_permission,
|
|
get_permission_info,
|
|
get_all_permissions,
|
|
get_required_permissions,
|
|
get_missing_required_permissions,
|
|
all_required_permissions_granted,
|
|
open_system_permission_settings,
|
|
get_permission_stats,
|
|
check_for_updates,
|
|
download_update,
|
|
install_update,
|
|
cancel_update,
|
|
get_update_state,
|
|
get_updater_settings,
|
|
update_updater_settings,
|
|
switch_update_channel,
|
|
get_update_history,
|
|
// Multi-backend commands not applicable to Node.js subprocess
|
|
// get_available_backends,
|
|
// get_backend_config,
|
|
// is_backend_installed,
|
|
// install_backend,
|
|
// start_backend,
|
|
// stop_backend,
|
|
// switch_backend,
|
|
// get_active_backend,
|
|
// get_backend_instances,
|
|
// check_backend_health,
|
|
// get_backend_stats,
|
|
get_debug_settings,
|
|
update_debug_settings,
|
|
log_debug_message,
|
|
record_performance_metric,
|
|
take_memory_snapshot,
|
|
get_debug_logs,
|
|
get_performance_metrics,
|
|
get_memory_snapshots,
|
|
get_network_requests,
|
|
run_api_tests,
|
|
run_benchmarks,
|
|
generate_diagnostic_report,
|
|
clear_debug_data,
|
|
set_debug_mode,
|
|
get_debug_stats,
|
|
get_api_test_config,
|
|
update_api_test_config,
|
|
add_api_test_suite,
|
|
get_api_test_suite,
|
|
list_api_test_suites,
|
|
run_single_api_test,
|
|
run_api_test_suite,
|
|
get_api_test_history,
|
|
clear_api_test_history,
|
|
import_postman_collection,
|
|
export_api_test_suite,
|
|
get_auth_cache_config,
|
|
update_auth_cache_config,
|
|
store_auth_token,
|
|
get_auth_token,
|
|
store_auth_credential,
|
|
get_auth_credential,
|
|
clear_auth_cache_entry,
|
|
clear_all_auth_cache,
|
|
get_auth_cache_stats,
|
|
list_auth_cache_entries,
|
|
export_auth_cache,
|
|
import_auth_cache,
|
|
hash_password,
|
|
create_auth_cache_key,
|
|
detect_installed_terminals,
|
|
get_default_terminal,
|
|
set_default_terminal,
|
|
launch_terminal_emulator,
|
|
get_terminal_config,
|
|
update_terminal_config,
|
|
list_detected_terminals,
|
|
create_terminal_ssh_url,
|
|
get_terminal_integration_stats,
|
|
// Settings UI Commands
|
|
get_all_settings,
|
|
update_setting,
|
|
set_dashboard_password,
|
|
restart_server_with_port,
|
|
update_server_bind_address,
|
|
set_dock_icon_visibility,
|
|
set_log_level,
|
|
test_api_endpoint,
|
|
get_server_logs,
|
|
export_logs,
|
|
get_local_ip,
|
|
detect_terminals,
|
|
// App Mover Commands
|
|
app_mover::prompt_move_to_applications,
|
|
app_mover::is_in_applications_folder_command,
|
|
// Terminal Spawn Service Commands
|
|
terminal_spawn_service::spawn_terminal_for_session,
|
|
terminal_spawn_service::spawn_terminal_with_command,
|
|
terminal_spawn_service::spawn_custom_terminal,
|
|
// Keychain Commands
|
|
keychain_set_password,
|
|
keychain_get_password,
|
|
keychain_delete_password,
|
|
keychain_set_dashboard_password,
|
|
keychain_get_dashboard_password,
|
|
keychain_delete_dashboard_password,
|
|
keychain_set_ngrok_auth_token,
|
|
keychain_get_ngrok_auth_token,
|
|
keychain_delete_ngrok_auth_token,
|
|
keychain_list_keys,
|
|
// Welcome flow commands
|
|
request_all_permissions,
|
|
test_terminal,
|
|
check_vt_installation,
|
|
install_vt,
|
|
check_permissions,
|
|
request_automation_permission,
|
|
request_accessibility_permission,
|
|
save_dashboard_password,
|
|
open_dashboard,
|
|
finish_welcome,
|
|
])
|
|
.setup(|app| {
|
|
// Set app handle in managers
|
|
let state_clone = app.state::<AppState>().inner().clone();
|
|
let app_handle = app.handle().clone();
|
|
let app_handle2 = app.handle().clone();
|
|
let app_handle3 = app.handle().clone();
|
|
let app_handle4 = app.handle().clone();
|
|
let app_handle_for_move = app.handle().clone();
|
|
|
|
tauri::async_runtime::spawn(async move {
|
|
let state = state_clone;
|
|
state.notification_manager.set_app_handle(app_handle).await;
|
|
state.welcome_manager.set_app_handle(app_handle2).await;
|
|
state.permissions_manager.set_app_handle(app_handle3).await;
|
|
state.update_manager.set_app_handle(app_handle4).await;
|
|
|
|
// Start background workers now that we have a runtime
|
|
state.terminal_spawn_service.clone().start_worker().await;
|
|
state.auth_cache_manager.start_cleanup_task().await;
|
|
|
|
// Start Unix socket server for terminal spawning (macOS/Linux)
|
|
#[cfg(unix)]
|
|
{
|
|
if let Err(e) = state.unix_socket_server.start() {
|
|
tracing::error!("Failed to start Unix socket server: {}", e);
|
|
}
|
|
}
|
|
|
|
// Start session monitoring
|
|
state.session_monitor.start_monitoring().await;
|
|
|
|
// Load welcome state and check if should show welcome
|
|
let _ = state.welcome_manager.load_state().await;
|
|
if state.welcome_manager.should_show_welcome().await {
|
|
let _ = state.welcome_manager.show_welcome_window().await;
|
|
}
|
|
|
|
// Check permissions on startup
|
|
let _ = state.permissions_manager.check_all_permissions().await;
|
|
|
|
// Check if app should be moved to Applications folder (macOS only)
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let app_handle_move = app_handle_for_move.clone();
|
|
tokio::spawn(async move {
|
|
// Small delay to let the app fully initialize
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
|
let _ = app_mover::check_and_prompt_move(app_handle_move).await;
|
|
});
|
|
}
|
|
|
|
// Load updater settings and start auto-check
|
|
let _ = state.update_manager.load_settings().await;
|
|
state.update_manager.clone().start_auto_check().await;
|
|
});
|
|
|
|
// Create system tray icon using tray-icon.png for macOS (menu-bar-icon.png is for Windows/Linux)
|
|
let tray_icon = if let Ok(resource_dir) = app.path().resource_dir() {
|
|
// On macOS, use tray-icon.png which has the proper design for the menu bar
|
|
let icon_name = if cfg!(target_os = "macos") {
|
|
"tray-icon.png"
|
|
} else {
|
|
"menu-bar-icon.png"
|
|
};
|
|
|
|
let icon_path = resource_dir.join(icon_name);
|
|
if let Ok(icon_data) = std::fs::read(&icon_path) {
|
|
tauri::image::Image::from_bytes(&icon_data).ok()
|
|
} else {
|
|
// Try alternative path
|
|
let icon_path2 = resource_dir.join("icons").join(icon_name);
|
|
if let Ok(icon_data) = std::fs::read(&icon_path2) {
|
|
tauri::image::Image::from_bytes(&icon_data).ok()
|
|
} else {
|
|
// Fallback to default icon
|
|
app.default_window_icon().cloned()
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback to default icon if resource dir not found
|
|
app.default_window_icon().cloned()
|
|
};
|
|
|
|
if let Some(icon) = tray_icon {
|
|
// Create enhanced tray menu
|
|
let menu = tray_menu::TrayMenuManager::create_menu(&app.handle())?;
|
|
|
|
// Build tray icon with template mode for macOS
|
|
let _tray = TrayIconBuilder::with_id("main")
|
|
.icon(icon)
|
|
.icon_as_template(true) // Enable template mode for proper macOS tinting
|
|
.tooltip("VibeTunnel")
|
|
.menu(&menu)
|
|
.on_menu_event(move |app, event| {
|
|
handle_tray_menu_event(app, event.id.as_ref());
|
|
})
|
|
.on_tray_icon_event(|tray, event| {
|
|
if let TrayIconEvent::Click {
|
|
button: MouseButton::Left,
|
|
button_state: MouseButtonState::Up,
|
|
..
|
|
} = event
|
|
{
|
|
// Get server status and open dashboard in browser
|
|
let app = tray.app_handle();
|
|
let state = app.state::<AppState>();
|
|
if state.backend_manager.blocking_is_running() {
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
|
|
let _ = open::that(url);
|
|
}
|
|
}
|
|
})
|
|
.build(app)?;
|
|
}
|
|
|
|
// Load settings to determine initial dock icon visibility
|
|
let settings = settings::Settings::load().unwrap_or_default();
|
|
|
|
// Set initial dock icon visibility on macOS
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
// Force dock icon to be visible for debugging
|
|
app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
|
// if !settings.general.show_dock_icon {
|
|
// app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
|
// }
|
|
}
|
|
|
|
// Show settings window for debugging
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
let app_handle = app.handle().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
// Wait a bit for the app to fully initialize
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
|
|
// Open settings window
|
|
if let Err(e) = open_settings_window(app_handle, None) {
|
|
tracing::error!("Failed to open settings window: {}", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Auto-start server with monitoring
|
|
let app_handle = app.handle().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
tracing::info!("Starting server with monitoring...");
|
|
start_server_with_monitoring(app_handle).await;
|
|
});
|
|
|
|
Ok(())
|
|
})
|
|
.on_menu_event(handle_menu_event)
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[allow(dead_code)]
|
|
fn create_app_menu(app: &tauri::App) -> Result<Menu<tauri::Wry>, tauri::Error> {
|
|
// Create the menu using the builder pattern
|
|
let menu = Menu::new(app)?;
|
|
|
|
// For now, return a basic menu
|
|
// TODO: Once we understand the correct Tauri v2 menu API, implement full menu
|
|
Ok(menu)
|
|
}
|
|
|
|
fn handle_tray_menu_event(app: &AppHandle, event_id: &str) {
|
|
match event_id {
|
|
"dashboard" => {
|
|
// Get server status and open dashboard in browser
|
|
let state = app.state::<AppState>();
|
|
let backend_manager = state.backend_manager.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
if backend_manager.is_running().await {
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
|
|
let _ = open::that(url);
|
|
}
|
|
});
|
|
}
|
|
"show_tutorial" => {
|
|
// Show welcome window instead
|
|
let state = app.state::<AppState>();
|
|
let welcome_manager = state.welcome_manager.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let _ = welcome_manager.show_welcome_window().await;
|
|
});
|
|
}
|
|
"website" => {
|
|
let _ = open::that("https://vibetunnel.sh");
|
|
}
|
|
"report_issue" => {
|
|
let _ = open::that("https://github.com/amantus-ai/vibetunnel/issues");
|
|
}
|
|
"check_updates" => {
|
|
// TODO: Implement update check
|
|
tracing::info!("Check for updates");
|
|
}
|
|
"about" => {
|
|
// TODO: Show about dialog
|
|
tracing::info!("About VibeTunnel");
|
|
}
|
|
"settings" => {
|
|
// Open native settings window
|
|
let _ = open_settings_window(app.clone(), None);
|
|
}
|
|
"quit" => {
|
|
quit_app(app.clone());
|
|
}
|
|
_ => {
|
|
// Handle session clicks (format: "session_<id>")
|
|
if event_id.starts_with("session_") {
|
|
let session_id = event_id.strip_prefix("session_").unwrap_or("");
|
|
if !session_id.is_empty() {
|
|
// Open session detail window
|
|
let _ = open_session_detail_window(app.clone(), session_id.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_menu_event(app: &AppHandle, event: tauri::menu::MenuEvent) {
|
|
match event.id.as_ref() {
|
|
"settings" => {
|
|
// Open native settings window instead of main window
|
|
let _ = open_settings_window(app.clone(), None);
|
|
}
|
|
"new-terminal" => {
|
|
// Terminal creation should be done via the web dashboard
|
|
// Open dashboard in browser
|
|
let state = app.state::<AppState>();
|
|
let backend_manager = state.backend_manager.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
if backend_manager.is_running().await {
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
|
|
let _ = open::that(url);
|
|
}
|
|
});
|
|
}
|
|
"reload" => {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.eval("window.location.reload()");
|
|
}
|
|
}
|
|
"show-dashboard" => {
|
|
// Open dashboard in browser instead of showing main window
|
|
let state = app.state::<AppState>();
|
|
let backend_manager = state.backend_manager.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
if backend_manager.is_running().await {
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let url = format!("http://127.0.0.1:{}", settings.dashboard.server_port);
|
|
let _ = open::that(url);
|
|
}
|
|
});
|
|
}
|
|
"quit" => {
|
|
quit_app(app.clone());
|
|
}
|
|
"hide" => {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.hide();
|
|
}
|
|
}
|
|
"minimize" => {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.minimize();
|
|
}
|
|
}
|
|
"zoom" => {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
// Toggle maximize state
|
|
if window.is_maximized().unwrap_or(false) {
|
|
let _ = window.unmaximize();
|
|
} else {
|
|
let _ = window.maximize();
|
|
}
|
|
}
|
|
}
|
|
"fullscreen" => {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.set_fullscreen(!window.is_fullscreen().unwrap_or(false));
|
|
}
|
|
}
|
|
"close-window" => {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.close();
|
|
}
|
|
}
|
|
"cut" | "copy" | "paste" | "select-all" | "undo" | "redo" => {
|
|
// These are handled by the system automatically for text fields
|
|
// For terminal, we'll emit events
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.emit(&format!("menu:{}", event.id.as_ref()), ());
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn show_main_window(app: AppHandle) -> Result<(), String> {
|
|
let window = if let Some(window) = app.get_webview_window("main") {
|
|
window
|
|
} else {
|
|
// Create main window if it doesn't exist
|
|
tauri::WebviewWindowBuilder::new(&app, "main", tauri::WebviewUrl::App("index.html".into()))
|
|
.title("VibeTunnel")
|
|
.inner_size(1200.0, 800.0)
|
|
.center()
|
|
.resizable(true)
|
|
.decorations(true)
|
|
.build()
|
|
.map_err(|e| e.to_string())?
|
|
};
|
|
|
|
window.show().map_err(|e| e.to_string())?;
|
|
window.set_focus().map_err(|e| e.to_string())?;
|
|
|
|
// Show dock icon on macOS when window is shown
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let _ = app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
|
}
|
|
|
|
// Handle window close event to hide instead of quit
|
|
let window_clone = window.clone();
|
|
let app_clone = app.clone();
|
|
window.on_window_event(move |event| {
|
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
|
api.prevent_close();
|
|
let _ = window_clone.hide();
|
|
|
|
// Hide dock icon on macOS when window is hidden (only if settings say so)
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Ok(settings) = settings::Settings::load() {
|
|
if !settings.general.show_dock_icon {
|
|
let _ = app_clone.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn quit_app(app: AppHandle) {
|
|
// Stop monitoring before exit
|
|
let state = app.state::<AppState>();
|
|
state
|
|
.server_monitoring
|
|
.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
|
|
// Close all terminal sessions
|
|
let terminal_manager = state.terminal_manager.clone();
|
|
tauri::async_runtime::block_on(async move {
|
|
let _ = terminal_manager.close_all_sessions().await;
|
|
});
|
|
|
|
app.exit(0);
|
|
}
|
|
|
|
async fn start_server_with_monitoring(app_handle: AppHandle) {
|
|
let state = app_handle.state::<AppState>();
|
|
let state_clone = state.inner().clone();
|
|
|
|
// Start initial server
|
|
match start_server_internal(&*state).await {
|
|
Ok(status) => {
|
|
tracing::info!("Server started on port {}", status.port);
|
|
*state.server_target_port.write().await = Some(status.port);
|
|
|
|
// Update tray menu with server status
|
|
update_tray_menu_status(&app_handle, status.port, 0);
|
|
|
|
// Show notification
|
|
let _ = state
|
|
.notification_manager
|
|
.notify_server_status(true, status.port)
|
|
.await;
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to start server: {}", e);
|
|
let _ = state
|
|
.notification_manager
|
|
.notify_error(
|
|
"Server Start Failed",
|
|
&format!("Failed to start server: {}", e),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
// Monitor server health
|
|
let monitoring_state = state_clone.clone();
|
|
let monitoring_app = app_handle.clone();
|
|
|
|
tauri::async_runtime::spawn(async move {
|
|
let mut check_interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
|
|
|
while monitoring_state
|
|
.server_monitoring
|
|
.load(std::sync::atomic::Ordering::Relaxed)
|
|
{
|
|
check_interval.tick().await;
|
|
|
|
// Check if server is still running
|
|
let server_running = monitoring_state.backend_manager.is_running().await;
|
|
|
|
if server_running {
|
|
// Perform health check
|
|
let health_check_result = perform_server_health_check(&monitoring_state).await;
|
|
|
|
if !health_check_result {
|
|
tracing::warn!("Server health check failed, attempting restart...");
|
|
|
|
// Stop current server
|
|
let _ = stop_server_internal(&monitoring_state).await;
|
|
|
|
// Wait a bit before restart
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
|
|
|
// Restart server
|
|
match start_server_internal(&monitoring_state).await {
|
|
Ok(status) => {
|
|
tracing::info!("Server restarted on port {}", status.port);
|
|
*monitoring_state.server_target_port.write().await = Some(status.port);
|
|
|
|
// Update tray menu with server status
|
|
update_tray_menu_status(&monitoring_app, status.port, 0);
|
|
|
|
// Notify frontend of server restart
|
|
if let Some(window) = monitoring_app.get_webview_window("main") {
|
|
let _ = window.emit("server:restarted", &status);
|
|
}
|
|
|
|
// Show notification
|
|
let _ = monitoring_state
|
|
.notification_manager
|
|
.notify_server_status(true, status.port)
|
|
.await;
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to restart server: {}", e);
|
|
let _ = monitoring_state
|
|
.notification_manager
|
|
.notify_error(
|
|
"Server Restart Failed",
|
|
&format!("Failed to restart server: {}", e),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Server is not running, attempt to start it
|
|
let target_port = *monitoring_state.server_target_port.read().await;
|
|
if target_port.is_some() {
|
|
tracing::info!("Server not running, attempting to start...");
|
|
|
|
match start_server_internal(&monitoring_state).await {
|
|
Ok(status) => {
|
|
tracing::info!("Server started on port {}", status.port);
|
|
|
|
// Notify frontend of server restart
|
|
if let Some(window) = monitoring_app.get_webview_window("main") {
|
|
let _ = window.emit("server:restarted", &status);
|
|
}
|
|
|
|
// Show notification
|
|
let _ = monitoring_state
|
|
.notification_manager
|
|
.notify_server_status(true, status.port)
|
|
.await;
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to start server: {}", e);
|
|
let _ = monitoring_state
|
|
.notification_manager
|
|
.notify_error(
|
|
"Server Start Failed",
|
|
&format!("Failed to start server: {}", e),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tracing::info!("Server monitoring stopped");
|
|
});
|
|
}
|
|
|
|
async fn perform_server_health_check(state: &AppState) -> bool {
|
|
// Try to get server status
|
|
match get_server_status_internal(state).await {
|
|
Ok(status) if status.running => {
|
|
// Server reports as running, perform additional check
|
|
// by trying to access the API endpoint
|
|
let url = format!("http://127.0.0.1:{}/api/sessions", status.port);
|
|
|
|
match reqwest::Client::new()
|
|
.get(&url)
|
|
.timeout(std::time::Duration::from_secs(2))
|
|
.send()
|
|
.await
|
|
{
|
|
Ok(response) => response.status().is_success(),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// Internal server management functions that work directly with AppState
|
|
async fn start_server_internal(state: &AppState) -> Result<ServerStatus, String> {
|
|
// Check if backend is already running
|
|
if state.backend_manager.is_running().await {
|
|
// Get port from settings
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let port = settings.dashboard.server_port;
|
|
|
|
// Check if ngrok is active
|
|
let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() {
|
|
ngrok_tunnel.url
|
|
} else {
|
|
match settings.dashboard.access_mode.as_str() {
|
|
"network" => format!("http://0.0.0.0:{}", port),
|
|
_ => format!("http://127.0.0.1:{}", port),
|
|
}
|
|
};
|
|
|
|
return Ok(ServerStatus {
|
|
running: true,
|
|
port,
|
|
url,
|
|
});
|
|
}
|
|
|
|
// Start the Node.js backend
|
|
state.backend_manager.start().await?;
|
|
|
|
// Load settings for access mode
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let port = settings.dashboard.server_port;
|
|
|
|
// Handle access mode
|
|
let url = match settings.dashboard.access_mode.as_str() {
|
|
"network" => {
|
|
// Node.js server handles network binding internally
|
|
format!("http://0.0.0.0:{}", port)
|
|
}
|
|
"ngrok" => {
|
|
// Try to start ngrok tunnel if auth token is configured
|
|
if let Some(auth_token) = settings.advanced.ngrok_auth_token {
|
|
if !auth_token.is_empty() {
|
|
match state
|
|
.ngrok_manager
|
|
.start_tunnel(port, Some(auth_token))
|
|
.await
|
|
{
|
|
Ok(tunnel) => tunnel.url,
|
|
Err(e) => {
|
|
tracing::error!("Failed to start ngrok tunnel: {}", e);
|
|
return Err(format!("Failed to start ngrok tunnel: {}", e));
|
|
}
|
|
}
|
|
} else {
|
|
return Err("Ngrok auth token is required for ngrok access mode".to_string());
|
|
}
|
|
} else {
|
|
return Err("Ngrok auth token is required for ngrok access mode".to_string());
|
|
}
|
|
}
|
|
_ => {
|
|
format!("http://127.0.0.1:{}", port)
|
|
}
|
|
};
|
|
|
|
Ok(ServerStatus {
|
|
running: true,
|
|
port,
|
|
url,
|
|
})
|
|
}
|
|
|
|
async fn stop_server_internal(state: &AppState) -> Result<(), String> {
|
|
// Stop the Node.js backend
|
|
state.backend_manager.stop().await?;
|
|
|
|
// Also stop ngrok tunnel if active
|
|
let _ = state.ngrok_manager.stop_tunnel().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_server_status_internal(state: &AppState) -> Result<ServerStatus, String> {
|
|
if state.backend_manager.is_running().await {
|
|
// Get port from settings
|
|
let settings = crate::settings::Settings::load().unwrap_or_default();
|
|
let port = settings.dashboard.server_port;
|
|
|
|
// Check if ngrok is active and return its URL
|
|
let url = if let Some(ngrok_tunnel) = state.ngrok_manager.get_tunnel_status() {
|
|
ngrok_tunnel.url
|
|
} else {
|
|
// Check settings to determine the correct URL format
|
|
match settings.dashboard.access_mode.as_str() {
|
|
"network" => format!("http://0.0.0.0:{}", port),
|
|
_ => format!("http://127.0.0.1:{}", port),
|
|
}
|
|
};
|
|
|
|
Ok(ServerStatus {
|
|
running: true,
|
|
port,
|
|
url,
|
|
})
|
|
} else {
|
|
Ok(ServerStatus {
|
|
running: false,
|
|
port: 0,
|
|
url: String::new(),
|
|
})
|
|
}
|
|
}
|