Compare commits
3 Commits
cb14fe0989
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e700fb867 | |||
| e90cf242bd | |||
| efbbccb36f |
5
.gitignore
vendored
@@ -7,10 +7,11 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# VS Code
|
# VS Code - Ignore everything but project-specific config
|
||||||
.vscode/
|
.vscode/*
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# Node / Frontend (om det läggs till senare)
|
# Node / Frontend (om det läggs till senare)
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"vadimcn.vscode-lldb",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Tauri App",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/src-tauri/target/debug/ai-translater-client",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src-tauri",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"env": {
|
||||||
|
"WEBKIT_DISABLE_COMPOSITING_MODE": "1",
|
||||||
|
// "GDK_BACKEND": "x11" // Uncomment if the above doesn't fix the initialization error
|
||||||
|
},
|
||||||
|
"sourceLanguages": ["rust"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
69
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cargo",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--manifest-path=./src-tauri/Cargo.toml",
|
||||||
|
"--no-default-features"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build Release (Linux)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cargo",
|
||||||
|
"args": [
|
||||||
|
"tauri",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src-tauri"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build Release (Windows)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cargo",
|
||||||
|
"args": [
|
||||||
|
"tauri",
|
||||||
|
"build",
|
||||||
|
"--target",
|
||||||
|
"x86_64-pc-windows-msvc"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src-tauri"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "tauri dev",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cargo",
|
||||||
|
"args": [
|
||||||
|
"tauri",
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/src-tauri",
|
||||||
|
"env": {
|
||||||
|
"WEBKIT_DISABLE_COMPOSITING_MODE": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
237
README.md
@@ -1,62 +1,21 @@
|
|||||||
# AI Typist Client
|
# AI Typist Client
|
||||||
|
|
||||||
En cross-platform skrivbordsapplikation utvecklad i Rust och Tauri v2 för Windows 11 och Linux (med Wayland-stöd). Applikationen fungerar som en smart AI-assistent som integrerar sömlöst med ditt arbetsflöde via system-tray, globala genvägar och urklippshantering.
|
En smart skrivbordsassistent utvecklad i Rust och Tauri v2. Applikationen hjälper dig att översätta, rättstava och förbättra text i realtid genom att integrera lokala AI-modeller (Ollama) eller molntjänster (OpenAI) direkt i ditt arbetsflöde.
|
||||||
|
|
||||||
## Projektbeskrivning
|
## 🚀 Komma Igång
|
||||||
|
|
||||||
Målet med detta projekt är att skapa ett verktyg som kan förbättra, rättstava eller översätta text i alla applikationer genom att utnyttja lokala LLM:er (via Ollama) eller molnbaserade API:er (OpenAI).
|
### Förutsättningar
|
||||||
|
|
||||||
### Kärnfunktionalitet
|
För att utveckla och bygga applikationen behöver du följande installerat:
|
||||||
|
|
||||||
1. **System Tray Integration**:
|
1. **Rust & Cargo**:
|
||||||
* Applikationen körs i bakgrunden med en ikon i statusfältet/systemfältet.
|
```bash
|
||||||
* Högerklick ger en meny för att komma åt inställningar eller avsluta appen.
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
```
|
||||||
2. **AI-Workflow**:
|
2. **System-beroenden (Linux/Debian/Ubuntu)**:
|
||||||
* **Input**: Appen hämtar text direkt från systemets urklipp (clipboard).
|
```bash
|
||||||
* **Bearbetning**: Texten bakas in i en prompt och skickas till en LLM (Ollama/OpenAI) för översättning eller rättstavning.
|
sudo apt update
|
||||||
* **Output**: Det bearbetade svaret kan antingen:
|
sudo apt install libwebkit2gtk-4.1-dev \
|
||||||
* Kopieras tillbaka till urklipp.
|
|
||||||
* Skrivas ut direkt i det aktiva fönstret genom att imitera tangentbordstryckningar (Keyboard Mimicry).
|
|
||||||
|
|
||||||
3. **Teknisk Stack**:
|
|
||||||
* **Språk**: Rust
|
|
||||||
* **GUI/Ramverk**: Tauri v2
|
|
||||||
* **Tangentbord/Input**: `enigo` (mimic) & `global-hotkey` (lyssna)
|
|
||||||
* **Urklipp**: `arboard`
|
|
||||||
* **AI-Kommunikation**: `reqwest` & `ollama-rs`
|
|
||||||
|
|
||||||
## Förutsättningar
|
|
||||||
|
|
||||||
Du behöver Rust installerat (rekommenderat via `rustup`).
|
|
||||||
|
|
||||||
**Installera Rust (Linux/macOS):**
|
|
||||||
```bash
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
||||||
# Följ instruktionerna på skärmen (standardinstallation är oftast bäst)
|
|
||||||
source $HOME/.cargo/env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Felsökning: "cannot install while Rust is installed"**
|
|
||||||
Om du får detta felmeddelande har du redan Rust installerat via systemets pakethanterare (vilket sällan stöder cross-compilation smidigt).
|
|
||||||
|
|
||||||
*Lösning:* Avinstallera system-versionen och kör scriptet igen.
|
|
||||||
|
|
||||||
* **Arch Linux:** `sudo pacman -Rs rust` (Kolla även om du har `cargo` installerat separat)
|
|
||||||
* **Ubuntu/Debian:** `sudo apt remove rustc cargo`
|
|
||||||
|
|
||||||
**Verifiera installationen:**
|
|
||||||
```bash
|
|
||||||
rustc --version
|
|
||||||
cargo --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux-beroenden (Ubuntu/Debian)
|
|
||||||
För att kompilera Tauri på Linux krävs följande bibliotek:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install libwebkit2gtk-4.1-dev \
|
|
||||||
build-essential \
|
build-essential \
|
||||||
curl \
|
curl \
|
||||||
wget \
|
wget \
|
||||||
@@ -65,127 +24,87 @@ sudo apt install libwebkit2gtk-4.1-dev \
|
|||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libayatana-appindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
librsvg2-dev
|
librsvg2-dev
|
||||||
```
|
|
||||||
|
|
||||||
### Linux-beroenden (Arch Linux / Manjaro)
|
|
||||||
```bash
|
|
||||||
sudo pacman -Syu
|
|
||||||
sudo pacman -S webkit2gtk-4.1 \
|
|
||||||
base-devel \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
file \
|
|
||||||
openssl \
|
|
||||||
gtk3 \
|
|
||||||
libayatana-appindicator \
|
|
||||||
librsvg
|
|
||||||
```
|
|
||||||
|
|
||||||
*Obs: För Wayland-stöd hanterar GTK3/4 detta oftast automatiskt, men säkerställ att du kör en modern distribution.*
|
|
||||||
|
|
||||||
## Utveckling
|
|
||||||
|
|
||||||
### Installera Tauri CLI
|
|
||||||
```bash
|
|
||||||
cargo install tauri-cli --version "^2.0.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kör i utvecklingsläge
|
|
||||||
Gå in i `src-tauri` mappen eller kör från roten om konfigurerat korrekt, men standard är:
|
|
||||||
```bash
|
|
||||||
cd src-tauri
|
|
||||||
cargo tauri dev
|
|
||||||
```
|
|
||||||
Detta kommer starta applikationen. Eftersom vi inte har en frontend (HTML/JS) ännu, kommer fönstret vara tomt eller vitt, men System Tray-ikonen ska synas.
|
|
||||||
|
|
||||||
## Debugging med VS Code (CodeLLDB)
|
|
||||||
|
|
||||||
För att debugga Rust-koden effektivt i VS Code, skapa filen `.vscode/launch.json` i roten av projektet med följande innehåll. Detta binder CodeLLDB till Tauri-processen.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Debug Tauri App",
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "${workspaceFolder}/src-tauri/target/debug/ai-translater-client",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}/src-tauri",
|
|
||||||
"preLaunchTask": "build",
|
|
||||||
"sourceLanguages": ["rust"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cross-Compilation: Windows 11 från Linux
|
|
||||||
|
|
||||||
Att bygga Windows `.exe` från Linux är fullt möjligt med `cargo-xwin`.
|
|
||||||
|
|
||||||
1. **Installera cross-kompileringsverktyg:**
|
|
||||||
|
|
||||||
**Ubuntu/Debian:**
|
|
||||||
```bash
|
|
||||||
sudo apt install mingw-w64
|
|
||||||
```
|
```
|
||||||
|
3. **Cross-compilation (Windows från Linux)**:
|
||||||
**Arch Linux:**
|
Om du vill bygga för Windows från Linux behöver du `nsis` samt Rust-målet för Windows:
|
||||||
```bash
|
|
||||||
sudo pacman -S mingw-w64 clang lld
|
|
||||||
# Välj "alla" (tryck Enter) om du tillfrågas om medlemmar i gruppen
|
|
||||||
```
|
|
||||||
|
|
||||||
**Alla:**
|
|
||||||
```bash
|
|
||||||
cargo install cargo-xwin
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Lägg till Windows target:**
|
|
||||||
```bash
|
```bash
|
||||||
rustup target add x86_64-pc-windows-msvc
|
rustup target add x86_64-pc-windows-msvc
|
||||||
|
# Observera att cross-compilation med MSVC kan kräva ytterligare konfiguration (libs).
|
||||||
|
# Alternativt använd x86_64-pc-windows-gnu om du har Mingw installerat.
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Bygg för Windows:**
|
---
|
||||||
Stå i `src-tauri` mappen och kör:
|
|
||||||
```bash
|
|
||||||
cargo tauri build --target x86_64-pc-windows-msvc --runner cargo-xwin
|
|
||||||
```
|
|
||||||
|
|
||||||
Resultatet (exe & msi) hamnar i `src-tauri/target/x86_64-pc-windows-msvc/release/bundle/`.
|
## 💻 Utveckling i VS Code
|
||||||
|
|
||||||
## Windows-testning på Linux (Wine/Proton)
|
Det här projektet är konfigurerat för en smidig upplevelse i VS Code.
|
||||||
|
|
||||||
För att snabbt verifiera att Windows-bygget startar utan att byta OS:
|
### Rekommenderade Tillägg
|
||||||
|
VS Code kommer automatiskt rekommendera dessa, men se till att du har:
|
||||||
|
* **rust-analyzer** (Rust språkstöd)
|
||||||
|
* **Tauri** (Tauri verktyg)
|
||||||
|
* **CodeLLDB** (För debugging)
|
||||||
|
|
||||||
1. **Installera Wine:**
|
### Debugging (Felsökning)
|
||||||
```bash
|
Du kan köra och debugga appen direkt inifrån redigeraren:
|
||||||
sudo apt install wine64
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Kör applikationen:**
|
1. Gå till **Run and Debug** fliken (Ctrl+Shift+D).
|
||||||
Navigera till output-mappen från steget ovan och kör:
|
2. Välj **"Debug Tauri App"** i menyn.
|
||||||
```bash
|
3. Tryck **F5** (Play).
|
||||||
wine "ai-translater-client.exe"
|
* *Detta bygger appen och startar debuggern automatiskt. Du kan sätta breakpoints i din Rust-kod.*
|
||||||
```
|
|
||||||
*Notera: System Tray kan bete sig annorlunda i Wine än i äkta Windows.*
|
|
||||||
|
|
||||||
## Packaging & Distribution
|
### Bygga för Release 📦
|
||||||
|
För att skapa färdiga körbara filer (binärer) utan debug-info:
|
||||||
|
|
||||||
Tauri hanterar paketering automatiskt baserat på ditt OS.
|
1. Öppna **Command Palette** (Ctrl+Shift+P).
|
||||||
|
2. Skriv och välj **"Tasks: Run Task"**.
|
||||||
|
3. Välj en av följande:
|
||||||
|
* **Build Release (Linux)**: Skapar en optimerad build för Linux.
|
||||||
|
* *Resultat:* `src-tauri/target/release/bundle/deb/` (eller AppImage)
|
||||||
|
* **Build Release (Windows)**: Skapar en `.exe` för Windows.
|
||||||
|
* *Resultat:* `src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/` (eller nsis)
|
||||||
|
|
||||||
### Linux (.deb & .AppImage)
|
---
|
||||||
|
|
||||||
|
## 📂 Projektstruktur
|
||||||
|
|
||||||
|
Så här hittar du i koden:
|
||||||
|
|
||||||
|
* **`src-tauri/`**: Backend-koden (Rust) och Tauri-konfigurationen.
|
||||||
|
* `Cargo.toml`: Beroenden för Rust.
|
||||||
|
* `tauri.conf.json`: Inställningar för fönster, ikoner, behörigheter och bundles.
|
||||||
|
* `build.rs`: Byggscript.
|
||||||
|
* **`src/`**: Källkoden för backend.
|
||||||
|
* `main.rs`: Entry point.
|
||||||
|
* `controllers/`: Logik för applikationens tillstånd och kommandon.
|
||||||
|
* `viewers/`: Hantering av fönster och System Tray.
|
||||||
|
* `utilities/`: Hjälpfunktioner (loggning, config).
|
||||||
|
* **`dist/`**: (Frontend) Om du har en frontend (HTML/JS/CSS) ligger de kompilerade filerna här som Tauri laddar.
|
||||||
|
* **`.vscode/`**: Inställningar för debugging och tasks i VS Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Felsökning
|
||||||
|
|
||||||
|
### Error 71 (Protocol Error) på Linux (Wayland)
|
||||||
|
Om appen kraschar eller inte startar på Wayland:
|
||||||
|
Debuggern är konfigurerad att automatiskt sätta `WEBKIT_DISABLE_COMPOSITING_MODE=1`.
|
||||||
|
Om du kör manuellt från terminalen:
|
||||||
```bash
|
```bash
|
||||||
cargo tauri build
|
WEBKIT_DISABLE_COMPOSITING_MODE=1 cargo tauri dev
|
||||||
```
|
```
|
||||||
Filerna genereras i `src-tauri/target/release/bundle/deb/` och `appimage/`.
|
|
||||||
|
|
||||||
### Windows (.msi & .exe)
|
### Debuggern startar inte
|
||||||
När du bygger via `cargo-xwin` (se ovan) eller på en Windows-maskin, genereras en `.msi` via WiX Toolset (om installerat) eller en `.exe` setup-fil via NSIS (standard i v2).
|
* Kontrollera att du installerat **CodeLLDB** tillägget.
|
||||||
|
* Om du får fel vid länkning, kontrollera att du har alla system-beroenden installerade (se "Förutsättningar").
|
||||||
|
|
||||||
## Projektstruktur
|
---
|
||||||
* `src-tauri/Cargo.toml`: Alla beroenden (tauri, enigo, ollama-rs, etc).
|
|
||||||
* `src-tauri/src/main.rs`: Entry point. Innehåller logik för System Tray.
|
## 🧠 Teknisk Stack
|
||||||
* `src-tauri/tauri.conf.json`: Konfiguration för fönster och byggprocess.
|
|
||||||
|
* **Core**: Rust
|
||||||
|
* **Framework**: Tauri v2
|
||||||
|
* **Input Monitoring**: `enigo` (tangentbords-simulering), `global-hotkey` (genvägar)
|
||||||
|
* **Clipboard**: `arboard`
|
||||||
|
* **AI**: `ollama-rs` (lokal) & `reqwest` (API)
|
||||||
|
|
||||||
|
|||||||
15
src-tauri/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Tauri App",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/src-tauri/target/debug/ai-translater-client",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src-tauri",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"sourceLanguages": ["rust"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
src-tauri/Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
33
src-tauri/src/controllers/app_state.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::sync::{Mutex, Arc};
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use tauri::{App, Manager, Runtime};
|
||||||
|
use arboard::Clipboard;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub ollama_ready: Mutex<bool>,
|
||||||
|
pub clipboard: Mutex<Option<Clipboard>>,
|
||||||
|
pub should_stop: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let clipboard = match Clipboard::new() {
|
||||||
|
Ok(cb) => Some(cb),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to initialize global clipboard: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ollama_ready: Mutex::new(false),
|
||||||
|
clipboard: Mutex::new(clipboard),
|
||||||
|
should_stop: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_state<R: Runtime>(app: &mut App<R>) {
|
||||||
|
app.manage(AppState::new());
|
||||||
|
}
|
||||||
6
src-tauri/src/controllers/greet.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use tauri::command;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
|
}
|
||||||
35
src-tauri/src/controllers/logs.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use tauri::command;
|
||||||
|
use crate::utilities::config::load_config;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub fn get_log_content() -> Result<String, String> {
|
||||||
|
let config = load_config().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Logic duplicated from proper logging setup, ideally should be shared or stored in state
|
||||||
|
// But for now, we re-resolve the path
|
||||||
|
let exe_path = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||||
|
let exe_dir = exe_path.parent().ok_or("Could not find exe directory")?;
|
||||||
|
|
||||||
|
let log_path_config = 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() {
|
||||||
|
return Ok("No log directory found.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest log file (assuming today's date)
|
||||||
|
let file_name = chrono::Local::now().format("%Y-%m-%d.log").to_string();
|
||||||
|
let log_path = log_dir.join(file_name);
|
||||||
|
|
||||||
|
if log_path.exists() {
|
||||||
|
fs::read_to_string(log_path).map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
Ok("No log file for today yet.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src-tauri/src/controllers/manual_test.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use tauri::{command, AppHandle};
|
||||||
|
use crate::utilities::ai_handler::process_ai_task_with_text;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn run_manual_test(app: AppHandle, task_type: String, text: String) -> Result<(), String> {
|
||||||
|
process_ai_task_with_text(app, task_type, text).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
6
src-tauri/src/controllers/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod app_state;
|
||||||
|
pub mod settings;
|
||||||
|
pub mod greet;
|
||||||
|
pub mod ollama;
|
||||||
|
pub mod logs;
|
||||||
|
pub mod manual_test;
|
||||||
59
src-tauri/src/controllers/ollama.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use tauri::command;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use log::{info, error};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct OllamaModel {
|
||||||
|
pub name: String,
|
||||||
|
// other fields ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct OllamaTagsResponse {
|
||||||
|
pub models: Vec<OllamaModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn test_ollama_connection(url: String) -> Result<String, String> {
|
||||||
|
info!("Testing Ollama connection to {}", url);
|
||||||
|
let client = Client::new();
|
||||||
|
let res = client.get(&url).send().await.map_err(|e| {
|
||||||
|
error!("Connection test failed: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let text = res.text().await.map_err(|e| e.to_string())?;
|
||||||
|
info!("Connection OK");
|
||||||
|
Ok(text)
|
||||||
|
} else {
|
||||||
|
error!("Connection test failed with status: {}", res.status());
|
||||||
|
Err(format!("Status: {}", res.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn fetch_ollama_models(url: String) -> Result<Vec<String>, String> {
|
||||||
|
let client = Client::new();
|
||||||
|
let tags_url = format!("{}/api/tags", url.trim_end_matches('/'));
|
||||||
|
info!("Fetching models from {}", tags_url);
|
||||||
|
|
||||||
|
let res = client.get(&tags_url).send().await.map_err(|e| {
|
||||||
|
error!("Fetch models failed: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let parsed: OllamaTagsResponse = res.json().await.map_err(|e| {
|
||||||
|
error!("Fetch models parse error: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
let names = parsed.models.into_iter().map(|m| m.name).collect();
|
||||||
|
info!("Fetched models successfully");
|
||||||
|
Ok(names)
|
||||||
|
} else {
|
||||||
|
error!("Fetch models failed with status: {}", res.status());
|
||||||
|
Err(format!("Failed to fetch models: {}", res.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src-tauri/src/controllers/settings.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -1,81 +1,128 @@
|
|||||||
// 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 utilities::ai_handler::{parse_hotkey, handle_ai_task};
|
||||||
}
|
use controllers::app_state::init_state;
|
||||||
|
use controllers::settings::{get_settings, save_settings};
|
||||||
|
use controllers::greet::greet;
|
||||||
|
use controllers::ollama::{test_ollama_connection, fetch_ollama_models};
|
||||||
|
use controllers::logs::get_log_content;
|
||||||
|
use controllers::manual_test::run_manual_test;
|
||||||
|
use viewers::tray::setup_tray;
|
||||||
|
use global_hotkey::{GlobalHotKeyManager, GlobalHotKeyEvent};
|
||||||
|
|
||||||
#[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,
|
||||||
|
test_ollama_connection,
|
||||||
|
fetch_ollama_models,
|
||||||
|
get_log_content,
|
||||||
|
run_manual_test
|
||||||
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Setup Application State
|
info!("Setting up application...");
|
||||||
app.manage(AppState {
|
|
||||||
ollama_ready: Mutex::new(false),
|
// 3. Init State
|
||||||
|
init_state(app);
|
||||||
|
|
||||||
|
// 4. Setup Tray
|
||||||
|
setup_tray(app)?;
|
||||||
|
|
||||||
|
// 5. Setup Hotkeys
|
||||||
|
let config = load_config().unwrap_or_default();
|
||||||
|
// Initialize manager and leak it to keep it alive for the session
|
||||||
|
let manager = GlobalHotKeyManager::new().expect("Failed to init GlobalHotKeyManager");
|
||||||
|
|
||||||
|
use log::info; // Ensure info is imported
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use controllers::app_state::AppState;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
// ... inside setup ...
|
||||||
|
let mut key_map = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
if let Some(k) = parse_hotkey(&config.keybindings.speling) {
|
||||||
|
if let Ok(_) = manager.register(k) { key_map.insert(k.id(), "speling".to_string()); }
|
||||||
|
}
|
||||||
|
if let Some(k) = parse_hotkey(&config.keybindings.summarize) {
|
||||||
|
if let Ok(_) = manager.register(k) { key_map.insert(k.id(), "summarize".to_string()); }
|
||||||
|
}
|
||||||
|
if let Some(k) = parse_hotkey(&config.keybindings.translate) {
|
||||||
|
if let Ok(_) = manager.register(k) { key_map.insert(k.id(), "translate".to_string()); }
|
||||||
|
}
|
||||||
|
if let Some(k) = parse_hotkey(&config.keybindings.stop) {
|
||||||
|
if let Ok(_) = manager.register(k) { key_map.insert(k.id(), "stop".to_string()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep manager alive
|
||||||
|
Box::leak(Box::new(manager));
|
||||||
|
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
|
||||||
|
// Spawn Hotkey Listener Thread
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let receiver = GlobalHotKeyEvent::receiver();
|
||||||
|
loop {
|
||||||
|
if let Ok(event) = receiver.recv() {
|
||||||
|
if event.state == global_hotkey::HotKeyState::Released {
|
||||||
|
if let Some(action) = key_map.get(&event.id) {
|
||||||
|
if action == "stop" {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
state.should_stop.store(true, Ordering::Relaxed);
|
||||||
|
info!("Stop signal received by hotkey.");
|
||||||
|
} else {
|
||||||
|
let action_clone = action.clone();
|
||||||
|
let app_handle_clone = app_handle.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
handle_ai_task(app_handle_clone, action_clone).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- System Tray Setup ---
|
|
||||||
|
|
||||||
// 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])?;
|
|
||||||
|
|
||||||
// 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
259
src-tauri/src/utilities/ai_handler.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
use global_hotkey::hotkey::{HotKey, Modifiers, Code};
|
||||||
|
use crate::utilities::config::{load_config, save_config}; // Import save_config
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde_json::json;
|
||||||
|
use log::{info, error, debug, warn}; // Import warn
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use crate::controllers::app_state::AppState;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use enigo::{Enigo, Settings, Keyboard};
|
||||||
|
|
||||||
|
pub fn parse_hotkey(hotkey_str: &str) -> Option<HotKey> {
|
||||||
|
let parts: Vec<&str> = hotkey_str.split('+').map(|s| s.trim()).collect();
|
||||||
|
if parts.is_empty() { return None; }
|
||||||
|
|
||||||
|
let mut mods = Modifiers::empty();
|
||||||
|
let mut code = Code::KeyA;
|
||||||
|
let mut code_found = false;
|
||||||
|
|
||||||
|
for part in parts {
|
||||||
|
match part.to_lowercase().as_str() {
|
||||||
|
"ctrl" | "control" => mods |= Modifiers::CONTROL,
|
||||||
|
"shift" => mods |= Modifiers::SHIFT,
|
||||||
|
"alt" => mods |= Modifiers::ALT,
|
||||||
|
"super" | "cmd" | "meta" => mods |= Modifiers::META,
|
||||||
|
key => {
|
||||||
|
if key.len() == 1 {
|
||||||
|
let c = key.chars().next().unwrap();
|
||||||
|
if c.is_alphabetic() {
|
||||||
|
code = match c.to_ascii_uppercase() {
|
||||||
|
'A' => Code::KeyA, 'B' => Code::KeyB, 'C' => Code::KeyC, 'D' => Code::KeyD, 'E' => Code::KeyE,
|
||||||
|
'F' => Code::KeyF, 'G' => Code::KeyG, 'H' => Code::KeyH, 'I' => Code::KeyI, 'J' => Code::KeyJ,
|
||||||
|
'K' => Code::KeyK, 'L' => Code::KeyL, 'M' => Code::KeyM, 'N' => Code::KeyN, 'O' => Code::KeyO,
|
||||||
|
'P' => Code::KeyP, 'Q' => Code::KeyQ, 'R' => Code::KeyR, 'S' => Code::KeyS, 'T' => Code::KeyT,
|
||||||
|
'U' => Code::KeyU, 'V' => Code::KeyV, 'W' => Code::KeyW, 'X' => Code::KeyX, 'Y' => Code::KeyY,
|
||||||
|
'Z' => Code::KeyZ,
|
||||||
|
_ => continue
|
||||||
|
};
|
||||||
|
code_found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code_found {
|
||||||
|
Some(HotKey::new(Some(mods), code))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_ai_task(app: AppHandle, task_type: String) {
|
||||||
|
let text;
|
||||||
|
{
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
// Reset stop signal at start of task
|
||||||
|
state.should_stop.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
|
let mut clipboard_guard = state.clipboard.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(ref mut cb) = *clipboard_guard {
|
||||||
|
text = match cb.get_text() {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Clipboard Read Error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
error!("Global clipboard not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if text.trim().is_empty() { return; }
|
||||||
|
|
||||||
|
process_ai_task_with_text(app, task_type, text).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_ai_task_with_text(app: AppHandle, task_type: String, text: String) {
|
||||||
|
let config = match load_config() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => { eprintln!("Config Load Error: {}", e); return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt_template = match task_type.as_str() {
|
||||||
|
"speling" => config.prompts.speling.clone(),
|
||||||
|
"summarize" => config.prompts.summarize.clone(),
|
||||||
|
"translate" => config.prompts.translate.clone(),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_prompt = format!("{} {}\n\n{}", config.prompts.general_prompt_modefier, prompt_template, text);
|
||||||
|
|
||||||
|
let use_stream = config.ollama.stream;
|
||||||
|
let mimic_typing = config.output.mimic_typing;
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
let body = json!({
|
||||||
|
"model": config.ollama.model,
|
||||||
|
"prompt": full_prompt,
|
||||||
|
"stream": use_stream
|
||||||
|
});
|
||||||
|
|
||||||
|
let url = format!("{}/api/generate", config.ollama.url.trim_end_matches('/'));
|
||||||
|
|
||||||
|
info!("Sending Ollama request to: {} (Stream: {}, Mimic: {})", url, use_stream, mimic_typing);
|
||||||
|
debug!("Request Body: {}", body);
|
||||||
|
|
||||||
|
let mut res = match client.post(&url).json(&body).send().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Ollama Request Failed: {}", e);
|
||||||
|
eprintln!("Ollama Req Error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = res.status();
|
||||||
|
info!("Ollama Response Status: {}", status);
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let error_text = res.text().await.unwrap_or_default();
|
||||||
|
error!("Ollama API Error: Status {}, Body: {}", status, error_text);
|
||||||
|
|
||||||
|
// Helper to extract cleaner error message from JSON if possible
|
||||||
|
let error_msg = if let Ok(json_err) = serde_json::from_str::<serde_json::Value>(&error_text) {
|
||||||
|
json_err.get("error").and_then(|s| s.as_str()).unwrap_or(&error_text).to_string()
|
||||||
|
} else {
|
||||||
|
error_text.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// SMART RECOVERY: Check if model is not found, trying to auto-fix
|
||||||
|
if status == reqwest::StatusCode::NOT_FOUND && error_msg.contains("model") && error_msg.contains("not found") {
|
||||||
|
warn!("Model not found. Attempting to discover available models...");
|
||||||
|
let tags_url = format!("{}/api/tags", config.ollama.url.trim_end_matches('/'));
|
||||||
|
if let Ok(tag_res) = client.get(&tags_url).send().await {
|
||||||
|
if tag_res.status().is_success() {
|
||||||
|
if let Ok(tag_data) = tag_res.json::<serde_json::Value>().await {
|
||||||
|
if let Some(models) = tag_data.get("models").and_then(|m| m.as_array()) {
|
||||||
|
if let Some(first_model) = models.first().and_then(|m| m.get("name")).and_then(|n| n.as_str()) {
|
||||||
|
let new_model = first_model.to_string();
|
||||||
|
info!("Found available model: {}. Retrying request...", new_model);
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
let mut new_config = config.clone();
|
||||||
|
new_config.ollama.model = new_model.clone();
|
||||||
|
if let Err(e) = save_config(&new_config) {
|
||||||
|
error!("Failed to save new config: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry Logic
|
||||||
|
let new_body = json!({
|
||||||
|
"model": new_model,
|
||||||
|
"prompt": full_prompt,
|
||||||
|
"stream": false
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Ok(retry_res) = client.post(&url).json(&new_body).send().await {
|
||||||
|
if retry_res.status().is_success() {
|
||||||
|
// If retry works, process output normally
|
||||||
|
let json_resp: serde_json::Value = retry_res.json().await.unwrap_or(json!({}));
|
||||||
|
if let Some(response_text) = json_resp.get("response").and_then(|v| v.as_str()) {
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
if let Ok(mut clipboard_guard) = state.clipboard.lock() {
|
||||||
|
if let Some(ref mut cb) = *clipboard_guard {
|
||||||
|
let _ = cb.set_text(response_text.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_notification = format!("⚠️ AI Error: {}\nCheck Config/Models.", error_msg);
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
if let Ok(mut clipboard_guard) = state.clipboard.lock() {
|
||||||
|
if let Some(ref mut cb) = *clipboard_guard {
|
||||||
|
let _ = cb.set_text(user_notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
let mut enigo_opt = if mimic_typing {
|
||||||
|
match Enigo::new(&Settings::default()) {
|
||||||
|
Ok(e) => Some(e),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to init Enigo: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
|
let mut full_buffer = String::new();
|
||||||
|
|
||||||
|
if use_stream {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
while let Ok(Some(chunk)) = res.chunk().await {
|
||||||
|
if state.should_stop.load(Ordering::Relaxed) {
|
||||||
|
info!("Detailed: Generation stopped by user.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_str = String::from_utf8_lossy(&chunk);
|
||||||
|
buffer.push_str(&chunk_str);
|
||||||
|
|
||||||
|
while let Some(pos) = buffer.find('\n') {
|
||||||
|
let line = buffer[..pos].to_string();
|
||||||
|
buffer.drain(..pos+1);
|
||||||
|
|
||||||
|
if let Ok(json_obj) = serde_json::from_str::<serde_json::Value>(&line) {
|
||||||
|
if let Some(token) = json_obj.get("response").and_then(|s| s.as_str()) {
|
||||||
|
full_buffer.push_str(token);
|
||||||
|
if let Some(ref mut enigo) = enigo_opt {
|
||||||
|
let _ = enigo.text(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if json_obj.get("done").and_then(|b| b.as_bool()).unwrap_or(false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match res.json::<serde_json::Value>().await {
|
||||||
|
Ok(json_obj) => {
|
||||||
|
if let Some(response_text) = json_obj.get("response").and_then(|v| v.as_str()) {
|
||||||
|
full_buffer = response_text.to_string();
|
||||||
|
if state.should_stop.load(Ordering::Relaxed) { return; }
|
||||||
|
|
||||||
|
if let Some(ref mut enigo) = enigo_opt {
|
||||||
|
let _ = enigo.text(&full_buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => error!("JSON Parse Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output to Clipboard if NOT mimic typing
|
||||||
|
if !mimic_typing && !full_buffer.is_empty() {
|
||||||
|
if let Ok(mut clipboard_guard) = state.clipboard.lock() {
|
||||||
|
if let Some(ref mut cb) = *clipboard_guard {
|
||||||
|
if let Err(e) = cb.set_text(full_buffer) {
|
||||||
|
error!("Clipboard Write Error: {}", e);
|
||||||
|
} else {
|
||||||
|
info!("Copied {} chars to clipboard.", text.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src-tauri/src/utilities/config.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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 OutputConfig {
|
||||||
|
pub mimic_typing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OutputConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mimic_typing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct OllamaConfig {
|
||||||
|
pub url: String,
|
||||||
|
pub model: String,
|
||||||
|
pub stream: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OllamaConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
url: "http://localhost:11434".to_string(),
|
||||||
|
model: "gemma3".to_string(),
|
||||||
|
stream: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct PromptsConfig {
|
||||||
|
pub general_prompt_modefier: String,
|
||||||
|
pub speling: String,
|
||||||
|
pub summarize: String,
|
||||||
|
pub translate: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PromptsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
general_prompt_modefier: "".to_string(),
|
||||||
|
speling: "Rätta stavningen och grammatiken i följande text:".to_string(),
|
||||||
|
summarize: "Summera följande text:".to_string(),
|
||||||
|
translate: "Översätt följande text till Svenska:".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct KeybindingsConfig {
|
||||||
|
pub speling: String,
|
||||||
|
pub summarize: String,
|
||||||
|
pub translate: String,
|
||||||
|
pub stop: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeybindingsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
summarize: "Ctrl+Shift+S".to_string(),
|
||||||
|
translate: "Ctrl+Shift+T".to_string(),
|
||||||
|
speling: "Ctrl+Shift+E".to_string(),
|
||||||
|
stop: "Ctrl+Shift+Q".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub logging: LogConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub general: GeneralConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub output: OutputConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ollama: OllamaConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub prompts: PromptsConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keybindings: KeybindingsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
logging: LogConfig::default(),
|
||||||
|
general: GeneralConfig::default(),
|
||||||
|
output: OutputConfig::default(),
|
||||||
|
ollama: OllamaConfig::default(),
|
||||||
|
prompts: PromptsConfig::default(),
|
||||||
|
keybindings: KeybindingsConfig::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(())
|
||||||
|
}
|
||||||
45
src-tauri/src/utilities/logging.rs
Normal 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(())
|
||||||
|
}
|
||||||
3
src-tauri/src/utilities/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod logging;
|
||||||
|
pub mod ai_handler;
|
||||||
1
src-tauri/src/viewers/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod tray;
|
||||||
152
src-tauri/src/viewers/tray.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_logs_window<R: Runtime>(app: &tauri::AppHandle<R>) {
|
||||||
|
match app.get_webview_window("logs") {
|
||||||
|
Some(window) => {
|
||||||
|
if let Ok(true) = window.is_visible() {
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let _ = WebviewWindowBuilder::new(
|
||||||
|
app,
|
||||||
|
"logs",
|
||||||
|
WebviewUrl::App("logs.html".into())
|
||||||
|
)
|
||||||
|
.title("AI Typist Loggar")
|
||||||
|
.inner_size(900.0, 600.0)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_tester_window<R: Runtime>(app: &tauri::AppHandle<R>) {
|
||||||
|
match app.get_webview_window("tester") {
|
||||||
|
Some(window) => {
|
||||||
|
if let Ok(true) = window.is_visible() {
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let _ = WebviewWindowBuilder::new(
|
||||||
|
app,
|
||||||
|
"tester",
|
||||||
|
WebviewUrl::App("tester.html".into())
|
||||||
|
)
|
||||||
|
.title("Testa AI")
|
||||||
|
.inner_size(500.0, 400.0)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>)?;
|
||||||
|
let logs_i = MenuItem::with_id(app, "logs", "Visa Loggar", true, None::<&str>)?;
|
||||||
|
let tester_i = MenuItem::with_id(app, "tester", "Testa AI", true, None::<&str>)?;
|
||||||
|
|
||||||
|
// Skapa menyn
|
||||||
|
let menu = Menu::with_items(app, &[&settings_i, &logs_i, &tester_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);
|
||||||
|
}
|
||||||
|
"logs" => {
|
||||||
|
info!("User clicked logs from tray");
|
||||||
|
toggle_logs_window(app);
|
||||||
|
}
|
||||||
|
"tester" => {
|
||||||
|
info!("User clicked tester from tray");
|
||||||
|
toggle_tester_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(())
|
||||||
|
}
|
||||||
@@ -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": "settings.html",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600,
|
"height": 600,
|
||||||
"visible": false
|
"visible": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||