├── .cargo
└── config.toml
├── src-tauri
├── rustfmt.toml
├── src
│ ├── helpers
│ │ ├── mod.rs
│ │ └── clamshell.rs
│ ├── audio_toolkit
│ │ ├── constants.rs
│ │ ├── mod.rs
│ │ ├── audio
│ │ │ ├── mod.rs
│ │ │ ├── utils.rs
│ │ │ ├── device.rs
│ │ │ └── resampler.rs
│ │ ├── utils.rs
│ │ └── vad
│ │ │ ├── mod.rs
│ │ │ ├── silero.rs
│ │ │ └── smoothed.rs
│ ├── managers
│ │ └── mod.rs
│ ├── main.rs
│ ├── llm_client.rs
│ ├── commands
│ │ ├── transcription.rs
│ │ ├── history.rs
│ │ └── mod.rs
│ ├── utils.rs
│ └── signal_handle.rs
├── build.rs
├── icons
│ ├── 32x32.png
│ ├── 64x64.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ ├── logo.png
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── StoreLogo.png
│ ├── Square30x30Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square310x310Logo.png
│ ├── ios
│ │ ├── AppIcon-512@2x.png
│ │ ├── AppIcon-20x20@1x.png
│ │ ├── AppIcon-20x20@2x.png
│ │ ├── AppIcon-20x20@3x.png
│ │ ├── AppIcon-29x29@1x.png
│ │ ├── AppIcon-29x29@2x.png
│ │ ├── AppIcon-29x29@3x.png
│ │ ├── AppIcon-40x40@1x.png
│ │ ├── AppIcon-40x40@2x.png
│ │ ├── AppIcon-40x40@3x.png
│ │ ├── AppIcon-60x60@2x.png
│ │ ├── AppIcon-60x60@3x.png
│ │ ├── AppIcon-76x76@1x.png
│ │ ├── AppIcon-76x76@2x.png
│ │ ├── AppIcon-20x20@2x-1.png
│ │ ├── AppIcon-29x29@2x-1.png
│ │ ├── AppIcon-40x40@2x-1.png
│ │ └── AppIcon-83.5x83.5@2x.png
│ └── android
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
│ │ └── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_launcher_foreground.png
├── resources
│ ├── handy.png
│ ├── pop_start.wav
│ ├── pop_stop.wav
│ ├── recording.png
│ ├── tray_idle.png
│ ├── marimba_start.wav
│ ├── marimba_stop.wav
│ ├── transcribing.png
│ ├── tray_idle_dark.png
│ ├── tray_recording.png
│ ├── tray_recording_dark.png
│ ├── tray_transcribing.png
│ ├── models
│ │ └── silero_vad_v4.onnx
│ ├── tray_transcribing_dark.png
│ └── default_settings.json
├── .gitignore
├── Info.plist
├── capabilities
│ ├── desktop.json
│ └── default.json
├── Entitlements.plist
├── gen
│ └── apple
│ │ └── PrivacyInfo.xcprivacy
├── tauri.conf.json
└── Cargo.toml
├── .prettierrc
├── src
├── vite-env.d.ts
├── components
│ ├── footer
│ │ ├── index.ts
│ │ └── Footer.tsx
│ ├── onboarding
│ │ ├── index.ts
│ │ ├── ModelCard.tsx
│ │ └── Onboarding.tsx
│ ├── update-checker
│ │ └── index.ts
│ ├── settings
│ │ ├── PostProcessingSettingsApi
│ │ │ ├── types.ts
│ │ │ ├── index.tsx
│ │ │ ├── ProviderSelect.tsx
│ │ │ ├── ApiKeyField.tsx
│ │ │ ├── BaseUrlField.tsx
│ │ │ └── ModelSelect.tsx
│ │ ├── PostProcessingSettingsPrompts.tsx
│ │ ├── debug
│ │ │ ├── index.ts
│ │ │ ├── WordCorrectionThreshold.tsx
│ │ │ ├── DebugPaths.tsx
│ │ │ ├── LogLevelSelector.tsx
│ │ │ ├── DebugSettings.tsx
│ │ │ └── LogDirectory.tsx
│ │ ├── VolumeSlider.tsx
│ │ ├── PushToTalk.tsx
│ │ ├── StartHidden.tsx
│ │ ├── AutostartToggle.tsx
│ │ ├── PostProcessingToggle.tsx
│ │ ├── UpdateChecksToggle.tsx
│ │ ├── MuteWhileRecording.tsx
│ │ ├── AlwaysOnMicrophone.tsx
│ │ ├── AppendTrailingSpace.tsx
│ │ ├── AudioFeedback.tsx
│ │ ├── advanced
│ │ │ └── AdvancedSettings.tsx
│ │ ├── general
│ │ │ └── GeneralSettings.tsx
│ │ ├── ShowOverlay.tsx
│ │ ├── HistoryLimit.tsx
│ │ ├── ClipboardHandling.tsx
│ │ ├── index.ts
│ │ ├── RecordingRetentionPeriod.tsx
│ │ ├── SoundPicker.tsx
│ │ ├── PasteMethod.tsx
│ │ ├── TranslateToEnglish.tsx
│ │ ├── MicrophoneSelector.tsx
│ │ ├── ModelUnloadTimeout.tsx
│ │ ├── AppDataDirectory.tsx
│ │ ├── OutputDeviceSelector.tsx
│ │ ├── about
│ │ │ └── AboutSettings.tsx
│ │ ├── ClamshellMicrophoneSelector.tsx
│ │ └── CustomWords.tsx
│ ├── shared
│ │ ├── index.ts
│ │ └── ProgressBar.tsx
│ ├── icons
│ │ ├── index.ts
│ │ ├── ResetIcon.tsx
│ │ ├── CancelIcon.tsx
│ │ └── MicrophoneIcon.tsx
│ ├── model-selector
│ │ ├── index.ts
│ │ ├── DownloadProgressDisplay.tsx
│ │ └── ModelStatusButton.tsx
│ ├── ui
│ │ ├── index.ts
│ │ ├── Badge.tsx
│ │ ├── SettingsGroup.tsx
│ │ ├── Textarea.tsx
│ │ ├── ResetButton.tsx
│ │ ├── Input.tsx
│ │ ├── Button.tsx
│ │ ├── ToggleSwitch.tsx
│ │ ├── Slider.tsx
│ │ ├── TextDisplay.tsx
│ │ └── Dropdown.tsx
│ ├── AccessibilityPermissions.tsx
│ └── Sidebar.tsx
├── main.tsx
├── overlay
│ ├── main.tsx
│ ├── index.html
│ ├── RecordingOverlay.css
│ └── RecordingOverlay.tsx
├── lib
│ ├── utils
│ │ └── format.ts
│ └── constants
│ │ └── languages.ts
├── App.css
├── hooks
│ └── useSettings.ts
└── App.tsx
├── .github
├── FUNDING.yml
├── workflows
│ ├── prettier.yml
│ ├── build-test.yml
│ └── release.yml
└── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── bug_report.md
│ └── feature_request.md
├── sponsor-images
├── epicenter.png
└── wordcab.png
├── .vscode
└── extensions.json
├── tsconfig.node.json
├── .prettierignore
├── index.html
├── .gitignore
├── tailwind.config.js
├── tsconfig.json
├── LICENSE
├── vite.config.ts
├── BUILD.md
├── package.json
├── CRUSH.md
└── AGENTS.md
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [build]
2 |
--------------------------------------------------------------------------------
/src-tauri/rustfmt.toml:
--------------------------------------------------------------------------------
1 | edition = "2021"
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf"
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/src/helpers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod clamshell;
2 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/footer/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./Footer";
2 |
--------------------------------------------------------------------------------
/src/components/onboarding/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./Onboarding";
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: cjpais
2 | custom: https://handy.computer/donate
3 |
--------------------------------------------------------------------------------
/src-tauri/src/audio_toolkit/constants.rs:
--------------------------------------------------------------------------------
1 | pub const WHISPER_SAMPLE_RATE: u32 = 16000;
2 |
--------------------------------------------------------------------------------
/src/components/update-checker/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./UpdateChecker";
2 |
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/64x64.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/logo.png
--------------------------------------------------------------------------------
/sponsor-images/epicenter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/sponsor-images/epicenter.png
--------------------------------------------------------------------------------
/sponsor-images/wordcab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/sponsor-images/wordcab.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/resources/handy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/handy.png
--------------------------------------------------------------------------------
/src-tauri/resources/pop_start.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/pop_start.wav
--------------------------------------------------------------------------------
/src-tauri/resources/pop_stop.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/pop_stop.wav
--------------------------------------------------------------------------------
/src-tauri/resources/recording.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/recording.png
--------------------------------------------------------------------------------
/src-tauri/resources/tray_idle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/tray_idle.png
--------------------------------------------------------------------------------
/src-tauri/src/managers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod audio;
2 | pub mod history;
3 | pub mod model;
4 | pub mod transcription;
5 |
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src-tauri/resources/marimba_start.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/marimba_start.wav
--------------------------------------------------------------------------------
/src-tauri/resources/marimba_stop.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/marimba_stop.wav
--------------------------------------------------------------------------------
/src-tauri/resources/transcribing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/transcribing.png
--------------------------------------------------------------------------------
/src-tauri/resources/tray_idle_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/tray_idle_dark.png
--------------------------------------------------------------------------------
/src-tauri/resources/tray_recording.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/tray_recording.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/resources/tray_recording_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/tray_recording_dark.png
--------------------------------------------------------------------------------
/src-tauri/resources/tray_transcribing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/tray_transcribing.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/resources/models/silero_vad_v4.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/models/silero_vad_v4.onnx
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsApi/types.ts:
--------------------------------------------------------------------------------
1 | export type ModelOption = {
2 | value: string;
3 | label: string;
4 | };
5 |
--------------------------------------------------------------------------------
/src-tauri/resources/tray_transcribing_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/resources/tray_transcribing_dark.png
--------------------------------------------------------------------------------
/src/components/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ProgressBar } from "./ProgressBar";
2 | export type { ProgressData } from "./ProgressBar";
3 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsApi/index.tsx:
--------------------------------------------------------------------------------
1 | export { PostProcessingSettingsApi } from "../post-processing/PostProcessingSettings";
2 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsPrompts.tsx:
--------------------------------------------------------------------------------
1 | export { PostProcessingSettingsPrompts } from "./post-processing/PostProcessingSettings";
2 |
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "tauri-apps.tauri-vscode",
4 | "rust-lang.rust-analyzer",
5 | "esbenp.prettier-vscode"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shanselman/Handy/main/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src/components/settings/debug/index.ts:
--------------------------------------------------------------------------------
1 | export { WordCorrectionThreshold } from "./WordCorrectionThreshold";
2 | export { LogDirectory } from "./LogDirectory";
3 | export { LogLevelSelector } from "./LogLevelSelector";
4 |
--------------------------------------------------------------------------------
/src/components/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as MicrophoneIcon } from "./MicrophoneIcon";
2 | export { default as TranscriptionIcon } from "./TranscriptionIcon";
3 | export { default as CancelIcon } from "./CancelIcon";
4 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | handy_app_lib::run()
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App";
4 |
5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
6 |
7 |
8 | ,
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/model-selector/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./ModelSelector";
2 | export { default as ModelStatusButton } from "./ModelStatusButton";
3 | export { default as ModelDropdown } from "./ModelDropdown";
4 | export { default as DownloadProgressDisplay } from "./DownloadProgressDisplay";
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules
3 | bun.lock
4 | package-lock.json
5 |
6 | # Build outputs
7 | dist
8 | target
9 | *.bundle.*
10 |
11 | # Tauri
12 | src-tauri/target
13 | src-tauri/gen
14 |
15 | # Generated files
16 | src/bindings.ts
17 |
18 | # Misc
19 | .DS_Store
20 | *.log
21 |
--------------------------------------------------------------------------------
/src/overlay/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import RecordingOverlay from "./RecordingOverlay";
4 |
5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
6 |
7 |
8 | ,
9 | );
10 |
--------------------------------------------------------------------------------
/src-tauri/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSMicrophoneUsageDescription
6 | Request microphone access to transcribe audio locally
7 |
8 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/desktop.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "desktop-capability",
3 | "platforms": ["macOS", "windows", "linux"],
4 | "windows": ["main"],
5 | "permissions": [
6 | "autostart:default",
7 | "global-shortcut:default",
8 | "autostart:default",
9 | "autostart:default",
10 | "updater:default"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/ui/index.ts:
--------------------------------------------------------------------------------
1 | export { Dropdown } from "./Dropdown";
2 | export { Slider } from "./Slider";
3 | export { ToggleSwitch } from "./ToggleSwitch";
4 | export { SettingContainer } from "./SettingContainer";
5 | export { SettingsGroup } from "./SettingsGroup";
6 | export { TextDisplay } from "./TextDisplay";
7 | export { Textarea } from "./Textarea";
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | handy
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src-tauri/Entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.device.microphone
6 |
7 | com.apple.security.device.audio-input
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src-tauri/src/audio_toolkit/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod audio;
2 | pub mod constants;
3 | pub mod text;
4 | pub mod utils;
5 | pub mod vad;
6 |
7 | pub use audio::{
8 | list_input_devices, list_output_devices, save_wav_file, AudioRecorder, CpalDeviceInfo,
9 | };
10 | pub use text::apply_custom_words;
11 | pub use utils::get_cpal_host;
12 | pub use vad::{SileroVad, VoiceActivityDetector};
13 |
--------------------------------------------------------------------------------
/src-tauri/src/audio_toolkit/audio/mod.rs:
--------------------------------------------------------------------------------
1 | // Re-export all audio components
2 | mod device;
3 | mod recorder;
4 | mod resampler;
5 | mod utils;
6 | mod visualizer;
7 |
8 | pub use device::{list_input_devices, list_output_devices, CpalDeviceInfo};
9 | pub use recorder::AudioRecorder;
10 | pub use resampler::FrameResampler;
11 | pub use utils::save_wav_file;
12 | pub use visualizer::AudioVisualiser;
13 |
--------------------------------------------------------------------------------
/src-tauri/resources/default_settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": {
3 | "transcribe": {
4 | "id": "transcribe",
5 | "name": "Transcribe Keyboard Shortcut",
6 | "description": "Converts your speech into text.",
7 | "default_binding": "Platform-specific: ctrl+space (Windows/Linux), alt+space (macOS)"
8 | }
9 | },
10 | "push_to_talk": true,
11 | "selected_language": "auto"
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | *.local.*
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
27 | /target/
28 | recording_*
29 | .crush/
30 |
--------------------------------------------------------------------------------
/.github/workflows/prettier.yml:
--------------------------------------------------------------------------------
1 | name: "prettier"
2 | on: [pull_request]
3 |
4 | jobs:
5 | prettier:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v4
9 |
10 | - uses: oven-sh/setup-bun@v1
11 | with:
12 | bun-version: latest
13 |
14 | - name: Install dependencies
15 | run: bun install --frozen-lockfile
16 |
17 | - name: Run prettier
18 | run: bun run format:check
19 |
--------------------------------------------------------------------------------
/src-tauri/src/audio_toolkit/utils.rs:
--------------------------------------------------------------------------------
1 | /// Returns the appropriate CPAL host for the current platform.
2 | /// On Linux, uses ALSA host. On other platforms, uses the default host.
3 | pub fn get_cpal_host() -> cpal::Host {
4 | #[cfg(target_os = "linux")]
5 | {
6 | cpal::host_from_id(cpal::HostId::Alsa).unwrap_or_else(|_| cpal::default_host())
7 | }
8 | #[cfg(not(target_os = "linux"))]
9 | {
10 | cpal::default_host()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {
6 | colors: {
7 | text: "var(--color-text)",
8 | background: "var(--color-background)",
9 | "logo-primary": "var(--color-logo-primary)",
10 | "logo-stroke": "var(--color-logo-stroke)",
11 | "text-stroke": "var(--color-text-stroke)",
12 | },
13 | },
14 | },
15 | plugins: [],
16 | };
17 |
--------------------------------------------------------------------------------
/src-tauri/gen/apple/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/ui/Badge.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface BadgeProps {
4 | children: React.ReactNode;
5 | variant?: "primary";
6 | className?: string;
7 | }
8 |
9 | const Badge: React.FC = ({
10 | children,
11 | variant = "primary",
12 | className = "",
13 | }) => {
14 | const variantClasses = {
15 | primary: "bg-logo-primary",
16 | };
17 |
18 | return (
19 |
22 | {children}
23 |
24 | );
25 | };
26 |
27 | export default Badge;
28 |
--------------------------------------------------------------------------------
/src/overlay/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Recording Overlay
6 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capabilities for the app",
5 | "windows": ["main", "recording_overlay"],
6 | "permissions": [
7 | "core:default",
8 | "opener:default",
9 | "store:default",
10 | "updater:default",
11 | "process:default",
12 | "global-shortcut:allow-is-registered",
13 | "global-shortcut:allow-register",
14 | "global-shortcut:allow-unregister",
15 | "global-shortcut:allow-unregister-all",
16 | "macos-permissions:default",
17 | "fs:read-files",
18 | "fs:allow-resource-read-recursive"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/utils/format.ts:
--------------------------------------------------------------------------------
1 | export const formatModelSize = (sizeMb: number | null | undefined): string => {
2 | if (!sizeMb || !Number.isFinite(sizeMb) || sizeMb <= 0) {
3 | return "Unknown size";
4 | }
5 |
6 | if (sizeMb >= 1024) {
7 | const sizeGb = sizeMb / 1024;
8 | const formatter = new Intl.NumberFormat(undefined, {
9 | minimumFractionDigits: sizeGb >= 10 ? 0 : 1,
10 | maximumFractionDigits: sizeGb >= 10 ? 0 : 1,
11 | });
12 | return `${formatter.format(sizeGb)} GB`;
13 | }
14 |
15 | const formatter = new Intl.NumberFormat(undefined, {
16 | minimumFractionDigits: sizeMb >= 100 ? 0 : 1,
17 | maximumFractionDigits: sizeMb >= 100 ? 0 : 1,
18 | });
19 |
20 | return `${formatter.format(sizeMb)} MB`;
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsApi/ProviderSelect.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Dropdown, type DropdownOption } from "../../ui/Dropdown";
3 |
4 | interface ProviderSelectProps {
5 | options: DropdownOption[];
6 | value: string;
7 | onChange: (value: string) => void;
8 | disabled?: boolean;
9 | }
10 |
11 | export const ProviderSelect: React.FC = React.memo(
12 | ({ options, value, onChange, disabled }) => {
13 | return (
14 |
21 | );
22 | },
23 | );
24 |
25 | ProviderSelect.displayName = "ProviderSelect";
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ✏️ Post-processing / Editing Transcripts
4 | url: https://github.com/cjpais/Handy/discussions/168
5 | about: Looking to edit, format, or post-process transcripts? Join this discussion
6 | - name: ⌨️ Keyboard Shortcuts / Hotkeys
7 | url: https://github.com/cjpais/Handy/discussions/211
8 | about: Want different keyboard shortcuts or hotkey configurations? Join this discussion
9 | - name: 💡 Feature Request or Idea
10 | url: https://github.com/cjpais/Handy/discussions
11 | about: Please post feature requests and ideas in our Discussions tab
12 | - name: 💬 General Discussion
13 | url: https://github.com/cjpais/Handy/discussions
14 | about: Ask questions and discuss Handy with the community
15 |
--------------------------------------------------------------------------------
/src-tauri/src/audio_toolkit/audio/utils.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use hound::{WavSpec, WavWriter};
3 | use log::debug;
4 | use std::path::Path;
5 |
6 | /// Save audio samples as a WAV file
7 | pub async fn save_wav_file>(file_path: P, samples: &[f32]) -> Result<()> {
8 | let spec = WavSpec {
9 | channels: 1,
10 | sample_rate: 16000,
11 | bits_per_sample: 16,
12 | sample_format: hound::SampleFormat::Int,
13 | };
14 |
15 | let mut writer = WavWriter::create(file_path.as_ref(), spec)?;
16 |
17 | // Convert f32 samples to i16 for WAV
18 | for sample in samples {
19 | let sample_i16 = (sample * i16::MAX as f32) as i16;
20 | writer.write_sample(sample_i16)?;
21 | }
22 |
23 | writer.finalize()?;
24 | debug!("Saved WAV file: {:?}", file_path.as_ref());
25 | Ok(())
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "types": ["node"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Path Aliases */
19 | "baseUrl": ".",
20 | "paths": {
21 | "@/bindings": ["./src/bindings.ts"]
22 | },
23 |
24 | /* Linting */
25 | "strict": true,
26 | "noUnusedLocals": false,
27 | "noUnusedParameters": false,
28 | "noFallthroughCasesInSwitch": true
29 | },
30 | "include": ["src"],
31 | "references": [{ "path": "./tsconfig.node.json" }]
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/settings/VolumeSlider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slider } from "../ui/Slider";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | export const VolumeSlider: React.FC<{ disabled?: boolean }> = ({
6 | disabled = false,
7 | }) => {
8 | const { getSetting, updateSetting } = useSettings();
9 | const audioFeedbackVolume = getSetting("audio_feedback_volume") ?? 0.5;
10 |
11 | return (
12 |
15 | updateSetting("audio_feedback_volume", value)
16 | }
17 | min={0}
18 | max={1}
19 | step={0.1}
20 | label="Volume"
21 | description="Adjust the volume of audio feedback sounds"
22 | descriptionMode="tooltip"
23 | grouped
24 | formatValue={(value) => `${Math.round(value * 100)}%`}
25 | disabled={disabled}
26 | />
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/ui/SettingsGroup.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface SettingsGroupProps {
4 | title?: string;
5 | description?: string;
6 | children: React.ReactNode;
7 | }
8 |
9 | export const SettingsGroup: React.FC = ({
10 | title,
11 | description,
12 | children,
13 | }) => {
14 | return (
15 |
16 | {title && (
17 |
18 |
19 | {title}
20 |
21 | {description && (
22 |
{description}
23 | )}
24 |
25 | )}
26 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/ui/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface TextareaProps
4 | extends React.TextareaHTMLAttributes {
5 | variant?: "default" | "compact";
6 | }
7 |
8 | export const Textarea: React.FC = ({
9 | className = "",
10 | variant = "default",
11 | ...props
12 | }) => {
13 | const baseClasses =
14 | "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left transition-[background-color,border-color] duration-150 hover:bg-logo-primary/10 hover:border-logo-primary focus:outline-none focus:bg-logo-primary/10 focus:border-logo-primary resize-y";
15 |
16 | const variantClasses = {
17 | default: "px-3 py-2 min-h-[100px]",
18 | compact: "px-2 py-1 min-h-[80px]",
19 | };
20 |
21 | return (
22 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src-tauri/src/audio_toolkit/vad/mod.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 |
3 | pub enum VadFrame<'a> {
4 | /// Speech – may aggregate several frames (prefill + current + hangover)
5 | Speech(&'a [f32]),
6 | /// Non-speech (silence, noise). Down-stream code can ignore it.
7 | Noise,
8 | }
9 |
10 | impl<'a> VadFrame<'a> {
11 | #[inline]
12 | pub fn is_speech(&self) -> bool {
13 | matches!(self, VadFrame::Speech(_))
14 | }
15 | }
16 |
17 | pub trait VoiceActivityDetector: Send + Sync {
18 | /// Primary streaming API: feed one 30-ms frame, get keep/drop decision.
19 | fn push_frame<'a>(&'a mut self, frame: &'a [f32]) -> Result>;
20 |
21 | fn is_voice(&mut self, frame: &[f32]) -> Result {
22 | Ok(self.push_frame(frame)?.is_speech())
23 | }
24 |
25 | fn reset(&mut self) {}
26 | }
27 |
28 | mod silero;
29 | mod smoothed;
30 |
31 | pub use silero::SileroVad;
32 | pub use smoothed::SmoothedVad;
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve Handy
4 | title: "[BUG] "
5 | labels: ["bug"]
6 | assignees: ""
7 | ---
8 |
9 | ## Before You Submit
10 |
11 | **Please search [existing issues](https://github.com/cjpais/Handy/issues) to avoid duplicates.** Your bug may already be reported! Right now it's just me maintaining this project so many issues can be overwhelming! Help me out by checking first.
12 |
13 | ## Bug Description
14 |
15 | A clear and concise description of what the bug is.
16 |
17 | ## System Information
18 |
19 | **App Version:**
20 |
21 |
22 |
23 | **Operating System:**
24 |
25 |
26 |
27 | **CPU:**
28 |
29 |
30 |
31 | **GPU:**
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/settings/PushToTalk.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface PushToTalkProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const PushToTalk: React.FC = React.memo(
11 | ({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const pttEnabled = getSetting("push_to_talk") || false;
15 |
16 | return (
17 | updateSetting("push_to_talk", enabled)}
20 | isUpdating={isUpdating("push_to_talk")}
21 | label="Push To Talk"
22 | description="Hold to record, release to stop"
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | />
26 | );
27 | },
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/ui/ResetButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ResetIcon from "../icons/ResetIcon";
3 |
4 | interface ResetButtonProps {
5 | onClick: () => void;
6 | disabled?: boolean;
7 | className?: string;
8 | ariaLabel?: string;
9 | children?: React.ReactNode;
10 | }
11 |
12 | export const ResetButton: React.FC = React.memo(
13 | ({ onClick, disabled = false, className = "", ariaLabel, children }) => (
14 |
27 | ),
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/settings/StartHidden.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface StartHiddenProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const StartHidden: React.FC = React.memo(
11 | ({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const startHidden = getSetting("start_hidden") ?? false;
15 |
16 | return (
17 | updateSetting("start_hidden", enabled)}
20 | isUpdating={isUpdating("start_hidden")}
21 | label="Start Hidden"
22 | description="Launch to system tray without opening the window."
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | tooltipPosition="bottom"
26 | />
27 | );
28 | },
29 | );
30 |
--------------------------------------------------------------------------------
/src/components/settings/AutostartToggle.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface AutostartToggleProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const AutostartToggle: React.FC = React.memo(
11 | ({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const autostartEnabled = getSetting("autostart_enabled") ?? false;
15 |
16 | return (
17 | updateSetting("autostart_enabled", enabled)}
20 | isUpdating={isUpdating("autostart_enabled")}
21 | label="Launch on Startup"
22 | description="Automatically start Handy when you log in to your computer."
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | />
26 | );
27 | },
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/ui/Input.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface InputProps extends React.InputHTMLAttributes {
4 | variant?: "default" | "compact";
5 | }
6 |
7 | export const Input: React.FC = ({
8 | className = "",
9 | variant = "default",
10 | disabled,
11 | ...props
12 | }) => {
13 | const baseClasses =
14 | "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left transition-all duration-150";
15 |
16 | const interactiveClasses = disabled
17 | ? "opacity-60 cursor-not-allowed bg-mid-gray/10 border-mid-gray/40"
18 | : "hover:bg-logo-primary/10 hover:border-logo-primary focus:outline-none focus:bg-logo-primary/20 focus:border-logo-primary";
19 |
20 | const variantClasses = {
21 | default: "px-3 py-2",
22 | compact: "px-2 py-1",
23 | } as const;
24 |
25 | return (
26 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingToggle.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface PostProcessingToggleProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const PostProcessingToggle: React.FC =
11 | React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const enabled = getSetting("post_process_enabled") || false;
15 |
16 | return (
17 | updateSetting("post_process_enabled", enabled)}
20 | isUpdating={isUpdating("post_process_enabled")}
21 | label="Post Process"
22 | description="Enable post-processing of transcribed text using language models via OpenAI Compatible API."
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | />
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/settings/UpdateChecksToggle.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface UpdateChecksToggleProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const UpdateChecksToggle: React.FC = ({
11 | descriptionMode = "tooltip",
12 | grouped = false,
13 | }) => {
14 | const { getSetting, updateSetting, isUpdating } = useSettings();
15 | const updateChecksEnabled = getSetting("update_checks_enabled") ?? true;
16 |
17 | return (
18 | updateSetting("update_checks_enabled", enabled)}
21 | isUpdating={isUpdating("update_checks_enabled")}
22 | label="Check for Updates"
23 | description="Allow Handy to automatically check for updates and enable manual checks from the footer or tray menu."
24 | descriptionMode={descriptionMode}
25 | grouped={grouped}
26 | />
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/settings/MuteWhileRecording.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface MuteWhileRecordingToggleProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const MuteWhileRecording: React.FC =
11 | React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const muteEnabled = getSetting("mute_while_recording") ?? false;
15 |
16 | return (
17 | updateSetting("mute_while_recording", enabled)}
20 | isUpdating={isUpdating("mute_while_recording")}
21 | label="Mute While Recording"
22 | description="Automatically mute all sound output while Handy is recording, then restore it when finished."
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | />
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/settings/AlwaysOnMicrophone.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface AlwaysOnMicrophoneProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const AlwaysOnMicrophone: React.FC = React.memo(
11 | ({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const alwaysOnMode = getSetting("always_on_microphone") || false;
15 |
16 | return (
17 | updateSetting("always_on_microphone", enabled)}
20 | isUpdating={isUpdating("always_on_microphone")}
21 | label="Always-On Microphone"
22 | description="Keep microphone active for low latency recording. This may prevent your computer from sleeping."
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | />
26 | );
27 | },
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/settings/AppendTrailingSpace.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 |
5 | interface AppendTrailingSpaceProps {
6 | descriptionMode?: "inline" | "tooltip";
7 | grouped?: boolean;
8 | }
9 |
10 | export const AppendTrailingSpace: React.FC =
11 | React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
12 | const { getSetting, updateSetting, isUpdating } = useSettings();
13 |
14 | const enabled = getSetting("append_trailing_space") ?? false;
15 |
16 | return (
17 | updateSetting("append_trailing_space", enabled)}
20 | isUpdating={isUpdating("append_trailing_space")}
21 | label="Append Trailing Space"
22 | description="Automatically add a space at the end of transcribed text, making it easier to dictate multiple sentences in a row."
23 | descriptionMode={descriptionMode}
24 | grouped={grouped}
25 | />
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsApi/ApiKeyField.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Input } from "../../ui/Input";
3 |
4 | interface ApiKeyFieldProps {
5 | value: string;
6 | onBlur: (value: string) => void;
7 | disabled: boolean;
8 | placeholder?: string;
9 | className?: string;
10 | }
11 |
12 | export const ApiKeyField: React.FC = React.memo(
13 | ({ value, onBlur, disabled, placeholder, className = "" }) => {
14 | const [localValue, setLocalValue] = useState(value);
15 |
16 | // Sync with prop changes
17 | React.useEffect(() => {
18 | setLocalValue(value);
19 | }, [value]);
20 |
21 | return (
22 | setLocalValue(event.target.value)}
26 | onBlur={() => onBlur(localValue)}
27 | placeholder={placeholder}
28 | variant="compact"
29 | disabled={disabled}
30 | className={`flex-1 min-w-[320px] ${className}`}
31 | />
32 | );
33 | },
34 | );
35 |
36 | ApiKeyField.displayName = "ApiKeyField";
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 CJ Pais
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/icons/ResetIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface ResetIconProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | className?: string;
8 | }
9 |
10 | const ResetIcon: React.FC = ({
11 | width = 20,
12 | height = 20,
13 | className = "",
14 | }) => {
15 | return (
16 |
34 | );
35 | };
36 |
37 | export default ResetIcon;
38 |
--------------------------------------------------------------------------------
/src/components/settings/debug/WordCorrectionThreshold.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slider } from "../../ui/Slider";
3 | import { useSettings } from "../../../hooks/useSettings";
4 |
5 | interface WordCorrectionThresholdProps {
6 | descriptionMode?: "tooltip" | "inline";
7 | grouped?: boolean;
8 | }
9 |
10 | export const WordCorrectionThreshold: React.FC<
11 | WordCorrectionThresholdProps
12 | > = ({ descriptionMode = "tooltip", grouped = false }) => {
13 | const { settings, updateSetting } = useSettings();
14 |
15 | const handleThresholdChange = (value: number) => {
16 | updateSetting("word_correction_threshold", value);
17 | };
18 |
19 | return (
20 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/settings/AudioFeedback.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToggleSwitch } from "../ui/ToggleSwitch";
3 | import { useSettings } from "../../hooks/useSettings";
4 | import { VolumeSlider } from "./VolumeSlider";
5 | import { SoundPicker } from "./SoundPicker";
6 |
7 | interface AudioFeedbackProps {
8 | descriptionMode?: "inline" | "tooltip";
9 | grouped?: boolean;
10 | }
11 |
12 | export const AudioFeedback: React.FC = React.memo(
13 | ({ descriptionMode = "tooltip", grouped = false }) => {
14 | const { getSetting, updateSetting, isUpdating } = useSettings();
15 | const audioFeedbackEnabled = getSetting("audio_feedback") || false;
16 |
17 | return (
18 |
19 | updateSetting("audio_feedback", enabled)}
22 | isUpdating={isUpdating("audio_feedback")}
23 | label="Audio Feedback"
24 | description="Play sound when recording starts and stops"
25 | descriptionMode={descriptionMode}
26 | grouped={grouped}
27 | />
28 |
29 | );
30 | },
31 | );
32 |
--------------------------------------------------------------------------------
/src-tauri/src/llm_client.rs:
--------------------------------------------------------------------------------
1 | use crate::settings::PostProcessProvider;
2 | use async_openai::{config::OpenAIConfig, Client};
3 |
4 | /// Create an OpenAI-compatible client configured for the given provider
5 | pub fn create_client(
6 | provider: &PostProcessProvider,
7 | api_key: String,
8 | ) -> Result, String> {
9 | let base_url = provider.base_url.trim_end_matches('/');
10 | let config = OpenAIConfig::new()
11 | .with_api_base(base_url)
12 | .with_api_key(api_key);
13 |
14 | // Create client with Anthropic-specific header if needed
15 | let client = if provider.id == "anthropic" {
16 | let mut headers = reqwest::header::HeaderMap::new();
17 | headers.insert(
18 | "anthropic-version",
19 | reqwest::header::HeaderValue::from_static("2023-06-01"),
20 | );
21 |
22 | let http_client = reqwest::Client::builder()
23 | .default_headers(headers)
24 | .build()
25 | .map_err(|e| format!("Failed to build HTTP client: {}", e))?;
26 |
27 | Client::with_config(config).with_http_client(http_client)
28 | } else {
29 | Client::with_config(config)
30 | };
31 |
32 | Ok(client)
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { getVersion } from "@tauri-apps/api/app";
3 |
4 | import ModelSelector from "../model-selector";
5 | import UpdateChecker from "../update-checker";
6 |
7 | const Footer: React.FC = () => {
8 | const [version, setVersion] = useState("");
9 |
10 | useEffect(() => {
11 | const fetchVersion = async () => {
12 | try {
13 | const appVersion = await getVersion();
14 | setVersion(appVersion);
15 | } catch (error) {
16 | console.error("Failed to get app version:", error);
17 | setVersion("0.1.2");
18 | }
19 | };
20 |
21 | fetchVersion();
22 | }, []);
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 | {/* Update Status */}
32 |
33 |
34 | •
35 | v{version}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default Footer;
43 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsApi/BaseUrlField.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Input } from "../../ui/Input";
3 |
4 | interface BaseUrlFieldProps {
5 | value: string;
6 | onBlur: (value: string) => void;
7 | disabled: boolean;
8 | placeholder?: string;
9 | className?: string;
10 | }
11 |
12 | export const BaseUrlField: React.FC = React.memo(
13 | ({ value, onBlur, disabled, placeholder, className = "" }) => {
14 | const [localValue, setLocalValue] = useState(value);
15 |
16 | // Sync with prop changes
17 | React.useEffect(() => {
18 | setLocalValue(value);
19 | }, [value]);
20 |
21 | const disabledMessage = disabled
22 | ? "Base URL is managed by the selected provider."
23 | : undefined;
24 |
25 | return (
26 | setLocalValue(event.target.value)}
30 | onBlur={() => onBlur(localValue)}
31 | placeholder={placeholder}
32 | variant="compact"
33 | disabled={disabled}
34 | className={`flex-1 min-w-[360px] ${className}`}
35 | title={disabledMessage}
36 | />
37 | );
38 | },
39 | );
40 |
41 | BaseUrlField.displayName = "BaseUrlField";
42 |
--------------------------------------------------------------------------------
/src/components/settings/debug/DebugPaths.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { SettingContainer } from "../../ui/SettingContainer";
3 |
4 | interface DebugPathsProps {
5 | descriptionMode?: "tooltip" | "inline";
6 | grouped?: boolean;
7 | }
8 |
9 | export const DebugPaths: React.FC = ({
10 | descriptionMode = "inline",
11 | grouped = false,
12 | }) => {
13 | return (
14 |
20 |
21 |
22 | App Data:{" "}
23 | %APPDATA%/handy
24 |
25 |
26 | Models:{" "}
27 | %APPDATA%/handy/models
28 |
29 |
30 | Settings:{" "}
31 |
32 | %APPDATA%/handy/settings_store.json
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/transcription.rs:
--------------------------------------------------------------------------------
1 | use crate::managers::transcription::TranscriptionManager;
2 | use crate::settings::{get_settings, write_settings, ModelUnloadTimeout};
3 | use serde::Serialize;
4 | use specta::Type;
5 | use tauri::{AppHandle, State};
6 |
7 | #[derive(Serialize, Type)]
8 | pub struct ModelLoadStatus {
9 | is_loaded: bool,
10 | current_model: Option,
11 | }
12 |
13 | #[tauri::command]
14 | #[specta::specta]
15 | pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) {
16 | let mut settings = get_settings(&app);
17 | settings.model_unload_timeout = timeout;
18 | write_settings(&app, settings);
19 | }
20 |
21 | #[tauri::command]
22 | #[specta::specta]
23 | pub fn get_model_load_status(
24 | transcription_manager: State,
25 | ) -> Result {
26 | Ok(ModelLoadStatus {
27 | is_loaded: transcription_manager.is_model_loaded(),
28 | current_model: transcription_manager.get_current_model(),
29 | })
30 | }
31 |
32 | #[tauri::command]
33 | #[specta::specta]
34 | pub fn unload_model_manually(
35 | transcription_manager: State,
36 | ) -> Result<(), String> {
37 | transcription_manager
38 | .unload_model()
39 | .map_err(|e| format!("Failed to unload model: {}", e))
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/icons/CancelIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface CancelIconProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | className?: string;
8 | }
9 |
10 | const CancelIcon: React.FC = ({
11 | width = 24,
12 | height = 24,
13 | color = "#FAA2CA",
14 | className = "",
15 | }) => {
16 | return (
17 |
34 | );
35 | };
36 |
37 | export default CancelIcon;
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea or new feature for Handy
4 | title: ""
5 | labels: []
6 | assignees: ""
7 | ---
8 |
9 | ## 🎯 Feature Requests Go in Discussions!
10 |
11 | Thanks for your interest in improving Handy!
12 |
13 | **Please post feature requests in our [Discussions tab](https://github.com/cjpais/Handy/discussions) instead of opening an issue.**
14 |
15 | This helps us:
16 |
17 | - Keep issues focused on bugs and actionable tasks
18 | - Have more open-ended conversations about features
19 | - Gather community feedback and input
20 | - Better organize ideas and suggestions
21 |
22 | ### 🔥 Common Feature Requests
23 |
24 | Before creating a new discussion, check if your request is already being discussed:
25 |
26 | - **✏️ Post-processing / Editing Transcripts** - [Join Discussion #168](https://github.com/cjpais/Handy/discussions/168)
27 | - **⌨️ Keyboard Shortcuts / Hotkeys** - [Join Discussion #211](https://github.com/cjpais/Handy/discussions/211)
28 |
29 | ### How to submit a new feature request:
30 |
31 | 1. Go to [Discussions](https://github.com/cjpais/Handy/discussions)
32 | 2. Search to see if your idea already exists
33 | 3. Click "New discussion"
34 | 4. Choose the appropriate category (Ideas, Feature Requests, etc.)
35 | 5. Share your idea!
36 |
37 | ---
38 |
39 | **This issue will be closed and you'll be redirected to discussions.**
40 |
--------------------------------------------------------------------------------
/src/components/settings/advanced/AdvancedSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ShowOverlay } from "../ShowOverlay";
3 | import { TranslateToEnglish } from "../TranslateToEnglish";
4 | import { ModelUnloadTimeoutSetting } from "../ModelUnloadTimeout";
5 | import { CustomWords } from "../CustomWords";
6 | import { SettingsGroup } from "../../ui/SettingsGroup";
7 | import { StartHidden } from "../StartHidden";
8 | import { AutostartToggle } from "../AutostartToggle";
9 | import { PasteMethodSetting } from "../PasteMethod";
10 | import { ClipboardHandlingSetting } from "../ClipboardHandling";
11 |
12 | export const AdvancedSettings: React.FC = () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import tailwindcss from "@tailwindcss/vite";
4 | import { resolve } from "path";
5 |
6 | const host = process.env.TAURI_DEV_HOST;
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig(async () => ({
10 | plugins: [react(), tailwindcss()],
11 |
12 | // Path aliases
13 | resolve: {
14 | alias: {
15 | "@/bindings": resolve(__dirname, "./src/bindings.ts"),
16 | },
17 | },
18 |
19 | // Multiple entry points for main app and overlay
20 | build: {
21 | rollupOptions: {
22 | input: {
23 | main: resolve(__dirname, "index.html"),
24 | overlay: resolve(__dirname, "src/overlay/index.html"),
25 | },
26 | },
27 | },
28 |
29 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
30 | //
31 | // 1. prevent vite from obscuring rust errors
32 | clearScreen: false,
33 | // 2. tauri expects a fixed port, fail if that port is not available
34 | server: {
35 | port: 1420,
36 | strictPort: true,
37 | host: host || false,
38 | hmr: host
39 | ? {
40 | protocol: "ws",
41 | host,
42 | port: 1421,
43 | }
44 | : undefined,
45 | watch: {
46 | // 3. tell vite to ignore watching `src-tauri`
47 | ignored: ["**/src-tauri/**"],
48 | },
49 | },
50 | }));
51 |
--------------------------------------------------------------------------------
/src/components/model-selector/DownloadProgressDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ProgressBar, ProgressData } from "../shared";
3 |
4 | interface DownloadProgress {
5 | model_id: string;
6 | downloaded: number;
7 | total: number;
8 | percentage: number;
9 | }
10 |
11 | interface DownloadStats {
12 | startTime: number;
13 | lastUpdate: number;
14 | totalDownloaded: number;
15 | speed: number;
16 | }
17 |
18 | interface DownloadProgressDisplayProps {
19 | downloadProgress: Map;
20 | downloadStats: Map;
21 | className?: string;
22 | }
23 |
24 | const DownloadProgressDisplay: React.FC = ({
25 | downloadProgress,
26 | downloadStats,
27 | className = "",
28 | }) => {
29 | if (downloadProgress.size === 0) {
30 | return null;
31 | }
32 |
33 | const progressData: ProgressData[] = Array.from(
34 | downloadProgress.values(),
35 | ).map((progress) => {
36 | const stats = downloadStats.get(progress.model_id);
37 | return {
38 | id: progress.model_id,
39 | percentage: progress.percentage,
40 | speed: stats?.speed,
41 | };
42 | });
43 |
44 | return (
45 |
51 | );
52 | };
53 |
54 | export default DownloadProgressDisplay;
55 |
--------------------------------------------------------------------------------
/src/components/settings/general/GeneralSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { MicrophoneSelector } from "../MicrophoneSelector";
3 | import { LanguageSelector } from "../LanguageSelector";
4 | import { HandyShortcut } from "../HandyShortcut";
5 | import { SettingsGroup } from "../../ui/SettingsGroup";
6 | import { OutputDeviceSelector } from "../OutputDeviceSelector";
7 | import { PushToTalk } from "../PushToTalk";
8 | import { AudioFeedback } from "../AudioFeedback";
9 | import { useSettings } from "../../../hooks/useSettings";
10 | import { VolumeSlider } from "../VolumeSlider";
11 |
12 | export const GeneralSettings: React.FC = () => {
13 | const { audioFeedbackEnabled } = useSettings();
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/settings/ShowOverlay.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Dropdown } from "../ui/Dropdown";
3 | import { SettingContainer } from "../ui/SettingContainer";
4 | import { useSettings } from "../../hooks/useSettings";
5 | import type { OverlayPosition } from "@/bindings";
6 |
7 | interface ShowOverlayProps {
8 | descriptionMode?: "inline" | "tooltip";
9 | grouped?: boolean;
10 | }
11 |
12 | const overlayOptions = [
13 | { value: "none", label: "None" },
14 | { value: "bottom", label: "Bottom" },
15 | { value: "top", label: "Top" },
16 | ];
17 |
18 | export const ShowOverlay: React.FC = React.memo(
19 | ({ descriptionMode = "tooltip", grouped = false }) => {
20 | const { getSetting, updateSetting, isUpdating } = useSettings();
21 |
22 | const selectedPosition = (getSetting("overlay_position") ||
23 | "bottom") as OverlayPosition;
24 |
25 | return (
26 |
32 |
36 | updateSetting("overlay_position", value as OverlayPosition)
37 | }
38 | disabled={isUpdating("overlay_position")}
39 | />
40 |
41 | );
42 | },
43 | );
44 |
--------------------------------------------------------------------------------
/src/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface ButtonProps extends React.ButtonHTMLAttributes {
4 | variant?: "primary" | "secondary" | "danger" | "ghost";
5 | size?: "sm" | "md" | "lg";
6 | }
7 |
8 | export const Button: React.FC = ({
9 | children,
10 | className = "",
11 | variant = "primary",
12 | size = "md",
13 | ...props
14 | }) => {
15 | const baseClasses =
16 | "font-medium rounded border focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer";
17 |
18 | const variantClasses = {
19 | primary:
20 | "text-white bg-background-ui border-background-ui hover:bg-background-ui/80 hover:border-background-ui/80 focus:ring-1 focus:ring-background-ui",
21 | secondary:
22 | "bg-mid-gray/10 border-mid-gray/20 hover:bg-background-ui/30 hover:border-logo-primary focus:outline-none",
23 | danger:
24 | "text-white bg-red-600 border-mid-gray/20 hover:bg-red-700 hover:border-red-700 focus:ring-1 focus:ring-red-500",
25 | ghost:
26 | "text-current border-transparent hover:bg-mid-gray/10 hover:border-logo-primary focus:bg-mid-gray/20",
27 | };
28 |
29 | const sizeClasses = {
30 | sm: "px-2 py-1 text-xs",
31 | md: "px-4 py-[5px] text-sm",
32 | lg: "px-4 py-2 text-base",
33 | };
34 |
35 | return (
36 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/.github/workflows/build-test.yml:
--------------------------------------------------------------------------------
1 | name: "Build Test"
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | build-test:
7 | permissions:
8 | contents: write
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | include:
13 | - platform: "macos-latest" # for Arm based macs (M1 and above).
14 | args: "--target aarch64-apple-darwin"
15 | target: "aarch64-apple-darwin"
16 | - platform: "macos-latest" # for Intel based macs.
17 | args: "--target x86_64-apple-darwin"
18 | target: "x86_64-apple-darwin"
19 | - platform: "ubuntu-22.04" # Build .deb on 22.04
20 | args: "--bundles deb"
21 | target: "x86_64-unknown-linux-gnu"
22 | - platform: "ubuntu-24.04" # Build AppImage and RPM on 24.04
23 | args: "--bundles appimage,rpm"
24 | target: "x86_64-unknown-linux-gnu"
25 | - platform: "windows-latest"
26 | args: ""
27 | target: "x86_64-pc-windows-msvc"
28 | - platform: "windows-11-arm" # for ARM64 Windows runner
29 | args: "--target aarch64-pc-windows-msvc"
30 | target: "aarch64-pc-windows-msvc"
31 |
32 | uses: ./.github/workflows/build.yml
33 | with:
34 | platform: ${{ matrix.platform }}
35 | target: ${{ matrix.target }}
36 | build-args: ${{ matrix.args }}
37 | sign-binaries: true
38 | asset-prefix: "handy-test"
39 | upload-artifacts: true
40 | is-debug-build: ${{ contains(matrix.args, '--debug') }}
41 | secrets: inherit
42 |
--------------------------------------------------------------------------------
/src/components/settings/PostProcessingSettingsApi/ModelSelect.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { ModelOption } from "./types";
3 | import { Select } from "../../ui/Select";
4 |
5 | type ModelSelectProps = {
6 | value: string;
7 | options: ModelOption[];
8 | disabled?: boolean;
9 | placeholder?: string;
10 | isLoading?: boolean;
11 | onSelect: (value: string) => void;
12 | onCreate: (value: string) => void;
13 | onBlur: () => void;
14 | className?: string;
15 | };
16 |
17 | export const ModelSelect: React.FC = React.memo(
18 | ({
19 | value,
20 | options,
21 | disabled,
22 | placeholder,
23 | isLoading,
24 | onSelect,
25 | onCreate,
26 | onBlur,
27 | className = "flex-1 min-w-[360px]",
28 | }) => {
29 | const handleCreate = (inputValue: string) => {
30 | const trimmed = inputValue.trim();
31 | if (!trimmed) return;
32 | onCreate(trimmed);
33 | };
34 |
35 | const computedClassName = `text-sm ${className}`;
36 |
37 | return (
38 |