use std::sync::Arc; use tauri::{AppHandle, Manager, State}; mod gpu; mod model_library; mod model_files; mod runtime; use runtime::process::find_localllm_binary; use runtime::types::{RuntimeConfig, RuntimeSnapshot}; use runtime::{bootstrap_runtime, get_runtime_snapshot, save_runtime_config_command, start_runtime, stop_runtime, RuntimeState}; use gpu::query_gpu; use model_library::huggingface::{ cancel_hugging_face_download, search_hugging_face_models, start_hugging_face_download, HuggingFaceDownloadState, }; use model_library::local_index::list_entries; use model_library::paths::{managed_models_dir, model_library_index_path, resolve_models_dir}; use runtime::config::{load_runtime_config, save_runtime_config}; use model_library::types::{ModelLibraryEntry, ModelLibraryState}; use model_files::{import_model, pick_model_files, pick_models_dir, scan_models}; // ─── Server lifecycle ───────────────────────────────────────────────────────── #[tauri::command] fn bootstrap_runtime_command( app: AppHandle, state: State, ) -> Result { bootstrap_runtime(app, state) } #[tauri::command] fn get_runtime_snapshot_command(state: State) -> Result { get_runtime_snapshot(state) } #[tauri::command] fn save_runtime_config_command_handler( app: AppHandle, state: State, config: RuntimeConfig, ) -> Result { save_runtime_config_command(app, state, config) } #[tauri::command] fn start_runtime_command( app: AppHandle, state: State, ) -> Result { start_runtime(app, state) } #[tauri::command] fn stop_runtime_command(state: State) -> Result { stop_runtime(state) } /// Returns the resolved path to the localllm binary (useful for debugging). #[tauri::command] fn find_server_binary() -> Result { find_localllm_binary() .map(|p| p.to_string_lossy().to_string()) .ok_or_else(|| { "localllm binary not found. Build with: cargo build --release -p inference-server" .to_string() }) } /// Returns the app data directory path (useful for debugging). #[tauri::command] fn get_app_data_dir(app: AppHandle) -> Result { app.path() .app_data_dir() .map(|p| p.to_string_lossy().to_string()) .map_err(|e| format!("Cannot resolve app data dir: {e}")) } #[tauri::command] async fn search_hugging_face_models_command( query: String, token: Option, ) -> Result, String> { search_hugging_face_models(&query, token.as_deref()).await } #[tauri::command] async fn start_hugging_face_download_command( app: AppHandle, state: State<'_, Arc>, repo: String, file_name: String, token: Option, ) -> Result { // Load the user-configured models_dir (defaults to ./models if None/empty) let config = load_runtime_config(&app)?; let models_root = resolve_models_dir(config.models_dir.as_deref()); start_hugging_face_download( app, state.inner().clone(), repo, file_name, token, models_root, ) .await } #[tauri::command] fn cancel_hugging_face_download_command( state: State<'_, Arc>, download_id: String, ) -> Result<(), String> { cancel_hugging_face_download(state.inner(), &download_id) } /// Swap the active model: stops the runtime, updates model_path, restarts. /// This is the hot-swap primitive — used when user picks a different local model. #[tauri::command] async fn swap_model_command( app: AppHandle, state: State<'_, RuntimeState>, new_model_path: String, ) -> Result { // Stop first — this is a no-op if already stopped stop_runtime(state.clone())?; // Load, update model_path, save let mut config = load_runtime_config(&app)?; config.model_path = Some(new_model_path); save_runtime_config(&app, &config) .map_err(|e| format!("swap save failed: {e}"))?; // Restart with the updated config start_runtime(app, state) } /// Delete a local .gguf model file from disk. #[tauri::command] fn delete_model_file(file_path: String) -> Result<(), String> { let path = std::path::Path::new(&file_path); if !path.exists() { return Err(format!("File not found: {file_path}")); } std::fs::remove_file(path).map_err(|e| format!("Delete failed: {e}")) } /// Open the folder containing a model file in the native file explorer /// and optionally select/highlight the file. #[tauri::command] fn open_model_folder_command(file_path: String) -> Result<(), String> { let path = std::path::Path::new(&file_path); let _folder = path.parent().unwrap_or(path); #[cfg(target_os = "windows")] { std::process::Command::new("explorer") .args(["/select,", &file_path]) .spawn() .map_err(|e| format!("Failed to open Explorer: {e}"))?; } #[cfg(target_os = "macos")] { std::process::Command::new("open") .args(["-R", &file_path]) .spawn() .map_err(|e| format!("Failed to open Finder: {e}"))?; } #[cfg(target_os = "linux")] { std::process::Command::new("xdg-open") .arg(folder) .spawn() .map_err(|e| format!("Failed to open file manager: {e}"))?; } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] { let _ = folder; // folder is only used on some platforms return Err("Unsupported platform".into()); } Ok(()) } #[tauri::command] fn list_model_library_entries_command(app: AppHandle) -> Result, String> { let app_data_dir = app .path() .app_data_dir() .map_err(|error| format!("Cannot resolve app data dir: {error}"))?; let index_path = model_library_index_path(&app_data_dir); list_entries(&index_path) } #[tauri::command] fn get_model_library_state_command(app: AppHandle) -> Result { let app_data_dir = app .path() .app_data_dir() .map_err(|error| format!("Cannot resolve app data dir: {error}"))?; let index_path = model_library_index_path(&app_data_dir); Ok(ModelLibraryState { entries: list_entries(&index_path)?, managed_dir: managed_models_dir(&app_data_dir).to_string_lossy().to_string(), }) } // ─── App entry ──────────────────────────────────────────────────────────────── #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .manage(RuntimeState::new()) .manage(Arc::new(HuggingFaceDownloadState::default())) .invoke_handler(tauri::generate_handler![ query_gpu, scan_models, pick_models_dir, pick_model_files, import_model, search_hugging_face_models_command, start_hugging_face_download_command, cancel_hugging_face_download_command, swap_model_command, delete_model_file, open_model_folder_command, list_model_library_entries_command, get_model_library_state_command, bootstrap_runtime_command, get_runtime_snapshot_command, save_runtime_config_command_handler, start_runtime_command, stop_runtime_command, get_app_data_dir, find_server_binary, ]) .setup(|app| { #[cfg(debug_assertions)] { let window = app.get_webview_window("main").unwrap(); window.open_devtools(); } Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }