feat: Enhance application with logging, configuration management, and system tray support

first working gui windows with configs
This commit is contained in:
2026-01-18 21:36:59 +01:00
parent cb14fe0989
commit efbbccb36f
14 changed files with 357 additions and 64 deletions

View File

@@ -51,6 +51,29 @@ rustc --version
cargo --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) ### Linux-beroenden (Ubuntu/Debian)
För att kompilera Tauri på Linux krävs följande bibliotek: 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/src/main.rs`: Entry point. Innehåller logik för System Tray.
* `src-tauri/tauri.conf.json`: Konfiguration för fönster och byggprocess. * `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.

13
src-tauri/Cargo.lock generated
View File

@@ -22,8 +22,10 @@ name = "ai-translater-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arboard", "arboard",
"chrono",
"enigo", "enigo",
"env_logger", "env_logger",
"fern",
"global-hotkey", "global-hotkey",
"log", "log",
"ollama-rs", "ollama-rs",
@@ -417,8 +419,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
@@ -913,6 +917,15 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "fern"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
dependencies = [
"log",
]
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"

View File

@@ -30,3 +30,5 @@ global-hotkey = "0.6" # Systemomfattande genvägar
# Logging (Strongly recommended for debugging) # Logging (Strongly recommended for debugging)
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
fern = "0.6"
chrono = { version = "0.4", features = ["serde"] }

View File

@@ -0,0 +1,19 @@
use std::sync::Mutex;
use tauri::{App, Manager, Runtime};
pub struct AppState {
#[allow(dead_code)]
pub ollama_ready: Mutex<bool>,
}
impl AppState {
pub fn new() -> Self {
Self {
ollama_ready: Mutex::new(false),
}
}
}
pub fn init_state<R: Runtime>(app: &mut App<R>) {
app.manage(AppState::new());
}

View File

@@ -0,0 +1,6 @@
use tauri::command;
#[command]
pub fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

View File

@@ -0,0 +1,3 @@
pub mod app_state;
pub mod settings;
pub mod greet;

View File

@@ -0,0 +1,16 @@
use tauri::command;
use crate::utilities::config::{AppConfig, load_config, save_config};
#[command]
pub fn get_settings() -> Result<AppConfig, String> {
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(())
}

View File

@@ -1,81 +1,60 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::{ mod utilities;
menu::{Menu, MenuItem}, mod controllers;
tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, mod viewers;
Manager,
};
use std::sync::Mutex;
// Placeholder for application state use log::info;
struct AppState { use utilities::config::load_config;
// Exempel: Spara status för ollama connection här use utilities::logging::setup_logging;
ollama_ready: Mutex<bool>, use controllers::app_state::init_state;
} use controllers::settings::{get_settings, save_settings};
use controllers::greet::greet;
use viewers::tray::setup_tray;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Initiera loggning // 1. Load configuration
env_logger::init(); 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() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![get_settings, save_settings, greet])
.setup(|app| { .setup(|app| {
// Setup Application State info!("Setting up application...");
app.manage(AppState {
ollama_ready: Mutex::new(false),
});
// --- System Tray Setup --- // 3. Init State
init_state(app);
// Skapa menyalternativ // 4. Setup Tray
let quit_i = MenuItem::with_id(app, "quit", "Avsluta", true, None::<&str>)?; setup_tray(app)?;
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])?;
// 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)?;
Ok(()) 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!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -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<AppConfig, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}

View File

@@ -0,0 +1,45 @@
use log::LevelFilter;
use std::str::FromStr;
use super::config::AppConfig;
pub fn setup_logging(config: &AppConfig) -> Result<(), Box<dyn std::error::Error>> {
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(())
}

View File

@@ -0,0 +1,2 @@
pub mod config;
pub mod logging;

View File

@@ -0,0 +1 @@
pub mod tray;

View File

@@ -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<R: Runtime>(app: &tauri::AppHandle<R>) {
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<R: Runtime>(app: &mut App<R>) -> Result<(), Box<dyn std::error::Error>> {
// 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(())
}

View File

@@ -8,12 +8,15 @@
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
"app": { "app": {
"withGlobalTauri": true,
"windows": [ "windows": [
{ {
"label": "settings",
"title": "AI Typist Inställningar", "title": "AI Typist Inställningar",
"url": "index.html",
"width": 800, "width": 800,
"height": 600, "height": 600,
"visible": false "visible": true
} }
], ],
"security": { "security": {