Compare commits
No commits in common. "v0.1.0-rc1" and "main" have entirely different histories.
v0.1.0-rc1
...
main
@ -1,492 +0,0 @@
|
|||||||
# Dev Cheat Sheet
|
|
||||||
|
|
||||||
Concise command reference for day-to-day work across Git, Python, Rust, Node, Tauri, and .NET.
|
|
||||||
|
|
||||||
Use this as a practical command sheet, not as a full tutorial.
|
|
||||||
|
|
||||||
## Table of Contents 📑
|
|
||||||
|
|
||||||
- [Setup](#setup)
|
|
||||||
- [Git](#git)
|
|
||||||
- [PAQ-Next (Advanced Compression)](#paq-next-advanced-compression)
|
|
||||||
- [Python](#python)
|
|
||||||
- [Node and npm](#node-and-npm)
|
|
||||||
- [Rust](#rust)
|
|
||||||
- [Tauri v2](#tauri-v2)
|
|
||||||
- [Dotnet and CSharp](#dotnet-and-csharp)
|
|
||||||
- [Common Output Folder Pattern](#common-output-folder-pattern)
|
|
||||||
- [Docker](#docker)
|
|
||||||
- [Useful Cleanup Commands](#useful-cleanup-commands)
|
|
||||||
- [Recommended Habits](#recommended-habits)
|
|
||||||
- [Workflows and SDT DevTool](#workflows-and-sdt-devtool)
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
### Setup w64devkit for C++ (PAQ-Next)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:PATH = "f:\w64devkit\w64devkit\bin;" + $env:PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
Adds GCC/G++ and Unix tools to the current PowerShell session.
|
|
||||||
|
|
||||||
### Probe toolchain availability
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python scripts/diag.py probe --tool dotnet --json
|
|
||||||
python scripts/diag.py probe --tool python --json
|
|
||||||
python scripts/diag.py probe --tool node --json
|
|
||||||
python scripts/diag.py probe --tool npm --json
|
|
||||||
python scripts/diag.py probe --tool cargo --json
|
|
||||||
python scripts/diag.py probe --tool tauri --json
|
|
||||||
python scripts/diag.py probe --tool git --json
|
|
||||||
```
|
|
||||||
|
|
||||||
Checks if required tools are in the current path.
|
|
||||||
|
|
||||||
### Shell bootstrap (doctor)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python scripts/dev_shell.py doctor
|
|
||||||
```
|
|
||||||
|
|
||||||
Analyzes the environment and reports missing components or configuration errors.
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Git
|
|
||||||
|
|
||||||
### Check where you are
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git status --short --branch
|
|
||||||
git remote -v
|
|
||||||
git branch -vv
|
|
||||||
|
|
||||||
# See which files are ignored by git
|
|
||||||
git clean -nd
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stashing
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git stash push -m "Description"
|
|
||||||
git stash pop
|
|
||||||
git stash drop # Discard
|
|
||||||
git stash clear # Wipe all stashes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remotes & Pushing - THE FLAGS
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Safer 'force push' (fails if remote has moved)
|
|
||||||
git push --force-with-lease
|
|
||||||
|
|
||||||
# Push all branches and all tags at once
|
|
||||||
git push --all
|
|
||||||
git push --tags
|
|
||||||
|
|
||||||
# Delete a remote branch
|
|
||||||
git push origin --delete branch-name
|
|
||||||
|
|
||||||
# Set upstream for current branch (first push)
|
|
||||||
git push -u origin branch-name
|
|
||||||
```
|
|
||||||
|
|
||||||
### History & Analysis
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Graphical log in terminal
|
|
||||||
git log --graph --oneline --decorate --all
|
|
||||||
|
|
||||||
# Search commit messages
|
|
||||||
git log --grep="bug fix"
|
|
||||||
|
|
||||||
# Find which commit introduced a string
|
|
||||||
git log -S "secret_key"
|
|
||||||
|
|
||||||
# See changes per line (last change for every line)
|
|
||||||
git blame file_path.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git Repository Cleanup
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Fast cleanup of untracked files
|
|
||||||
git clean -fd
|
|
||||||
|
|
||||||
# Prune stale remote tracking branches
|
|
||||||
git fetch origin --prune
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
[🔼 Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## PAQ-Next (Advanced Compression)
|
|
||||||
|
|
||||||
### Compile PAQ-Next (C++)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Add toolchain first (if not in path)
|
|
||||||
$env:PATH = "f:\w64devkit\w64devkit\bin;" + $env:PATH
|
|
||||||
|
|
||||||
# Build from root of src-paq-next/
|
|
||||||
g++ -O3 -mavx2 -Iinclude cli/main.cpp src/archiver.cpp -o paq_next_cli.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
Compiles with AVX2 optimizations and include-path support.
|
|
||||||
|
|
||||||
### Run PAQ-Next Compression
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Compress a folder
|
|
||||||
.\paq_next_cli.exe c <source_folder>
|
|
||||||
|
|
||||||
# Compress a folder to a specific file
|
|
||||||
.\paq_next_cli.exe c <source_folder> <output_name>.paq
|
|
||||||
|
|
||||||
# Extract an archive
|
|
||||||
.\paq_next_cli.exe x <archive>.paq <destination_folder>
|
|
||||||
```
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Python
|
|
||||||
|
|
||||||
### Package Management - THE FLAGS
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Install from requirements with upgrades
|
|
||||||
pip install -r requirements.txt --upgrade
|
|
||||||
|
|
||||||
# Install without saving to cache (Great for CI/Low disk)
|
|
||||||
pip install -r requirements.txt --no-cache-dir
|
|
||||||
|
|
||||||
# Target a specific directory
|
|
||||||
pip install -t ./lib <package>
|
|
||||||
|
|
||||||
# Search local packages for a string
|
|
||||||
pip list | Select-String "crypto"
|
|
||||||
|
|
||||||
# See everything about a package
|
|
||||||
pip show <package-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quality & Performance
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Parallel testing
|
|
||||||
pip install pytest-xdist
|
|
||||||
pytest -n auto # Use all CPU cores
|
|
||||||
|
|
||||||
# Benchmark a script
|
|
||||||
python -m cProfile -s time script.py
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
[🔼 Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Node and npm
|
|
||||||
|
|
||||||
### Dependency Management - THE FLAGS
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Force install (when versions conflict)
|
|
||||||
npm install --force
|
|
||||||
|
|
||||||
# Ignore peer dependency errors (Common in React)
|
|
||||||
npm install --legacy-peer-deps
|
|
||||||
|
|
||||||
# Production only (Skip devDependencies)
|
|
||||||
npm install --production
|
|
||||||
|
|
||||||
# Deep security audit and auto-fix
|
|
||||||
npm audit fix --force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scripts & Performance
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Build with specific log level
|
|
||||||
npm run build --loglevel silent
|
|
||||||
|
|
||||||
# Pass custom flags directly to a script
|
|
||||||
npm run dev -- --host 0.0.0.0 --port 3000
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
[🔼 Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Rust
|
|
||||||
|
|
||||||
### Essential Workflow - THE FLAGS
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Fast check (Skip code generation)
|
|
||||||
cargo check --all-targets --all-features
|
|
||||||
|
|
||||||
# Build with specific features enabled
|
|
||||||
cargo build --features "feature-a,feature-b"
|
|
||||||
|
|
||||||
# Build without default features
|
|
||||||
cargo build --no-default-features
|
|
||||||
|
|
||||||
# Cross-compilation (Requires target in toolchain)
|
|
||||||
cargo build --target x86_64-apple-darwin
|
|
||||||
cargo build --target aarch64-unknown-linux-gnu
|
|
||||||
```
|
|
||||||
|
|
||||||
### Diagnostics & Performance
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# See exactly why a build is slow
|
|
||||||
cargo build --timings
|
|
||||||
|
|
||||||
# Run specific test by name/substring
|
|
||||||
cargo test test_function_name
|
|
||||||
|
|
||||||
# Open local docs for all dependencies
|
|
||||||
cargo doc --open --no-deps
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
[🔼 Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Tauri v2
|
|
||||||
|
|
||||||
### Initialize Mobile (REQUIRED for first run)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# This creates the android/ios project folders in src-tauri/gen
|
|
||||||
npm run tauri android init
|
|
||||||
npm run tauri ios init
|
|
||||||
```
|
|
||||||
|
|
||||||
### Desktop Builds - THE FLAGS
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Fast build (Binary only)
|
|
||||||
npm run tauri build -- --no-bundle
|
|
||||||
|
|
||||||
# Explicit config file (Great for multi-env)
|
|
||||||
npm run tauri build -- --config path/to/config.json
|
|
||||||
|
|
||||||
# Verbose output for debugging packaging
|
|
||||||
npm run tauri build -- --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mobile Builds
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Debug mode on Android device (or Emulator)
|
|
||||||
npm run tauri android dev
|
|
||||||
|
|
||||||
# Production APK/AAB generation
|
|
||||||
npm run tauri android build -- --target aarch64
|
|
||||||
```
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# System diagnostic report
|
|
||||||
npm run tauri info
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
[🔼 Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
### Frontend-only build
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Builds the web frontend assets (the `dist/` folder) without launching or packaging the desktop app.
|
|
||||||
|
|
||||||
## Dotnet and CSharp
|
|
||||||
|
|
||||||
### Restore & Build - THE FLAGS
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Restore from specific NuGet source
|
|
||||||
dotnet restore --source https://api.nuget.org/v3/index.json
|
|
||||||
|
|
||||||
# Build without restoring (Great for build script repetition)
|
|
||||||
dotnet build --no-restore
|
|
||||||
|
|
||||||
# Build with minimal output (Keep it clean)
|
|
||||||
dotnet build --verbosity quiet # or minimal, normal, detailed, diagnostic
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run & Watch
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Run with custom environment variables
|
|
||||||
$env:ASPNETCORE_ENVIRONMENT="Development"; dotnet run
|
|
||||||
|
|
||||||
# Watch with specific project filter
|
|
||||||
dotnet watch --project Project.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Publishing
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Single-File for Linux (Self-Contained)
|
|
||||||
dotnet publish -c Release -r linux-x64 --self-contained true `
|
|
||||||
-p:PublishSingleFile=true `
|
|
||||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
|
||||||
-o ./output/linux-x64-single
|
|
||||||
|
|
||||||
# Ready-To-Run (R2R) - Fast startup at the cost of size
|
|
||||||
dotnet publish -c Release -o ./output -p:PublishReadyToRun=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tools
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Install a dotnet tool globally
|
|
||||||
dotnet tool install -g <tool-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
[🔼 Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Common Output Folder Pattern
|
|
||||||
|
|
||||||
Use explicit output folders so results are easy to find and easy to clean up.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
./output/
|
|
||||||
./output/win-x64/
|
|
||||||
./output/win-x64-single/
|
|
||||||
./output/linux-x64/
|
|
||||||
./output/linux-x64-single/
|
|
||||||
```
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
### Build & Run
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker build -t app-name .
|
|
||||||
docker run -p 8080:80 app-name
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker System Cleanup
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
docker system prune -a # Wipe everything unused
|
|
||||||
docker container prune
|
|
||||||
docker image prune
|
|
||||||
```
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Useful Cleanup Commands
|
|
||||||
|
|
||||||
### Remove Node artifacts
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Remove-Item -Recurse -Force .\node_modules
|
|
||||||
Remove-Item -Recurse -Force .\dist
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove Rust / Tauri artifacts
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Remove-Item -Recurse -Force .\src-tauri\target
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove .NET artifacts (Deep clean)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Get-ChildItem -Include bin,obj,output -Recurse | Remove-Item -Recurse -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove Python artifacts
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Remove-Item -Recurse -Force .\.venv
|
|
||||||
Get-ChildItem -Include __pycache__ -Recurse | Remove-Item -Recurse -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Recommended Habits
|
|
||||||
|
|
||||||
Before any risky Git action:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git status --short --branch
|
|
||||||
git branch backup/pre-risk
|
|
||||||
git tag pre-risk-YYYY-MM-DD
|
|
||||||
git fetch origin --prune
|
|
||||||
```
|
|
||||||
|
|
||||||
Before publishing builds:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run build
|
|
||||||
cargo test
|
|
||||||
dotnet test
|
|
||||||
```
|
|
||||||
|
|
||||||
Adjust to the stack used by the current project.
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
|
|
||||||
## Workflows and SDT DevTool
|
|
||||||
|
|
||||||
### Build / Run SDT (DevTool)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Quick build
|
|
||||||
python scripts/dotnet-min.py build
|
|
||||||
|
|
||||||
# Standard run
|
|
||||||
dotnet run --project DevTool.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
### Route & Workflow Verification
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Static check
|
|
||||||
python scripts/verify-workflow-routes.py --project-root .
|
|
||||||
|
|
||||||
# Headless execution test
|
|
||||||
python scripts/verify-workflow-routes.py --project-root . --workflow build --workflow tauri --execute --env-profile dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build & Sync Output
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Publish web assets + app
|
|
||||||
python scripts/publish-app.py --target web
|
|
||||||
|
|
||||||
# Publish specific projects
|
|
||||||
python scripts/publish-sidecar.py --project path/to/sidecar.csproj
|
|
||||||
python scripts/publish-webgateway.py --project path/to/gateway.csproj
|
|
||||||
|
|
||||||
# Sync all results to central output/
|
|
||||||
python scripts/sync-output.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Migration Gate
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python scripts/migration-gate.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### NuGet / Node Cleanup
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# NuGet cache export
|
|
||||||
python scripts/nuget-export-cache.py --output-zip cache-export.zip
|
|
||||||
|
|
||||||
# Clean node_modules safely
|
|
||||||
python scripts/npm-clean.py --working-dir .
|
|
||||||
```
|
|
||||||
|
|
||||||
[Back to Top](#dev-cheat-sheet)
|
|
||||||
@ -7,7 +7,7 @@ Thisper is a typing-first communication translator. It rewrites raw text into cl
|
|||||||
- Desktop text-to-text rewrite UI with diff review
|
- Desktop text-to-text rewrite UI with diff review
|
||||||
- Rewrite modes: `Preserve Voice`, `Clean`, `Readable`, `Formal`, `Concise`
|
- Rewrite modes: `Preserve Voice`, `Clean`, `Readable`, `Formal`, `Concise`
|
||||||
- Gemini-backed cloud rewrite provider with typed model selection
|
- Gemini-backed cloud rewrite provider with typed model selection
|
||||||
- Secure Gemini API key storage through native platform protection
|
- Secure Gemini API key storage through the system credential store
|
||||||
- Global cross-app rewrite shortcut: `Ctrl + Alt + R`
|
- Global cross-app rewrite shortcut: `Ctrl + Alt + R`
|
||||||
- System tray support with background operation
|
- System tray support with background operation
|
||||||
- Non-destructive clipboard replacement flow: selected text is only replaced after the full rewrite succeeds
|
- Non-destructive clipboard replacement flow: selected text is only replaced after the full rewrite succeeds
|
||||||
@ -62,10 +62,7 @@ If no API key is configured, Thisper brings the main window forward instead of a
|
|||||||
|
|
||||||
### Credentials
|
### Credentials
|
||||||
|
|
||||||
Use the in-app Settings dialog to save the Gemini API key using native platform protection.
|
Use the in-app Settings dialog to save the Gemini API key into your system credential store.
|
||||||
|
|
||||||
- Windows: DPAPI-encrypted local app storage tied to your Windows user account
|
|
||||||
- Other supported desktop platforms: native credential store backend
|
|
||||||
|
|
||||||
For development, Thisper also accepts:
|
For development, Thisper also accepts:
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="api-key-input">Gemini API Key</label>
|
<label for="api-key-input">Gemini API Key</label>
|
||||||
<input type="password" id="api-key-input" placeholder="Paste your API key here...">
|
<input type="password" id="api-key-input" placeholder="Paste your API key here...">
|
||||||
<p class="hint">Stored securely using native platform protection. On Windows, Thisper uses DPAPI-encrypted app data. The key is never written in plaintext.</p>
|
<p class="hint">Stored in your system credential store. Thisper never writes the key to a plaintext file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="save-settings-btn" class="primary-btn">Save Settings</button>
|
<button id="save-settings-btn" class="primary-btn">Save Settings</button>
|
||||||
|
|||||||
12
src-tauri/Cargo.lock
generated
12
src-tauri/Cargo.lock
generated
@ -4529,7 +4529,6 @@ dependencies = [
|
|||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"windows-dpapi",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5488,17 +5487,6 @@ dependencies = [
|
|||||||
"windows-strings 0.5.1",
|
"windows-strings 0.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-dpapi"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2981752d6f11bdcab4db52be8ad5c0e6a6d4d6d566764b3058cc1ee473e6479e"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"log",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-future"
|
name = "windows-future"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@ -32,9 +32,6 @@ futures-util = "0.3.32"
|
|||||||
keyring = "3.6.3"
|
keyring = "3.6.3"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
|
||||||
windows-dpapi = "0.2.0"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
use tauri::AppHandle;
|
|
||||||
|
|
||||||
const KEYRING_SERVICE: &str = "thisper";
|
|
||||||
const KEYRING_ACCOUNT: &str = "gemini_api_key";
|
|
||||||
|
|
||||||
pub fn load_api_key(app: &AppHandle) -> Result<Option<String>, String> {
|
|
||||||
backend::load_api_key(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_api_key(app: &AppHandle, key: &str) -> Result<(), String> {
|
|
||||||
backend::save_api_key(app, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn storage_description() -> &'static str {
|
|
||||||
backend::storage_description()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
mod backend {
|
|
||||||
use super::{KEYRING_ACCOUNT, KEYRING_SERVICE};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tauri::{AppHandle, Manager};
|
|
||||||
use windows_dpapi::{Scope, decrypt_data, encrypt_data};
|
|
||||||
|
|
||||||
const API_KEY_FILE: &str = "gemini_api_key.dpapi";
|
|
||||||
|
|
||||||
fn encrypted_file_path(app: &AppHandle) -> Result<PathBuf, String> {
|
|
||||||
let dir = app
|
|
||||||
.path()
|
|
||||||
.app_local_data_dir()
|
|
||||||
.map_err(|err| format!("Failed to resolve local app data directory: {err}"))?;
|
|
||||||
Ok(dir.join("credentials").join(API_KEY_FILE))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_legacy_keyring_value() -> Result<Option<String>, String> {
|
|
||||||
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
|
|
||||||
.map_err(|err| format!("Failed to access legacy credential store: {err}"))?;
|
|
||||||
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(password) if !password.trim().is_empty() => Ok(Some(password)),
|
|
||||||
Ok(_) => Ok(None),
|
|
||||||
Err(keyring::Error::NoEntry) => Ok(None),
|
|
||||||
Err(err) => Err(format!("Failed to read legacy credential store: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_legacy_keyring_value() {
|
|
||||||
if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT) {
|
|
||||||
let _ = entry.delete_credential();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_api_key(app: &AppHandle) -> Result<Option<String>, String> {
|
|
||||||
let path = encrypted_file_path(app)?;
|
|
||||||
if path.exists() {
|
|
||||||
let encrypted = fs::read(&path)
|
|
||||||
.map_err(|err| format!("Failed to read encrypted API key: {err}"))?;
|
|
||||||
if encrypted.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let decrypted = decrypt_data(&encrypted, Scope::User, None)
|
|
||||||
.map_err(|err| format!("Failed to decrypt API key: {err}"))?;
|
|
||||||
let key = String::from_utf8(decrypted)
|
|
||||||
.map_err(|err| format!("Failed to decode decrypted API key: {err}"))?;
|
|
||||||
|
|
||||||
if key.trim().is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Some(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(legacy_key) = load_legacy_keyring_value()? {
|
|
||||||
save_api_key(app, &legacy_key)?;
|
|
||||||
delete_legacy_keyring_value();
|
|
||||||
return Ok(Some(legacy_key));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_api_key(app: &AppHandle, key: &str) -> Result<(), String> {
|
|
||||||
let path = encrypted_file_path(app)?;
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
fs::create_dir_all(parent)
|
|
||||||
.map_err(|err| format!("Failed to create credential directory: {err}"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encrypted = encrypt_data(key.as_bytes(), Scope::User, None)
|
|
||||||
.map_err(|err| format!("Failed to encrypt API key with Windows DPAPI: {err}"))?;
|
|
||||||
|
|
||||||
fs::write(&path, encrypted)
|
|
||||||
.map_err(|err| format!("Failed to write encrypted API key: {err}"))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn storage_description() -> &'static str {
|
|
||||||
"Stored securely using Windows DPAPI in Thisper's local app data directory. The key is encrypted for your Windows user account and is never written in plaintext."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
mod backend {
|
|
||||||
use super::{KEYRING_ACCOUNT, KEYRING_SERVICE};
|
|
||||||
use tauri::AppHandle;
|
|
||||||
|
|
||||||
pub fn load_api_key(_app: &AppHandle) -> Result<Option<String>, String> {
|
|
||||||
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
|
|
||||||
.map_err(|err| format!("Failed to access native credential storage: {err}"))?;
|
|
||||||
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(password) if !password.trim().is_empty() => Ok(Some(password)),
|
|
||||||
Ok(_) => Ok(None),
|
|
||||||
Err(keyring::Error::NoEntry) => Ok(None),
|
|
||||||
Err(err) => Err(format!("Failed to read native credential storage: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_api_key(_app: &AppHandle, key: &str) -> Result<(), String> {
|
|
||||||
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
|
|
||||||
.map_err(|err| format!("Failed to access native credential storage: {err}"))?;
|
|
||||||
entry
|
|
||||||
.set_password(key)
|
|
||||||
.map_err(|err| format!("Failed to save API key securely: {err}"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn storage_description() -> &'static str {
|
|
||||||
"Stored securely using the native platform credential store. Thisper never writes the key in plaintext."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
pub mod credentials;
|
|
||||||
pub mod gemini;
|
pub mod gemini;
|
||||||
pub mod translator;
|
pub mod translator;
|
||||||
|
|
||||||
@ -18,6 +17,8 @@ use tauri::{AppHandle, Emitter, Manager, State, WebviewWindow, WindowEvent};
|
|||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
||||||
|
|
||||||
|
const KEYRING_SERVICE: &str = "thisper";
|
||||||
|
const KEYRING_ACCOUNT: &str = "gemini_api_key";
|
||||||
const MAIN_WINDOW_LABEL: &str = "main";
|
const MAIN_WINDOW_LABEL: &str = "main";
|
||||||
const TRAY_ID: &str = "main-tray";
|
const TRAY_ID: &str = "main-tray";
|
||||||
const MENU_SHOW_ID: &str = "show";
|
const MENU_SHOW_ID: &str = "show";
|
||||||
@ -68,6 +69,17 @@ struct RuntimeState {
|
|||||||
last_metrics: Option<RewriteMetrics>,
|
last_metrics: Option<RewriteMetrics>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_saved_api_key() -> Result<Option<String>, String> {
|
||||||
|
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
|
||||||
|
.map_err(|err| format!("Failed to access credential store: {err}"))?;
|
||||||
|
match entry.get_password() {
|
||||||
|
Ok(password) if !password.trim().is_empty() => Ok(Some(password)),
|
||||||
|
Ok(_) => Ok(None),
|
||||||
|
Err(keyring::Error::NoEntry) => Ok(None),
|
||||||
|
Err(err) => Err(format!("Failed to read credential store: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn api_key_configured(state: &AppState) -> bool {
|
fn api_key_configured(state: &AppState) -> bool {
|
||||||
let key = state.api_key.lock().unwrap();
|
let key = state.api_key.lock().unwrap();
|
||||||
!key.trim().is_empty() || std::env::var("GEMINI_API_KEY").is_ok()
|
!key.trim().is_empty() || std::env::var("GEMINI_API_KEY").is_ok()
|
||||||
@ -320,7 +332,11 @@ async fn save_api_key(
|
|||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
credentials::save_api_key(&app, &key)?;
|
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
|
||||||
|
.map_err(|err| format!("Failed to access credential store: {err}"))?;
|
||||||
|
entry
|
||||||
|
.set_password(&key)
|
||||||
|
.map_err(|err| format!("Failed to save API key securely: {err}"))?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut key_state = state.api_key.lock().unwrap();
|
let mut key_state = state.api_key.lock().unwrap();
|
||||||
@ -407,7 +423,11 @@ async fn run_clipboard_rewrite(app: AppHandle) -> Result<(), String> {
|
|||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let initial_key = std::env::var("GEMINI_API_KEY").unwrap_or_default();
|
let initial_key = load_saved_api_key()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.or_else(|| std::env::var("GEMINI_API_KEY").ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let provider =
|
let provider =
|
||||||
Box::new(gemini::GeminiProvider::new().expect("Gemini provider initialization failed"))
|
Box::new(gemini::GeminiProvider::new().expect("Gemini provider initialization failed"))
|
||||||
@ -454,21 +474,6 @@ pub fn run() {
|
|||||||
attach_window_lifecycle(&app.handle()).map_err(std::io::Error::other)?;
|
attach_window_lifecycle(&app.handle()).map_err(std::io::Error::other)?;
|
||||||
app.global_shortcut().register(shortcut_clone.clone())?;
|
app.global_shortcut().register(shortcut_clone.clone())?;
|
||||||
|
|
||||||
match credentials::load_api_key(&app.handle()) {
|
|
||||||
Ok(Some(saved_key)) => {
|
|
||||||
let state: State<'_, AppState> = app.state();
|
|
||||||
let mut api_key = state.api_key.lock().unwrap();
|
|
||||||
*api_key = saved_key;
|
|
||||||
}
|
|
||||||
Ok(None) => {}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Credential storage load error: {err}");
|
|
||||||
let state: State<'_, AppState> = app.state();
|
|
||||||
let mut runtime = state.runtime.lock().unwrap();
|
|
||||||
runtime.last_error = Some(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let state: State<'_, AppState> = app.state();
|
let state: State<'_, AppState> = app.state();
|
||||||
let mut runtime = state.runtime.lock().unwrap();
|
let mut runtime = state.runtime.lock().unwrap();
|
||||||
|
|||||||
@ -161,7 +161,7 @@ saveSettingsBtn.addEventListener("click", async () => {
|
|||||||
settingsModal.classList.add("hidden");
|
settingsModal.classList.add("hidden");
|
||||||
apiKeyInput.value = "";
|
apiKeyInput.value = "";
|
||||||
await refreshRuntimeStatus();
|
await refreshRuntimeStatus();
|
||||||
alert("API key saved using native platform protection.");
|
alert("API key saved to your system credential store.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Error saving API key: " + err);
|
alert("Error saving API key: " + err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user