diff --git a/README.md b/README.md index 16b474f..cc1f500 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,29 @@ rustc --version cargo --version ``` +## Starta Applikationen + +För att starta applikationen i utvecklingsläge: + +```bash +cargo tauri dev +``` + +### Felsökning: Linux (Wayland) & "Error 71" +Om du använder Linux med Wayland (t.ex. GNOME eller KDE Plasma) kan du stöta på "Error 71 (Protocol error)" vid start. Detta är en känd bugg relaterad till WebKitGTK och hårdvaruacceleration. + +**Lösning:** +Kör applikationen med inaktiverad kompositering: + +```bash +WEBKIT_DISABLE_COMPOSITING_MODE=1 cargo tauri dev +``` + +Alternativt för vissa NVIDIA-konfigurationer: +```bash +__NV_DISABLE_EXPLICIT_SYNC=1 cargo tauri dev +``` + ### Linux-beroenden (Ubuntu/Debian) För att kompilera Tauri på Linux krävs följande bibliotek: @@ -189,3 +212,13 @@ När du bygger via `cargo-xwin` (se ovan) eller på en Windows-maskin, genereras * `src-tauri/src/main.rs`: Entry point. Innehåller logik för System Tray. * `src-tauri/tauri.conf.json`: Konfiguration för fönster och byggprocess. +## Loggning + +Applikationen har inbyggt stöd för loggning för att underlätta felsökning. + +* **Plats:** Loggfiler sparas i en mapp som heter `loggs` som ligger i samma katalog som den körbara filen. +* **Filnamn:** En loggfil skapas per dag och döps efter dagens datum (t.ex. `2024-01-18.log`). +* **Format:** Loggarna innehåller tidsstämpel, loggnivå (INFO, DEBUG, ERROR), modul/funktion samt meddelandet. Exempel: + `[2024-01-18 10:00:00][INFO][ai_translater_client::main] Application started` +* **Loggade händelser:** Applikationsstart, system tray-händelser, felmeddelanden och annan viktig information loggas. + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 036b296..38cd555 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -22,8 +22,10 @@ name = "ai-translater-client" version = "0.1.0" dependencies = [ "arboard", + "chrono", "enigo", "env_logger", + "fern", "global-hotkey", "log", "ollama-rs", @@ -417,8 +419,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -913,6 +917,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + [[package]] name = "field-offset" version = "0.3.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d121b2e..3296a56 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,3 +30,5 @@ global-hotkey = "0.6" # Systemomfattande genvägar # Logging (Strongly recommended for debugging) log = "0.4" env_logger = "0.11" +fern = "0.6" +chrono = { version = "0.4", features = ["serde"] } diff --git a/src-tauri/src/controllers/app_state.rs b/src-tauri/src/controllers/app_state.rs new file mode 100644 index 0000000..8183624 --- /dev/null +++ b/src-tauri/src/controllers/app_state.rs @@ -0,0 +1,19 @@ +use std::sync::Mutex; +use tauri::{App, Manager, Runtime}; + +pub struct AppState { + #[allow(dead_code)] + pub ollama_ready: Mutex, +} + +impl AppState { + pub fn new() -> Self { + Self { + ollama_ready: Mutex::new(false), + } + } +} + +pub fn init_state(app: &mut App) { + app.manage(AppState::new()); +} diff --git a/src-tauri/src/controllers/greet.rs b/src-tauri/src/controllers/greet.rs new file mode 100644 index 0000000..da35b2e --- /dev/null +++ b/src-tauri/src/controllers/greet.rs @@ -0,0 +1,6 @@ +use tauri::command; + +#[command] +pub fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} diff --git a/src-tauri/src/controllers/mod.rs b/src-tauri/src/controllers/mod.rs new file mode 100644 index 0000000..77efb6f --- /dev/null +++ b/src-tauri/src/controllers/mod.rs @@ -0,0 +1,3 @@ +pub mod app_state; +pub mod settings; +pub mod greet; diff --git a/src-tauri/src/controllers/settings.rs b/src-tauri/src/controllers/settings.rs new file mode 100644 index 0000000..9b204e6 --- /dev/null +++ b/src-tauri/src/controllers/settings.rs @@ -0,0 +1,16 @@ +use tauri::command; +use crate::utilities::config::{AppConfig, load_config, save_config}; + +#[command] +pub fn get_settings() -> Result { + load_config().map_err(|e| e.to_string()) +} + +#[command] +pub fn save_settings(config: AppConfig) -> Result<(), String> { + save_config(&config).map_err(|e| e.to_string())?; + // Note: Re-initializing logging at runtime is complex with fern alone as it sets a global logger. + // Ideally we would have a reloadable logger or just ask user to restart. + // For now we just save. + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index af25332..3ea37d2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,81 +1,60 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use tauri::{ - menu::{Menu, MenuItem}, - tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, - Manager, -}; -use std::sync::Mutex; +mod utilities; +mod controllers; +mod viewers; -// Placeholder for application state -struct AppState { - // Exempel: Spara status för ollama connection här - ollama_ready: Mutex, -} +use log::info; +use utilities::config::load_config; +use utilities::logging::setup_logging; +use controllers::app_state::init_state; +use controllers::settings::{get_settings, save_settings}; +use controllers::greet::greet; +use viewers::tray::setup_tray; #[tokio::main] async fn main() { - // Initiera loggning - env_logger::init(); + // 1. Load configuration + let config = match load_config() { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to load config: {}", e); + // Default config fallback logic is inside load_config, but if file IO fails we might get here. + // We can try to proceed with default logging if possible, but for now just print to stderr. + return; + } + }; + + // 2. Init logging + if let Err(e) = setup_logging(&config) { + eprintln!("Failed to setup logging: {}", e); + } + + info!("Application started"); tauri::Builder::default() + .invoke_handler(tauri::generate_handler![get_settings, save_settings, greet]) .setup(|app| { - // Setup Application State - app.manage(AppState { - ollama_ready: Mutex::new(false), - }); - - // --- System Tray Setup --- + info!("Setting up application..."); - // Skapa menyalternativ - let quit_i = MenuItem::with_id(app, "quit", "Avsluta", true, None::<&str>)?; - let settings_i = MenuItem::with_id(app, "settings", "Inställningar", true, None::<&str>)?; - - // Skapa menyn - let menu = Menu::with_items(app, &[&settings_i, &quit_i])?; + // 3. Init State + init_state(app); - // Bygg Tray-ikonen - let _tray = TrayIconBuilder::new() - .icon(app.default_window_icon().unwrap().clone()) - .menu(&menu) - .show_menu_on_left_click(false) - .on_menu_event(|app, event| { - match event.id.as_ref() { - "quit" => { - println!("Avslutar applikationen..."); - app.exit(0); - } - "settings" => { - println!("Öppnar inställningar (Placeholder)..."); - // Här kan du öppna ditt inställningsfönster: - // if let Some(window) = app.get_webview_window("main") { - // window.show().unwrap(); - // window.set_focus().unwrap(); - // } - } - _ => {} - } - }) - .on_tray_icon_event(|tray, event| { - match event { - TrayIconEvent::Click { - button: MouseButton::Left, - .. - } => { - let app = tray.app_handle(); - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - } - } - _ => {} - } - }) - .build(app)?; + // 4. Setup Tray + setup_tray(app)?; Ok(()) }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Prevent the window from closing (destroying) + // Instead, hide it. This keeps the app running in the tray. + window.hide().unwrap(); + api.prevent_close(); + } + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } + diff --git a/src-tauri/src/utilities/config.rs b/src-tauri/src/utilities/config.rs new file mode 100644 index 0000000..649303c --- /dev/null +++ b/src-tauri/src/utilities/config.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct LogConfig { + pub path: String, + pub level: String, + pub enabled: bool, +} + +impl Default for LogConfig { + fn default() -> Self { + Self { + path: "loggs".to_string(), + level: "debug".to_string(), + enabled: true, + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct GeneralConfig { + pub theme: String, +} + +impl Default for GeneralConfig { + fn default() -> Self { + Self { + theme: "dark".to_string(), + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AppConfig { + #[serde(default)] + pub logging: LogConfig, + #[serde(default)] + pub general: GeneralConfig, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + logging: LogConfig::default(), + general: GeneralConfig::default(), + } + } +} + +pub fn load_config() -> Result> { + let exe_path = std::env::current_exe()?; + let exe_dir = exe_path.parent().ok_or("Could not find exe directory")?; + + let config_path = exe_dir.join("config.json"); + if config_path.exists() { + let content = std::fs::read_to_string(&config_path)?; + Ok(serde_json::from_str(&content).unwrap_or_else(|_| AppConfig::default())) + } else { + let config = AppConfig::default(); + if let Ok(content) = serde_json::to_string_pretty(&config) { + let _ = std::fs::write(&config_path, content); + } + Ok(config) + } +} + +pub fn save_config(config: &AppConfig) -> Result<(), Box> { + let exe_path = std::env::current_exe()?; + let exe_dir = exe_path.parent().ok_or("Could not find exe directory")?; + let config_path = exe_dir.join("config.json"); + + let content = serde_json::to_string_pretty(config)?; + std::fs::write(config_path, content)?; + Ok(()) +} diff --git a/src-tauri/src/utilities/logging.rs b/src-tauri/src/utilities/logging.rs new file mode 100644 index 0000000..5deae53 --- /dev/null +++ b/src-tauri/src/utilities/logging.rs @@ -0,0 +1,45 @@ +use log::LevelFilter; +use std::str::FromStr; +use super::config::AppConfig; + +pub fn setup_logging(config: &AppConfig) -> Result<(), Box> { + if !config.logging.enabled { + return Ok(()); + } + + let exe_path = std::env::current_exe()?; + let exe_dir = exe_path.parent().ok_or("Could not find exe directory")?; + + let log_path_config = std::path::Path::new(&config.logging.path); + let log_dir = if log_path_config.is_absolute() { + log_path_config.to_path_buf() + } else { + exe_dir.join(log_path_config) + }; + + if !log_dir.exists() { + std::fs::create_dir_all(&log_dir)?; + } + + let file_name = chrono::Local::now().format("%Y-%m-%d.log").to_string(); + let log_path = log_dir.join(file_name); + + let level_filter = LevelFilter::from_str(&config.logging.level).unwrap_or(LevelFilter::Debug); + + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}][{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + record.target(), + message + )) + }) + .level(level_filter) + .chain(std::io::stdout()) + .chain(fern::log_file(log_path)?) + .apply()?; + + Ok(()) +} diff --git a/src-tauri/src/utilities/mod.rs b/src-tauri/src/utilities/mod.rs new file mode 100644 index 0000000..8b0e66a --- /dev/null +++ b/src-tauri/src/utilities/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod logging; diff --git a/src-tauri/src/viewers/mod.rs b/src-tauri/src/viewers/mod.rs new file mode 100644 index 0000000..7047e70 --- /dev/null +++ b/src-tauri/src/viewers/mod.rs @@ -0,0 +1 @@ +pub mod tray; diff --git a/src-tauri/src/viewers/tray.rs b/src-tauri/src/viewers/tray.rs new file mode 100644 index 0000000..358d4eb --- /dev/null +++ b/src-tauri/src/viewers/tray.rs @@ -0,0 +1,96 @@ +use tauri::{ + menu::{Menu, MenuItem}, + tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, + App, Manager, Runtime, WebviewWindowBuilder, WebviewUrl, +}; +use log::{info, error}; + +fn toggle_settings_window(app: &tauri::AppHandle) { + match app.get_webview_window("settings") { + Some(window) => { + info!("Settings window found"); + if let Ok(true) = window.is_visible() { + info!("Window is visible, hiding..."); + let _ = window.hide(); + } else { + info!("Window is hidden, showing..."); + // On checking docs/issues: Some Wayland compositors dislike set_focus or show on hidden windows + // Creating the window 'visible' from start is safer. + let _ = window.show(); + // window.set_focus(); - Removed to prevent Wayland protocol error 71 + } + } + None => { + info!("Creating new settings window..."); + let build_result = WebviewWindowBuilder::new( + app, + "settings", + WebviewUrl::App("index.html".into()) + ) + .title("AI Typist Inställningar") + .inner_size(800.0, 600.0) + .visible(false) // Create hidden first to avoid Wayland focus issues on creation + .build(); + + match build_result { + Ok(window) => { + info!("Settings window created successfully"); + // Now show it explicitly + if let Err(e) = window.show() { + error!("Failed to show window: {}", e); + } + }, + Err(e) => error!("Failed to create settings window: {}", e), + } + } + } +} + +pub fn setup_tray(app: &mut App) -> Result<(), Box> { + // Settings window is now created via tauri.conf.json to ensure correct init context on Wayland + + // Skapa menyalternativ + let quit_i = MenuItem::with_id(app, "quit", "Avsluta", true, None::<&str>)?; + let settings_i = MenuItem::with_id(app, "settings", "Inställningar", true, None::<&str>)?; + + // Skapa menyn + let menu = Menu::with_items(app, &[&settings_i, &quit_i])?; + + info!("Tray menu created"); + + // Bygg Tray-ikonen + let _tray = TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| { + match event.id.as_ref() { + "quit" => { + info!("User clicked quit from tray"); + println!("Avslutar applikationen..."); + app.exit(0); + } + "settings" => { + info!("User clicked settings from tray"); + toggle_settings_window(app); + } + _ => {} + } + }) + .on_tray_icon_event(|tray, event| { + match event { + TrayIconEvent::Click { + button: MouseButton::Left, + .. + } => { + let app = tray.app_handle(); + toggle_settings_window(app); + } + _ => {} + } + }) + .build(app)?; + + info!("Tray system initialized successfully"); + Ok(()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8ea7118..053cb5a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,12 +8,15 @@ "frontendDist": "../dist" }, "app": { + "withGlobalTauri": true, "windows": [ { + "label": "settings", "title": "AI Typist Inställningar", + "url": "index.html", "width": 800, "height": 600, - "visible": false + "visible": true } ], "security": {