From 5b19b8a4795dd6b35ede3186dd8825f2e158222b Mon Sep 17 00:00:00 2001 From: Vlad Gerasimov Date: Sat, 2 Aug 2025 10:31:32 +0400 Subject: [PATCH 01/14] style: window height, logo width, group title size --- src-tauri/tauri.conf.json | 6 ++++-- src/App.tsx | 12 ++++++------ src/components/ui/SettingsGroup.tsx | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 988dae3..0fd843b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,7 +30,9 @@ "active": true, "createUpdaterArtifacts": true, "targets": "all", - "resources": ["resources/**/*"], + "resources": [ + "resources/**/*" + ], "license": "MIT", "icon": [ "icons/32x32.png", @@ -65,4 +67,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 99a670d..e3c80e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useEffect, useState } from "react"; import "./App.css"; -import { Settings } from "./components/settings/Settings"; -import HandyTextLogo from "./components/icons/HandyTextLogo"; import AccessibilityPermissions from "./components/AccessibilityPermissions"; import Footer from "./components/footer"; +import HandyTextLogo from "./components/icons/HandyTextLogo"; import Onboarding from "./components/onboarding"; -import { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; +import { Settings } from "./components/settings/Settings"; function App() { const [showOnboarding, setShowOnboarding] = useState(null); @@ -34,7 +34,7 @@ function App() { return (
- +
@@ -44,7 +44,7 @@ function App() { return (
- +
diff --git a/src/components/ui/SettingsGroup.tsx b/src/components/ui/SettingsGroup.tsx index f9266e7..4e588f9 100644 --- a/src/components/ui/SettingsGroup.tsx +++ b/src/components/ui/SettingsGroup.tsx @@ -15,7 +15,7 @@ export const SettingsGroup: React.FC = ({
{title && (
-

+

{title}

{description && ( From 10b3058359a6d48b426f75cbcb4c6c0b997c4dc9 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 10:13:51 -0700 Subject: [PATCH 02/14] only allow a single instance of handy to run --- src-tauri/Cargo.lock | 98 +++++++++++++++++--------------------------- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 3 ++ 3 files changed, 41 insertions(+), 62 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c129381..0a43bca 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -204,17 +204,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.4.0" @@ -2707,9 +2696,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -3026,9 +3015,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -5553,16 +5542,16 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.2.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c387d4d96690131dc46d1d2827df5c222b896a2bfeb15a16267229a55c50b5" +checksum = "50a0e5a4ce43cb3a733c3aef85e8478bc769dac743c615e26639cbf5d953faf7" dependencies = [ "serde", "serde_json", "tauri", "thiserror 2.0.11", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "zbus", ] @@ -6840,6 +6829,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -6888,10 +6886,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -6917,7 +6916,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d" dependencies = [ - "windows-targets 0.53.0", + "windows-targets 0.53.3", ] [[package]] @@ -7109,15 +7108,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.0" @@ -7270,16 +7260,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "xkbcommon" version = "0.8.0" @@ -7323,13 +7303,12 @@ dependencies = [ [[package]] name = "zbus" -version = "5.3.1" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2494e4b3f44d8363eef79a8a75fc0649efb710eef65a66b5e688a5eb4afe678a" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -7340,18 +7319,16 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-util", + "futures-lite", "hex", "nix", "ordered-stream", "serde", "serde_repr", - "static_assertions", "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.6.26", - "xdg-home", + "winnow 0.7.0", "zbus_macros", "zbus_names", "zvariant", @@ -7359,9 +7336,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.3.1" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445efc01929302aee95e2b25bbb62a301ea8a6369466e4278e58e7d1dfb23631" +checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", @@ -7374,13 +7351,13 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.1.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519629a3f80976d89c575895b05677cbc45eaf9f70d62a364d819ba646409cc8" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.6.26", + "winnow 0.7.0", "zvariant", ] @@ -7471,24 +7448,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.2.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", - "winnow 0.6.26", + "winnow 0.7.0", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.2.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", @@ -7499,14 +7475,14 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", "syn 2.0.96", - "winnow 0.6.26", + "winnow 0.7.0", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bb65c43..903ecf7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -73,5 +73,5 @@ whisper-rs = { version = "0.13.2", features = ["openblas", "vulkan"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2" tauri-plugin-global-shortcut = "2" -tauri-plugin-single-instance = "2" +tauri-plugin-single-instance = "2.3.2" tauri-plugin-updater = "2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8b5c5a7..ef76971 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -60,6 +60,9 @@ pub fn run() { env_logger::init(); tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + show_main_window(app); + })) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_updater::Builder::new().build()) From ee5ab6e2108029569bab40be50d7a6de68d6a546 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 10:28:17 -0700 Subject: [PATCH 03/14] tweak default height to 700 --- src-tauri/tauri.conf.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0fd843b..ceede8f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,9 +15,9 @@ { "title": "Handy", "width": 540, - "height": 720, + "height": 700, "minWidth": 540, - "minHeight": 720, + "minHeight": 700, "resizable": true, "maximizable": false } @@ -30,9 +30,7 @@ "active": true, "createUpdaterArtifacts": true, "targets": "all", - "resources": [ - "resources/**/*" - ], + "resources": ["resources/**/*"], "license": "MIT", "icon": [ "icons/32x32.png", @@ -67,4 +65,4 @@ ] } } -} \ No newline at end of file +} From 58c3d40713ed8a1deb8ea74f4f388d859d66198b Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 11:19:42 -0700 Subject: [PATCH 04/14] add dark assets for test --- src-tauri/resources/tray_idle_dark.png | Bin 0 -> 1522 bytes src-tauri/resources/tray_recording_dark.png | Bin 0 -> 1308 bytes src-tauri/resources/tray_transcribing_dark.png | Bin 0 -> 1461 bytes src-tauri/src/lib.rs | 9 ++++++++- 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src-tauri/resources/tray_idle_dark.png create mode 100644 src-tauri/resources/tray_recording_dark.png create mode 100644 src-tauri/resources/tray_transcribing_dark.png diff --git a/src-tauri/resources/tray_idle_dark.png b/src-tauri/resources/tray_idle_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..873b11b3a0c7689c1739835fa780585b3c5c0247 GIT binary patch literal 1522 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 Ik|WOi2g)%Yd;kCd literal 0 HcmV?d00001 diff --git a/src-tauri/resources/tray_transcribing_dark.png b/src-tauri/resources/tray_transcribing_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d9cd2f5b91e02b676a9b43c2c56a088877f067 GIT binary patch literal 1461 zcmY+Ec{mh!7{`AzSY}+gqGfVK2s<;bF*B|iCb`BnlC>!rXN_~3P`f6N++v9w6;FvA zr6R{TqD83O6=8%}S2@P8ES-62 z{!K#sJO^j%U3sqLeZtuufT*JYFk=DO=2gr&04|~cSU3v+E(ZYFkXtM#W8MOK&eqBj z?8m3Qe$JaDKln!FeI5q^OiNMN0<&{#a{E0>z)*p8NUt zX+x2CMee1EGz=k9$`V6(Mra~b6B@6M!bOpklDoWjI zm4rZ}p-Mu|xs&11XWof*N()36b~?>9bGozS_t!e|>mNz`AH={JLsb_H1lQrE$vM($ zZ{Zbjd24@x70U*jt-d@prP(*^vHMr}oyJ5#`{e2Fc!v2n1nlg%#AU=2ftsGu3A4er3^8rvhh8UH5_dFB90`BaOwo-IHUZM zhpJ${c}4eBF+xB?phz8$*EFm^mdq;%bdAcHA9>6zo>J`?p1Whsn)@KZZre%qDI=8` ze9}j#Iom2SRqv+09(wAB{}5;}PRp{euoWSzii_6=546+Ygm<@=qDIov?!hdsg)awn zOJq;~!S7h|R(^O~w8a6d9LxGURbgdGBy6#@^?anW*Rp;Ile_TZ!DCkh)4tCn!HEqT zQd6{!LLGlic&PWZrd)Gu!4Lr^^wqU$YM{rE^(U=CmK4M9*emSD*M7zl_(vk|GAE_h zLEYODh~zUb=WuGGH!}xH`92={AVx#Uc|xz=2^9eSv#+t~i`bVd^21VxTRftn+S}|( zcKu2s+#qtmg);JPajR*ucWrNXDwLxJPzpNn%_^g-aE-$nCO!JG78`97O?S;SxAz(g z#vJ0^kXXcWQ>|=}e^|d5=9e+Y3w=w&IzadUb-{~I>}GMOMVEQ&Nb=P{ZCy!u4~I<) zwj~J6=i^rOCs`A) z3kJuNk*ot&9jg?1RukBhkX+Ej!p3z?dgdn`69d-r>mk(B3$6;r zt{Bckx4x*U%AL2LHD?C~XBLN-XOnbZX`y2{)(gpAu+w&S-aVi9KkbYopS9h;#^vgp zC^xJ_G|=-Y_bGQDcoJo$hU8i{yzqLqZY24M#G<)D4kqXdHPtcXx2=8w6>46LLR}bR zJ4kQcCL^#J&uvLjUtBj)Pm0Pm4S2tjG@HXLB(1V!&8);n`={XtFjMJqw=4N)Xbs`? zGueJml&q+EIQLD@h#9zXda~=J0^(yw+Q)nwsu5Pwv`pqt)7DEf(Z$Wu)w%oPM9qba zNV6>#du3ixTjVE-3bKOsvcYuZlv`u3*z%UYk9#+sP`zfb7YIxeqO#VK!n#+=A#x)A z*ON`hzuZJjva&Va4?MTa%;YeMc*V-##??&Wn_fpewU?{RCibDBh=&ELb&@H(7o|og zouX5{=sq~_Fdtq3U6ihY4$4RerAtAfak@r0eSIwy3Wq`=jqj!Y0MLS|0ltwx0D3qc zqmTIxlvf;G;sF}}U7!che8TBow2=QZXd@h24~NEnXFNuasPGtIO(I)9BA$);7Ybr~ AH2?qr literal 0 HcmV?d00001 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ef76971..436ab3c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -78,9 +78,16 @@ pub fn run() { )) .manage(Mutex::new(ShortcutToggleStates::default())) .setup(move |app| { + // Log the current system theme + if let Some(main_window) = app.get_webview_window("main") { + if let Ok(theme) = main_window.theme() { + println!("System theme: {:?}", theme); + } + } + let tray = TrayIconBuilder::new() .icon(Image::from_path(app.path().resolve( - "resources/tray_idle.png", + "resources/tray_idle_dark.png", tauri::path::BaseDirectory::Resource, )?)?) .show_menu_on_left_click(true) From 0fa1f37474b6854f7e1967de68b78bbccdcfb28e Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 12:04:36 -0700 Subject: [PATCH 05/14] change the icon relative to the theme --- src-tauri/src/lib.rs | 28 +++++++++++++++++++++------- src-tauri/src/utils.rs | 29 ++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 436ab3c..11d54ba 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -78,16 +78,25 @@ pub fn run() { )) .manage(Mutex::new(ShortcutToggleStates::default())) .setup(move |app| { - // Log the current system theme - if let Some(main_window) = app.get_webview_window("main") { - if let Ok(theme) = main_window.theme() { - println!("System theme: {:?}", theme); - } - } + // Get the current theme to set the appropriate initial icon + let initial_theme = if let Some(main_window) = app.get_webview_window("main") { + main_window.theme().unwrap_or(tauri::Theme::Dark) + } else { + tauri::Theme::Dark + }; + + println!("Initial system theme: {:?}", initial_theme); + + // Choose the appropriate initial icon based on theme + let initial_icon_path = match initial_theme { + tauri::Theme::Dark => "resources/tray_idle.png", + tauri::Theme::Light => "resources/tray_idle_dark.png", + _ => "resources/tray_idle.png", // Default fallback + }; let tray = TrayIconBuilder::new() .icon(Image::from_path(app.path().resolve( - "resources/tray_idle_dark.png", + initial_icon_path, tauri::path::BaseDirectory::Resource, )?)?) .show_menu_on_left_click(true) @@ -158,6 +167,11 @@ pub fn run() { } } } + tauri::WindowEvent::ThemeChanged(theme) => { + println!("Theme changed to: {:?}", theme); + // Update tray icon to match new theme, maintaining idle state + utils::change_tray_icon(&window.app_handle(), utils::TrayIconState::Idle); + } _ => {} }) .invoke_handler(tauri::generate_handler![ diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index eae3d25..f7ffe34 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -14,7 +14,7 @@ use std::thread; use tauri::image::Image; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIcon; -use tauri::{AppHandle, Emitter, Manager, WebviewWindowBuilder}; +use tauri::{AppHandle, Emitter, Manager, Theme, WebviewWindowBuilder}; use tauri_plugin_clipboard_manager::ClipboardExt; /// Sends a paste command (Cmd+V or Ctrl+V) using platform-specific virtual key codes. @@ -82,13 +82,32 @@ pub enum TrayIconState { Transcribing, } +/// Gets the current system theme, defaulting to Dark if unavailable +fn get_current_theme(app: &AppHandle) -> Theme { + if let Some(main_window) = app.get_webview_window("main") { + main_window.theme().unwrap_or(Theme::Dark) + } else { + Theme::Dark + } +} + pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) { let tray = app.state::(); + let theme = get_current_theme(app); - let icon_path = match icon { - TrayIconState::Idle => "resources/tray_idle.png", - TrayIconState::Recording => "resources/tray_recording.png", - TrayIconState::Transcribing => "resources/tray_transcribing.png", + let icon_path = match (theme, &icon) { + // Dark theme uses regular icons (lighter colored for visibility) + (Theme::Dark, TrayIconState::Idle) => "resources/tray_idle.png", + (Theme::Dark, TrayIconState::Recording) => "resources/tray_recording.png", + (Theme::Dark, TrayIconState::Transcribing) => "resources/tray_transcribing.png", + // Light theme uses dark icons (darker colored for visibility) + (Theme::Light, TrayIconState::Idle) => "resources/tray_idle_dark.png", + (Theme::Light, TrayIconState::Recording) => "resources/tray_recording_dark.png", + (Theme::Light, TrayIconState::Transcribing) => "resources/tray_transcribing_dark.png", + // Fallback for any other theme variants + (_, TrayIconState::Idle) => "resources/tray_idle.png", + (_, TrayIconState::Recording) => "resources/tray_recording.png", + (_, TrayIconState::Transcribing) => "resources/tray_transcribing.png", }; let _ = tray.set_icon(Some( From 945acfed0776f83b0f541e256631a2f3949cfc4f Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 12:13:31 -0700 Subject: [PATCH 06/14] Release v0.3.4 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 381b230..623ede0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "handy-app", "private": true, - "version": "0.3.3", + "version": "0.3.4", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0a43bca..9b0bd94 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1992,7 +1992,7 @@ dependencies = [ [[package]] name = "handy" -version = "0.3.3" +version = "0.3.4" dependencies = [ "anyhow", "cpal", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 903ecf7..37c92c9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "handy" -version = "0.3.3" +version = "0.3.4" description = "Handy" authors = ["cjpais"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ceede8f..e72db97 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Handy", - "version": "0.3.3", + "version": "0.3.4", "identifier": "com.pais.handy", "build": { "beforeDevCommand": "bun run dev", From 86a1205619caef2325e176d1f4877b05b1022f75 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 18:25:30 -0700 Subject: [PATCH 07/14] add partial/resumable download capability --- src-tauri/src/commands/models.rs | 10 ++ src-tauri/src/lib.rs | 1 + src-tauri/src/managers/model.rs | 204 ++++++++++++++++++++++++++++--- src/lib/types.ts | 2 + 4 files changed, 201 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index 61eff63..5f617fc 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -108,6 +108,16 @@ pub async fn has_any_models_or_downloads( Ok(models.iter().any(|m| m.is_downloaded)) } +#[tauri::command] +pub async fn cancel_download( + model_manager: State<'_, Arc>, + model_id: String, +) -> Result<(), String> { + model_manager + .cancel_download(&model_id) + .map_err(|e| e.to_string()) +} + #[tauri::command] pub async fn get_recommended_first_model() -> Result { // Recommend small model for first-time users diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 11d54ba..59ecc47 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -190,6 +190,7 @@ pub fn run() { commands::models::get_model_info, commands::models::download_model, commands::models::delete_model, + commands::models::cancel_download, commands::models::set_active_model, commands::models::get_current_model, commands::models::get_transcription_model_status, diff --git a/src-tauri/src/managers/model.rs b/src-tauri/src/managers/model.rs index 943e77c..96a988c 100644 --- a/src-tauri/src/managers/model.rs +++ b/src-tauri/src/managers/model.rs @@ -18,6 +18,8 @@ pub struct ModelInfo { pub url: Option, pub size_mb: u64, pub is_downloaded: bool, + pub is_downloading: bool, + pub partial_size: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -61,6 +63,8 @@ impl ModelManager { url: Some("https://blob.handy.computer/ggml-small.bin".to_string()), size_mb: 244, is_downloaded: false, + is_downloading: false, + partial_size: 0, }, ); @@ -75,6 +79,8 @@ impl ModelManager { url: Some("https://blob.handy.computer/whisper-medium-q4_1.bin".to_string()), size_mb: 491, // Approximate size is_downloaded: false, + is_downloading: false, + partial_size: 0, }, ); @@ -88,6 +94,8 @@ impl ModelManager { url: Some("https://blob.handy.computer/ggml-large-v3-turbo.bin".to_string()), size_mb: 1600, // Approximate size is_downloaded: false, + is_downloading: false, + partial_size: 0, }, ); @@ -101,6 +109,8 @@ impl ModelManager { url: Some("https://blob.handy.computer/ggml-large-v3-q5_0.bin".to_string()), size_mb: 1080, // Approximate size is_downloaded: false, + is_downloading: false, + partial_size: 0, }, ); @@ -164,7 +174,17 @@ impl ModelManager { for model in models.values_mut() { let model_path = self.models_dir.join(&model.filename); + let partial_path = self.models_dir.join(format!("{}.partial", &model.filename)); + model.is_downloaded = model_path.exists(); + model.is_downloading = partial_path.exists(); + + // Get partial file size if it exists + if partial_path.exists() { + model.partial_size = partial_path.metadata().map(|m| m.len()).unwrap_or(0); + } else { + model.partial_size = 0; + } } Ok(()) @@ -209,37 +229,113 @@ impl ModelManager { .url .ok_or_else(|| anyhow::anyhow!("No download URL for model"))?; let model_path = self.models_dir.join(&model_info.filename); + let partial_path = self + .models_dir + .join(format!("{}.partial", &model_info.filename)); - // Don't download if downloaded version already exists + // Don't download if complete version already exists if model_path.exists() { - // Update status and return + // Clean up any partial file that might exist + if partial_path.exists() { + let _ = fs::remove_file(&partial_path); + } self.update_download_status()?; return Ok(()); } - println!("Downloading model {} from {}", model_id, url); + // Check if we have a partial download to resume + let resume_from = if partial_path.exists() { + let size = partial_path.metadata()?.len(); + println!("Resuming download of model {} from byte {}", model_id, size); + size + } else { + println!("Starting fresh download of model {} from {}", model_id, url); + 0 + }; - // Create HTTP client + // Mark as downloading + { + let mut models = self.available_models.lock().unwrap(); + if let Some(model) = models.get_mut(model_id) { + model.is_downloading = true; + } + } + + // Create HTTP client with range request for resuming let client = reqwest::Client::new(); - let response = client.get(&url).send().await?; + let mut request = client.get(&url); - if !response.status().is_success() { + if resume_from > 0 { + request = request.header("Range", format!("bytes={}-", resume_from)); + } + + let response = request.send().await?; + + // Check for success or partial content status + if !response.status().is_success() + && response.status() != reqwest::StatusCode::PARTIAL_CONTENT + { + // Mark as not downloading on error + { + let mut models = self.available_models.lock().unwrap(); + if let Some(model) = models.get_mut(model_id) { + model.is_downloading = false; + } + } return Err(anyhow::anyhow!( "Failed to download model: HTTP {}", response.status() )); } - let total_size = response.content_length().unwrap_or(0); - let mut downloaded = 0u64; + let total_size = if resume_from > 0 { + // For resumed downloads, add the resume point to content length + resume_from + response.content_length().unwrap_or(0) + } else { + response.content_length().unwrap_or(0) + }; + + let mut downloaded = resume_from; let mut stream = response.bytes_stream(); - // Create the file - let mut file = std::fs::File::create(&model_path)?; + // Open file for appending if resuming, or create new if starting fresh + let mut file = if resume_from > 0 { + std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&partial_path)? + } else { + std::fs::File::create(&partial_path)? + }; + + // Emit initial progress + let initial_progress = DownloadProgress { + model_id: model_id.to_string(), + downloaded, + total: total_size, + percentage: if total_size > 0 { + (downloaded as f64 / total_size as f64) * 100.0 + } else { + 0.0 + }, + }; + let _ = self + .app_handle + .emit("model-download-progress", &initial_progress); // Download with progress while let Some(chunk) = stream.next().await { - let chunk = chunk?; + let chunk = chunk.map_err(|e| { + // Mark as not downloading on error + { + let mut models = self.available_models.lock().unwrap(); + if let Some(model) = models.get_mut(model_id) { + model.is_downloading = false; + } + } + e + })?; + file.write_all(&chunk)?; downloaded += chunk.len() as u64; @@ -261,9 +357,20 @@ impl ModelManager { } file.flush()?; + drop(file); // Ensure file is closed before moving + + // Move partial file to final location + fs::rename(&partial_path, &model_path)?; // Update download status - self.update_download_status()?; + { + let mut models = self.available_models.lock().unwrap(); + if let Some(model) = models.get_mut(model_id) { + model.is_downloading = false; + model.is_downloaded = true; + model.partial_size = 0; + } + } // Emit completion event let _ = self.app_handle.emit("model-download-complete", model_id); @@ -272,6 +379,7 @@ impl ModelManager { "Successfully downloaded model {} to {:?}", model_id, model_path ); + Ok(()) } @@ -289,14 +397,32 @@ impl ModelManager { println!("ModelManager: Found model info: {:?}", model_info); let model_path = self.models_dir.join(&model_info.filename); + let partial_path = self + .models_dir + .join(format!("{}.partial", &model_info.filename)); println!("ModelManager: Model path: {:?}", model_path); + println!("ModelManager: Partial path: {:?}", partial_path); + let mut deleted_something = false; + + // Delete complete model file if it exists if model_path.exists() { println!("ModelManager: Deleting model file at: {:?}", model_path); fs::remove_file(&model_path)?; println!("ModelManager: Model file deleted successfully"); - } else { - return Err(anyhow::anyhow!("Model file not found")); + deleted_something = true; + } + + // Delete partial file if it exists + if partial_path.exists() { + println!("ModelManager: Deleting partial file at: {:?}", partial_path); + fs::remove_file(&partial_path)?; + println!("ModelManager: Partial file deleted successfully"); + deleted_something = true; + } + + if !deleted_something { + return Err(anyhow::anyhow!("No model files found to delete")); } // Update download status @@ -315,11 +441,57 @@ impl ModelManager { return Err(anyhow::anyhow!("Model not available: {}", model_id)); } + // Ensure we don't return partial files + if model_info.is_downloading { + return Err(anyhow::anyhow!( + "Model is currently downloading: {}", + model_id + )); + } + let model_path = self.models_dir.join(&model_info.filename); - if model_path.exists() { + let partial_path = self + .models_dir + .join(format!("{}.partial", &model_info.filename)); + + // Ensure we only return complete model files, not partial ones + if model_path.exists() && !partial_path.exists() { Ok(model_path) } else { - Err(anyhow::anyhow!("Model file not found: {}", model_id)) + Err(anyhow::anyhow!( + "Complete model file not found: {}", + model_id + )) } } + + pub fn cancel_download(&self, model_id: &str) -> Result<()> { + println!("ModelManager: cancel_download called for: {}", model_id); + + let _model_info = { + let models = self.available_models.lock().unwrap(); + models.get(model_id).cloned() + }; + + let _model_info = + _model_info.ok_or_else(|| anyhow::anyhow!("Model not found: {}", model_id))?; + + // Mark as not downloading + { + let mut models = self.available_models.lock().unwrap(); + if let Some(model) = models.get_mut(model_id) { + model.is_downloading = false; + } + } + + // Note: The actual download cancellation would need to be handled + // by the download task itself. This just updates the state. + // The partial file is kept so the download can be resumed later. + + // Update download status to reflect current state + self.update_download_status()?; + + println!("ModelManager: Download cancelled for: {}", model_id); + Ok(()) + } } diff --git a/src/lib/types.ts b/src/lib/types.ts index a122a4b..5ac069c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -51,6 +51,8 @@ export const ModelInfoSchema = z.object({ description: z.string(), size_mb: z.number(), is_downloaded: z.boolean(), + is_downloading: z.boolean(), + partial_size: z.number(), }); export type ModelInfo = z.infer; From 98eb336ab09d12ef556987b54089aa8fb166c11e Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 18:43:59 -0700 Subject: [PATCH 08/14] disallow invalid bindings from frontend --- src-tauri/src/lib.rs | 2 + src-tauri/src/shortcut.rs | 51 +++++++++++++++++++++++ src/components/settings/HandyShortcut.tsx | 29 ++++++++++++- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 59ecc47..68b5b52 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -183,6 +183,8 @@ pub fn run() { shortcut::change_selected_language_setting, shortcut::change_show_overlay_setting, shortcut::change_debug_mode_setting, + shortcut::suspend_binding, + shortcut::resume_binding, trigger_update_check, commands::cancel_operation, commands::get_app_dir_path, diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index d2d609b..cae54f3 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -56,6 +56,9 @@ pub fn change_binding( }); } + // Validate the new shortcut before we touch the current registration + validate_shortcut_string(&binding)?; + // Create an updated binding let mut updated_binding = binding_to_modify; updated_binding.current_binding = binding; @@ -143,13 +146,61 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St Ok(()) } +/// Determine whether a shortcut string contains at least one non-modifier key. +/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow +/// modifier-only combos (e.g. "ctrl" or "ctrl+shift"). +fn validate_shortcut_string(raw: &str) -> Result<(), String> { + let modifiers = [ + "ctrl", "control", "shift", "alt", "option", "meta", "command", "cmd", "super", "win", + "windows", + ]; + let has_non_modifier = raw + .split('+') + .any(|part| !modifiers.contains(&part.trim().to_lowercase().as_str())); + if has_non_modifier { + Ok(()) + } else { + Err("Shortcut must contain at least one non-modifier key".into()) + } +} + +/// Temporarily unregister a binding while the user is editing it in the UI. +/// This avoids firing the action while keys are being recorded. +#[tauri::command] +pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> { + if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { + _unregister_shortcut(&app, b)?; + } + Ok(()) +} + +/// Re-register the binding after the user has finished editing. +#[tauri::command] +pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> { + if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { + _register_shortcut(&app, b)?; + } + Ok(()) +} + fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { + // Validate human-level rules first + validate_shortcut_string(&binding.current_binding)?; + // Parse shortcut and return error if it fails let shortcut = match binding.current_binding.parse::() { Ok(s) => s, Err(e) => return Err(format!("Failed to parse shortcut: {}", e)), }; + // Prevent duplicate registrations that would silently shadow one another + if app.global_shortcut().is_registered(shortcut) { + return Err(format!( + "Shortcut '{}' is already in use", + binding.current_binding + )); + } + // Clone binding.id for use in the closure let binding_id_for_closure = binding.id.clone(); diff --git a/src/components/settings/HandyShortcut.tsx b/src/components/settings/HandyShortcut.tsx index 6ea5459..2819a30 100644 --- a/src/components/settings/HandyShortcut.tsx +++ b/src/components/settings/HandyShortcut.tsx @@ -5,6 +5,7 @@ import { getKeyName } from "../../lib/utils/keyboard"; import ResetIcon from "../icons/ResetIcon"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; +import { invoke } from "@tauri-apps/api/core"; interface HandyShortcutProps { descriptionMode?: "inline" | "tooltip"; @@ -82,6 +83,20 @@ export const HandyShortcut: React.FC = ({ // Keyboard event listeners const handleKeyDown = async (e: KeyboardEvent) => { + if (e.repeat) return; // ignore auto-repeat + if (e.key === "Escape") { + // Cancel recording + if (editingShortcutId) { + await invoke("resume_binding", { id: editingShortcutId }).catch( + console.error, + ); + } + setEditingShortcutId(null); + setKeyPressed([]); + setRecordedKeys([]); + setOriginalBinding(""); + return; + } e.preventDefault(); // Get the key and normalize it (unify left/right modifiers) @@ -118,6 +133,10 @@ export const HandyShortcut: React.FC = ({ if (editingShortcutId && bindings[editingShortcutId]) { try { await updateBinding(editingShortcutId, newShortcut); + // Re-register the shortcut now that recording is finished + await invoke("resume_binding", { id: editingShortcutId }).catch( + console.error, + ); } catch (error) { console.error("Failed to change binding:", error); } @@ -136,6 +155,11 @@ export const HandyShortcut: React.FC = ({ const activeElement = shortcutRefs.current.get(editingShortcutId); if (activeElement && !activeElement.contains(e.target as Node)) { // Cancel shortcut recording - the hook will handle rollback + if (editingShortcutId) { + invoke("resume_binding", { id: editingShortcutId }).catch( + console.error, + ); + } setEditingShortcutId(null); setKeyPressed([]); setRecordedKeys([]); @@ -162,9 +186,12 @@ export const HandyShortcut: React.FC = ({ ]); // Start recording a new shortcut - const startRecording = (id: string) => { + const startRecording = async (id: string) => { if (editingShortcutId === id) return; // Already editing this shortcut + // Suspend current binding to avoid firing while recording + await invoke("suspend_binding", { id }).catch(console.error); + // Store the original binding to restore if canceled setOriginalBinding(bindings[id]?.current_binding || ""); setEditingShortcutId(id); From 32a630cc5272a03bb49dcfe993f5315f6d594322 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 2 Aug 2025 19:03:56 -0700 Subject: [PATCH 09/14] potentially fix cross-platform keybinding handling Naming at the very least. --- src/components/settings/HandyShortcut.tsx | 89 ++++++++++------------ src/lib/utils/keyboard.ts | 91 ++++++++++++++++++----- 2 files changed, 110 insertions(+), 70 deletions(-) diff --git a/src/components/settings/HandyShortcut.tsx b/src/components/settings/HandyShortcut.tsx index 2819a30..b4ebd96 100644 --- a/src/components/settings/HandyShortcut.tsx +++ b/src/components/settings/HandyShortcut.tsx @@ -1,7 +1,12 @@ import React, { useEffect, useState, useRef } from "react"; import { BindingResponseSchema, ShortcutBindingsMap } from "../../lib/types"; import { type } from "@tauri-apps/plugin-os"; -import { getKeyName } from "../../lib/utils/keyboard"; +import { + getKeyName, + formatKeyCombination, + normalizeKey, + type OSType, +} from "../../lib/utils/keyboard"; import ResetIcon from "../icons/ResetIcon"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; @@ -24,57 +29,42 @@ export const HandyShortcut: React.FC = ({ null, ); const [originalBinding, setOriginalBinding] = useState(""); - const [isMacOS, setIsMacOS] = useState(false); + const [osType, setOsType] = useState("unknown"); const shortcutRefs = useRef>(new Map()); const bindings = getSetting("bindings") || {}; - // Check if running on macOS + // Detect and store OS type useEffect(() => { - const checkOsType = async () => { + const detectOsType = async () => { try { - const osType = await type(); - setIsMacOS(osType === "macos"); + const detectedType = await type(); + let normalizedType: OSType; + + switch (detectedType) { + case "macos": + normalizedType = "macos"; + break; + case "windows": + normalizedType = "windows"; + break; + case "linux": + normalizedType = "linux"; + break; + default: + normalizedType = "unknown"; + } + + setOsType(normalizedType); } catch (error) { console.error("Error detecting OS type:", error); - setIsMacOS(false); + setOsType("unknown"); } }; - checkOsType(); + detectOsType(); }, []); - // Normalize modifier keys (unify left/right variants) - const normalizeKey = (key: string): string => { - // Handle left/right variants of modifier keys - if (key.startsWith("left ") || key.startsWith("right ")) { - const parts = key.split(" "); - if (parts.length === 2) { - // Return just the modifier name without left/right prefix - return parts[1]; - } - } - return key; - }; - - // Format keys for macOS display - const formatMacOSKeys = (key: string): string => { - if (!isMacOS) return key; // Only format for macOS - - const keyMap: Record = { - alt: "option", - }; - - return keyMap[key.toLowerCase()] || key; - }; - - // Format a key combination for display - const formatKeyCombination = (combination: string): string => { - if (!isMacOS) return combination; // Only format for macOS - - return combination.split("+").map(formatMacOSKeys).join(" + "); - }; - useEffect(() => { // Only add event listeners when we're in editing mode if (editingShortcutId === null) return; @@ -99,8 +89,8 @@ export const HandyShortcut: React.FC = ({ } e.preventDefault(); - // Get the key and normalize it (unify left/right modifiers) - const rawKey = getKeyName(e); + // Get the key with OS-specific naming and normalize it + const rawKey = getKeyName(e, osType); const key = normalizeKey(rawKey); console.log("You pressed", rawKey, "normalized to", key); @@ -117,8 +107,8 @@ export const HandyShortcut: React.FC = ({ const handleKeyUp = async (e: KeyboardEvent) => { e.preventDefault(); - // Get the key and normalize it - const rawKey = getKeyName(e); + // Get the key with OS-specific naming and normalize it + const rawKey = getKeyName(e, osType); const key = normalizeKey(rawKey); // Remove from currently pressed keys @@ -183,6 +173,7 @@ export const HandyShortcut: React.FC = ({ bindings, originalBinding, updateBinding, + osType, ]); // Start recording a new shortcut @@ -200,15 +191,11 @@ export const HandyShortcut: React.FC = ({ }; // Format the current shortcut keys being recorded - const formatCurrentKeys = () => { + const formatCurrentKeys = (): string => { if (recordedKeys.length === 0) return "Press keys..."; - if (!isMacOS) { - return recordedKeys.join("+"); - } - - // Map each key to its macOS-friendly name for display - return recordedKeys.map(formatMacOSKeys).join(" + "); + // Use the same formatting as the display to ensure consistency + return formatKeyCombination(recordedKeys.join("+"), osType); }; // Store references to shortcut elements @@ -275,7 +262,7 @@ export const HandyShortcut: React.FC = ({ className="px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 hover:bg-logo-primary/10 rounded cursor-pointer hover:border-logo-primary" onClick={() => startRecording(primaryId)} > - {formatKeyCombination(primaryBinding.current_binding)} + {formatKeyCombination(primaryBinding.current_binding, osType)}
)}