diff --git a/CLAUDE.md b/CLAUDE.md index 6e662dd4..add8a32a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,107 +4,128 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -OpenLess is a native macOS menu-bar voice-input layer (SwiftPM, macOS 15+, Swift 5.9). User holds/toggles a global hotkey, speaks, and the dictated text is polished and inserted at the current cursor in any app. The product principles, state machine, and module list live in `docs/openless-development.md` and `docs/openless-overall-logic.md` — read those before changing product behavior. +OpenLess is a menu-bar/tray voice-input layer. Hold or toggle a global hotkey, speak, and the dictated text is polished and inserted at the current cursor in any app. Product principles, state machine, and module list live in `docs/openless-development.md` and `docs/openless-overall-logic.md` — read those before changing product behavior. + +The repository contains **two parallel implementations** of the same product: + +| Path | Stack | Status | +| --- | --- | --- | +| `Sources/`, `Tests/`, `Package.swift`, `scripts/`, `appcast.xml` | SwiftPM macOS-only (macOS 15+, Swift 5.9) | Legacy. Still ships Sparkle updates for `v*` tags so old users keep auto-updating. | +| `openless -all/app/` (note the space) | Tauri 2 + Rust backend + React/TS frontend, macOS 12+ and Windows | **Active**. All current development happens here. | + +The Tauri port is a faithful module-for-module rewrite of the Swift app. **The Swift original is the behavior authority — when Rust and TS disagree, Swift wins.** When porting, open the Rust file and the matching `Sources/OpenLess/...` Swift file side by side. UI must match `openless -all/design_handoff_openless/*.jsx` pixel-for-pixel; the JSX is reference-only, never imported. ## Build, Run, Test -The app must run as a bundled `.app` to receive TCC entitlements (Accessibility, Microphone, AppleEvents). Running `swift run` directly will leave permission prompts broken. +### Tauri (current — start here) ```bash -# Library/test build (no bundle, fastest) -swift build -swift test -swift test --filter OpenLessCoreTests.PolishModeTests/ # single test +cd "openless -all/app" +npm ci -# Full app: builds release, assembles build/OpenLess.app, ad-hoc codesigns, -# kills the running instance, and resets TCC by default -./scripts/build-app.sh +# Dev: vite at :1420 + tauri shell +npm run tauri dev -# Keep existing TCC approvals across rebuilds -RESET_TCC=0 ./scripts/build-app.sh +# Build .app (+ DMG) — use this script, not `tauri build` directly, +# because it threads Apple signing env vars and validates Info.plist. +./scripts/build-mac.sh # build, sign, install to /Applications, reset TCC +INSTALL=0 ./scripts/build-mac.sh # build only -# Launch -open build/OpenLess.app -# Useful launch args (handled in AppDelegate.runLaunchActions) -open build/OpenLess.app --args --open-settings -open build/OpenLess.app --args --start-recording - -# Live logs -tail -f ~/Library/Logs/OpenLess/OpenLess.log +# Frontend-only TS check +npm run build # = tsc && vite build ``` -After a fresh build with `RESET_TCC=1` (default), macOS will re-prompt for Accessibility + Microphone. The app must be **fully quit and relaunched** after granting Accessibility before the global hotkey tap works. +Generated artifacts: +- `openless -all/app/src-tauri/target/release/bundle/macos/OpenLess.app` +- `openless -all/app/src-tauri/target/release/bundle/dmg/OpenLess__aarch64.dmg` + +Logs: `~/Library/Logs/OpenLess/openless.log` (macOS) / `%LOCALAPPDATA%\OpenLess\Logs\openless.log` (Windows). + +There is no test runner wired in for the frontend. `src/lib/providerSetup.test.ts` is a hand-rolled assertion script — run with `npx tsx src/lib/providerSetup.test.ts` if you need it. Rust side has no `cargo test` targets yet; behavior is verified by running the app. + +### Swift (legacy — only touch for Sparkle releases) + +```bash +swift build +swift test +swift test --filter OpenLessCoreTests.PolishModeTests/ + +./scripts/build-app.sh # build .app, ad-hoc sign, embed Sparkle, reset TCC +RESET_TCC=0 ./scripts/build-app.sh # keep TCC approvals +./scripts/release.sh # bump build-app.sh, sign zip, append appcast.xml, tag, gh release +``` + +Logs: `~/Library/Logs/OpenLess/OpenLess.log`. ## Architecture -SwiftPM workspace with one executable (`OpenLessApp`) and seven libraries. Every library depends only on `OpenLessCore` — there are no library-to-library deps. `OpenLessApp` is the only place that wires them together (`DictationCoordinator`). +`DictationCoordinator` (Swift) / `coordinator::Coordinator` (Rust) is the **single owner of session state**. Hotkey edges drive a small phase enum (`Idle → Starting → Listening → Processing`); recorder, ASR, polish, insertion, and history are wired here and nowhere else. Library/module code never calls across modules — they each depend only on shared types. ``` -OpenLessCore // Pure value types: DictationSession, PolishMode, HotkeyBinding, - // AudioConsumer protocol, RawTranscript/FinalText, errors. - // No AppKit. Every other target imports this. -OpenLessHotkey // CGEventTap-based modifier-key monitor. Emits .toggled / .cancelled - // on an AsyncStream. Requires Accessibility permission. -OpenLessRecorder // AVAudioEngine → 16 kHz mono Int16 PCM, RMS level callback, - // pushes Data chunks into an AudioConsumer. -OpenLessASR // Volcengine streaming ASR over WebSocket (binary framing in - // VolcengineFrame). Implements AudioConsumer so the recorder - // feeds it directly. -OpenLessPolish // Doubao (Ark) chat-completions client. Mode-driven prompts in - // PolishPrompts. -OpenLessInsertion // TextInserter: AX focused-element write first, then clipboard - // + simulated Cmd+V, then copy-only fallback. -OpenLessPersistence // CredentialsVault (Keychain), HistoryStore (JSON in Application - // Support), UserPreferences (UserDefaults). -OpenLessUI // Capsule SwiftUI view + state enum (no window plumbing). -OpenLessApp // AppDelegate, MenuBarController, SettingsWindowController, - // CapsuleWindowController, Log, BufferingAudioConsumer, - // DictationCoordinator (the state machine). +Swift (Sources/OpenLess*) Rust (openless -all/app/src-tauri/src) Purpose +───────────────────────── ────────────────────────────────────── ──────────────────────────────── +OpenLessCore types.rs Pure value types: DictationSession, PolishMode, HotkeyBinding, errors +OpenLessHotkey hotkey.rs Global hotkey monitor (modifier-key edges) +OpenLessRecorder recorder.rs Mic → 16 kHz mono Int16 PCM, RMS callback +OpenLessASR asr/{mod,frame,volcengine}.rs Volcengine streaming ASR over WebSocket +OpenLessPolish polish.rs OpenAI-compatible chat completions (Ark / DeepSeek / etc.) +OpenLessInsertion insertion.rs AX focused-element write → clipboard + Cmd+V → copy-only fallback +OpenLessPersistence persistence.rs History/preferences/vocab JSON + Keychain credentials +OpenLessUI src/components/Capsule.tsx Capsule view + state enum +OpenLessApp / DictationCoord. coordinator.rs + commands.rs + lib.rs State machine, IPC surface, tray icon, window plumbing + permissions.rs TCC checks (Accessibility / Microphone) + src/ (React) Main window UI: Overview / History / Vocab / Style / Settings ``` ### Dictation pipeline -`DictationCoordinator` (`Sources/OpenLessApp/DictationCoordinator.swift`) is the single owner of session state. It is `@MainActor` and uses a one-bit `inSession` toggle driven by `HotkeyMonitor.events`: - ``` -.toggled (1st) → beginSession: - Recorder.start (mic, level callback → capsule) - VolcengineStreamingASR.openSession (WebSocket) - BufferingAudioConsumer.attach(asr) // any pre-connect audio is replayed - -.toggled (2nd) → endSession: - Recorder.stop, ASR.sendLastFrame, awaitFinalResult - DoubaoPolishClient.polish(raw, mode) - TextInserter.insert(final) // .inserted | .copiedFallback - HistoryStore.save(DictationSession) - CapsuleWindowController hides after delay - -.cancelled → ASR.cancel, Recorder.stop, capsule .cancelled +hotkey edge (1st) → beginSession: Recorder.start → ASR.openSession → BufferingAudioConsumer.attach +hotkey edge (2nd) → endSession: Recorder.stop → ASR.sendLastFrame → awaitFinal → Polish → Insert → History.save +.cancelled → ASR.cancel, Recorder.stop, capsule .cancelled ``` -Important behaviors: -- **Polish/ASR fallbacks are silent**: if Ark creds are missing or polish fails, the raw transcript is inserted; if Volcengine creds are missing, a mock pipeline runs and copies a placeholder. Don't add hard errors here — the contract is "user's words don't get lost." +Invariants: +- **Polish/ASR fallbacks are silent.** Missing Ark creds → insert raw transcript. Missing Volcengine creds → mock pipeline copies a placeholder. The contract is *"the user's words don't get lost"* — don't add hard errors here. - **`BufferingAudioConsumer`** queues PCM until the WebSocket is ready, then drains. Recorder always pushes to it; ASR is attached after `openSession` resolves. -- **Hotkey is toggle-only**, not press-and-hold. The monitor yields one `.toggled` per modifier-key keydown; the coordinator interprets odd/even. +- **Hotkey is toggle-only**, not press-and-hold. The monitor yields one edge per modifier-key keydown; the coordinator interprets odd/even. ### Permissions, credentials, on-disk state -- **TCC**: `Info.plist` declares `NSMicrophoneUsageDescription` and `NSAppleEventsUsageDescription`. Accessibility is requested at first run via `AXIsProcessTrustedWithOptions`. `LSUIElement = true` (menu-bar only, no Dock icon). -- **Bundle ID** is `com.openless.app` (hard-coded in `scripts/build-app.sh` and `CredentialsVault.serviceName`). Changing it breaks Keychain lookups and all granted TCC entries. -- **Credentials** live in Keychain under accounts defined in `CredentialAccount` (`volcengine.app_key`, `volcengine.access_key`, `volcengine.resource_id`, `ark.api_key`, `ark.model_id`, `ark.endpoint`). Set them via the Settings window — never hard-code. -- **History**: `~/Library/Application Support/OpenLess/history.json` (JSON array, capped at 200 entries). -- **Logs**: `~/Library/Logs/OpenLess/OpenLess.log`. Use `Log.write` from `OpenLessApp`; libraries don't log directly. +- **Bundle ID `com.openless.app`** is shared between Swift and Tauri builds (hard-coded in `scripts/build-app.sh`, `openless -all/app/src-tauri/tauri.conf.json`, and `CredentialsVault.serviceName`). Changing it breaks Keychain lookups *and* every existing TCC grant. +- **TCC**: Microphone + Accessibility + AppleEvents. Both apps declare `NSMicrophoneUsageDescription` / `NSAccessibilityUsageDescription` / `NSAppleEventsUsageDescription` in their Info.plist. Tauri's lives at `openless -all/app/src-tauri/Info.plist`. After a fresh build that resets TCC, the app must be **fully quit and relaunched** after granting Accessibility before the global hotkey tap installs. +- **Credentials** live in Keychain under accounts in `CredentialAccount` (`volcengine.app_key`, `volcengine.access_key`, `volcengine.resource_id`, `ark.api_key`, `ark.model_id`, `ark.endpoint`). The Rust port additionally reads the legacy plaintext fallback at `~/.openless/credentials.json` so users who configured the Swift app keep their creds without re-entering. Never hard-code keys. +- **Per-user data**: + - macOS: `~/Library/Application Support/OpenLess/{history.json, preferences.json, dictionary.json}` — same paths as the Swift app, capped at 200 history entries. **Do not rename `dictionary.json` to `vocab.json`** (drops user data). + - Windows: `%APPDATA%\OpenLess\` + - Linux: `$XDG_DATA_HOME/OpenLess` (Tauri only) + +### Release pipelines + +Two separate flows, by design: + +- **Swift (Sparkle, old users):** `scripts/release.sh ` bumps `build-app.sh`, builds the `.app`, ditto-zips it, signs with Sparkle EdDSA private key (Keychain item, not in repo), appends `` to `appcast.xml`, commits, tags `v`, pushes, and creates the GitHub Release. The public EdDSA key in `build-app.sh` (`SPARKLE_PUBLIC_KEY`) and the appcast URL `https://raw.githubusercontent.com/appergb/openless/main/appcast.xml` are baked into shipped clients — changing either strands existing users. +- **Tauri (cross-platform):** push a `v*-tauri` tag → `.github/workflows/release-tauri.yml` builds macOS arm64 `.dmg` and Windows x64 `.msi`. macOS Developer ID signing + notarization runs only when `APPLE_CERTIFICATE` / `APPLE_CERTIFICATE_PASSWORD` / `APPLE_ID` / `APPLE_PASSWORD` / `APPLE_TEAM_ID` secrets are set; otherwise it falls back to ad-hoc signing with a CI warning. Tauri tags use `-tauri` suffix specifically to not collide with Swift `vX.Y.Z` tags. + +When bumping versions, update **both** `version` fields: `openless -all/app/package.json` and `openless -all/app/src-tauri/tauri.conf.json` (and `Cargo.toml`). For Swift releases, bump `APP_VERSION` *and* `BUILD_NUMBER` in `scripts/build-app.sh`. + +## Repo conventions + +- **Comments, log messages, user-facing strings, and most docs are in Simplified Chinese.** Match that when editing existing strings; new internal type/API names stay in English. +- **macOS hotkey monitor must use native `CGEventTap`, never `rdev`.** `rdev` synchronously calls `TSMGetInputSourceProperty` from non-main threads, which macOS 14+ aborts via `dispatch_assert_queue_fail` → SIGTRAP. The Swift impl uses CGEventTap; the Rust impl uses CGEventTap on macOS and `rdev` only on Linux/Windows. Don't unify them. +- **Don't `NSApp.activate` on the dictation path** — it steals focus and breaks insertion. The Tauri equivalent: only call `set_activation_policy(Regular)` + `activateIgnoringOtherApps` from `show_main_window` / mic-permission prompts, never from `start_dictation`. +- All public Swift API surface is `Sendable`; UI/coordinator is `@MainActor`; audio/ASR/insertion classes that bridge C APIs are `@unchecked Sendable` with explicit locks. The Rust port mirrors this with `Arc>` (parking_lot) wrappers — keep the locking discipline when adding fields. +- Swift libraries depend only on `OpenLessCore`. Rust modules depend only on `types.rs`. New cross-module wiring goes in `DictationCoordinator` / `coordinator.rs`, not in the leaf modules. ### Adding a new module +Tauri (preferred): +1. Add a `.rs` (or directory) under `openless -all/app/src-tauri/src/`, importing only from `types`. +2. Register it in `lib.rs` (`mod ;`). +3. Wire it into `coordinator.rs` and expose any frontend-callable surface via `commands.rs` + `invoke_handler!`. +4. Add the matching TS wrapper in `openless -all/app/src/lib/ipc.ts` (with a mock branch for browser dev). + +Swift (only if also patching the legacy app): 1. Add target in `Package.swift` under `Sources/OpenLess`, depending only on `OpenLessCore`. -2. Add it to `OpenLessApp`'s dependency list. -3. Wire it in `DictationCoordinator` — do not call across libraries. -4. If it has pure logic worth testing, add a `Tests/OpenLessTests` target alongside `OpenLessCoreTests`. - -## Conventions specific to this repo - -- Comments, log messages, user-facing strings, and most doc files are in **Simplified Chinese**. Match that when editing existing strings; new internal types/APIs stay in English. -- All public API surface in libraries is `Sendable`; concurrency uses `@MainActor` for UI/coordinator code and `@unchecked Sendable` with explicit locks for audio/ASR classes that bridge C APIs (`Recorder`, `VolcengineStreamingASR`, `TextInserter`). -- The capsule window is `LSUIElement`-friendly: never call `NSApp.activate` from the dictation path (it steals focus and breaks insertion). The only exception is the mic-permission prompt in `beginSession`. -- When bumping `APP_VERSION`/`BUILD_NUMBER` in `scripts/build-app.sh`, do both — they go straight into `Info.plist` as `CFBundleShortVersionString` / `CFBundleVersion`. +2. Add it to `OpenLessApp`'s dependency list and wire it in `DictationCoordinator`. +3. Add `Tests/OpenLessTests` for pure logic. diff --git a/openless -all/app/src-tauri/src/asr/volcengine.rs b/openless -all/app/src-tauri/src/asr/volcengine.rs index 73092e29..91ab2b8a 100644 --- a/openless -all/app/src-tauri/src/asr/volcengine.rs +++ b/openless -all/app/src-tauri/src/asr/volcengine.rs @@ -449,10 +449,8 @@ impl AudioConsumer for VolcengineStreamingASR { if st.pending_audio.len() < TARGET_AUDIO_CHUNK_BYTES { None } else { - let chunk: Vec = st - .pending_audio - .drain(..TARGET_AUDIO_CHUNK_BYTES) - .collect(); + let chunk: Vec = + st.pending_audio.drain(..TARGET_AUDIO_CHUNK_BYTES).collect(); let seq = st.next_sequence; st.next_sequence += 1; st.bytes_sent += chunk.len(); diff --git a/openless -all/app/src-tauri/src/hotkey.rs b/openless -all/app/src-tauri/src/hotkey.rs index 0ff1acc4..4d0f3516 100644 --- a/openless -all/app/src-tauri/src/hotkey.rs +++ b/openless -all/app/src-tauri/src/hotkey.rs @@ -39,10 +39,7 @@ impl HotkeyMonitor { /// the OS-level hook installed (CGEventTap on macOS / rdev::listen otherwise). /// Returns Err if installation failed (typically Accessibility not granted on macOS), /// so the caller can schedule a retry instead of silently dropping events. - pub fn start( - binding: HotkeyBinding, - tx: Sender, - ) -> anyhow::Result { + pub fn start(binding: HotkeyBinding, tx: Sender) -> anyhow::Result { let shared = Arc::new(Shared { binding: RwLock::new(binding), trigger_held: AtomicBool::new(false), @@ -293,9 +290,9 @@ mod platform { match trigger { HotkeyTrigger::LeftControl | HotkeyTrigger::RightControl => FLAG_MASK_CONTROL, HotkeyTrigger::RightCommand => FLAG_MASK_COMMAND, - HotkeyTrigger::LeftOption - | HotkeyTrigger::RightOption - | HotkeyTrigger::RightAlt => FLAG_MASK_ALTERNATE, + HotkeyTrigger::LeftOption | HotkeyTrigger::RightOption | HotkeyTrigger::RightAlt => { + FLAG_MASK_ALTERNATE + } HotkeyTrigger::Fn => FLAG_MASK_SECONDARY_FN, } } diff --git a/openless -all/app/src-tauri/src/insertion.rs b/openless -all/app/src-tauri/src/insertion.rs index 58f9ef02..266d3f05 100644 --- a/openless -all/app/src-tauri/src/insertion.rs +++ b/openless -all/app/src-tauri/src/insertion.rs @@ -68,7 +68,9 @@ fn simulate_paste() -> Result<(), String> { use enigo::{Direction, Enigo, Key, Keyboard, Settings}; let mut enigo = Enigo::new(&Settings::default()).map_err(|e| e.to_string())?; let modifier = Key::Control; - enigo.key(modifier, Direction::Press).map_err(|e| e.to_string())?; + enigo + .key(modifier, Direction::Press) + .map_err(|e| e.to_string())?; let press_v = enigo.key(Key::Unicode('v'), Direction::Click); let release_modifier = enigo.key(modifier, Direction::Release); if let Err(e) = release_modifier { diff --git a/openless -all/app/src-tauri/src/persistence.rs b/openless -all/app/src-tauri/src/persistence.rs index 2f1ecfd7..14245102 100644 --- a/openless -all/app/src-tauri/src/persistence.rs +++ b/openless -all/app/src-tauri/src/persistence.rs @@ -70,8 +70,7 @@ fn data_dir() -> Result { } fn ensure_dir(dir: &Path) -> Result<()> { - fs::create_dir_all(dir) - .with_context(|| format!("create dir failed: {}", dir.display()))?; + fs::create_dir_all(dir).with_context(|| format!("create dir failed: {}", dir.display()))?; Ok(()) } @@ -83,8 +82,7 @@ fn atomic_write(path: &Path, contents: &[u8]) -> Result<()> { let tmp_path = path.with_extension("tmp"); fs::write(&tmp_path, contents) .with_context(|| format!("write tmp failed: {}", tmp_path.display()))?; - fs::rename(&tmp_path, path) - .with_context(|| format!("rename failed: {}", path.display()))?; + fs::rename(&tmp_path, path).with_context(|| format!("rename failed: {}", path.display()))?; Ok(()) } @@ -92,8 +90,7 @@ fn read_or_default Deserialize<'de> + Default>(path: &Path) -> Resul if !path.exists() { return Ok(T::default()); } - let bytes = fs::read(path) - .with_context(|| format!("read failed: {}", path.display()))?; + let bytes = fs::read(path).with_context(|| format!("read failed: {}", path.display()))?; if bytes.is_empty() { return Ok(T::default()); } @@ -133,7 +130,9 @@ struct CredsRoot { providers: CredsProviders, } -fn credsroot_default_version() -> u32 { 1 } +fn credsroot_default_version() -> u32 { + 1 +} #[derive(Debug, Serialize, Deserialize, Clone)] struct CredsActive { @@ -152,8 +151,12 @@ impl Default for CredsActive { } } -fn creds_default_asr() -> String { "volcengine".into() } -fn creds_default_llm() -> String { "ark".into() } +fn creds_default_asr() -> String { + "volcengine".into() +} +fn creds_default_llm() -> String { + "ark".into() +} #[derive(Debug, Serialize, Deserialize, Default, Clone)] struct CredsProviders { @@ -215,7 +218,11 @@ impl CredsLlmEntry { && self.baseURL.as_deref().unwrap_or("").is_empty() && self.model.as_deref().unwrap_or("").is_empty() && self.temperature.is_none() - && self.extraHeaders.as_ref().map(|h| h.is_empty()).unwrap_or(true) + && self + .extraHeaders + .as_ref() + .map(|h| h.is_empty()) + .unwrap_or(true) } } @@ -225,19 +232,22 @@ fn credentials_path() -> Result { #[cfg(target_os = "windows")] { let appdata = std::env::var("APPDATA").context("APPDATA not set")?; - return Ok(PathBuf::from(appdata).join("OpenLess").join(LEGACY_CREDS_FILE)); + return Ok(PathBuf::from(appdata) + .join("OpenLess") + .join(LEGACY_CREDS_FILE)); } #[cfg(not(target_os = "windows"))] { let home = std::env::var("HOME").context("HOME not set")?; - Ok(PathBuf::from(home).join(LEGACY_CREDS_DIR).join(LEGACY_CREDS_FILE)) + Ok(PathBuf::from(home) + .join(LEGACY_CREDS_DIR) + .join(LEGACY_CREDS_FILE)) } } fn ensure_credentials_dir(path: &Path) -> Result<()> { if let Some(dir) = path.parent() { - fs::create_dir_all(dir) - .with_context(|| format!("create dir {} failed", dir.display()))?; + fs::create_dir_all(dir).with_context(|| format!("create dir {} failed", dir.display()))?; // 0700 on parent so other users can't peek #[cfg(unix)] { @@ -279,8 +289,7 @@ fn save_credentials(root: &CredsRoot) -> Result<()> { let json = serde_json::to_vec_pretty(&cleaned).context("encode credentials failed")?; let tmp = path.with_extension("json.tmp"); fs::write(&tmp, &json).with_context(|| format!("write {} failed", tmp.display()))?; - fs::rename(&tmp, &path) - .with_context(|| format!("rename to {} failed", path.display()))?; + fs::rename(&tmp, &path).with_context(|| format!("rename to {} failed", path.display()))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -294,7 +303,9 @@ fn lookup_account(root: &CredsRoot, account: CredentialAccount) -> Option| s.as_ref().filter(|v| !v.is_empty()).cloned(); match account { - CredentialAccount::VolcengineAppKey => asr.and_then(|e| pick(&e.appKey).or_else(|| pick(&e.apiKey))), + CredentialAccount::VolcengineAppKey => { + asr.and_then(|e| pick(&e.appKey).or_else(|| pick(&e.apiKey))) + } CredentialAccount::VolcengineAccessKey => asr.and_then(|e| pick(&e.accessKey)), CredentialAccount::VolcengineResourceId => asr.and_then(|e| pick(&e.resourceId)), CredentialAccount::ArkApiKey => llm.and_then(|e| pick(&e.apiKey)), @@ -570,7 +581,11 @@ impl CredentialsVault { pub fn set(account: CredentialAccount, value: &str) -> Result<()> { let mut root = load_credentials(); - let v = if value.is_empty() { None } else { Some(value.to_string()) }; + let v = if value.is_empty() { + None + } else { + Some(value.to_string()) + }; write_account(&mut root, account, v); save_credentials(&root) } @@ -593,4 +608,3 @@ impl CredentialsVault { } } } - diff --git a/openless -all/app/src-tauri/src/types.rs b/openless -all/app/src-tauri/src/types.rs index 7fdaa8c8..ef98afa6 100644 --- a/openless -all/app/src-tauri/src/types.rs +++ b/openless -all/app/src-tauri/src/types.rs @@ -71,7 +71,9 @@ pub struct DictionaryEntry { pub created_at: String, } -fn default_true() -> bool { true } +fn default_true() -> bool { + true +} #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -160,7 +162,7 @@ pub enum CapsuleState { #[serde(rename_all = "camelCase")] pub struct CapsulePayload { pub state: CapsuleState, - pub level: f32, // 0..1 RMS + pub level: f32, // 0..1 RMS pub elapsed_ms: u64, pub message: Option, pub inserted_chars: Option,