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

13
src-tauri/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

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!!
#![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<bool>,
}
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");
}

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"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "settings",
"title": "AI Typist Inställningar",
"url": "index.html",
"width": 800,
"height": 600,
"visible": false
"visible": true
}
],
"security": {