Add file based debug logging (#347)

* add basic logging to handy

* 100kb of logs

* add open button to the ui for logs

* use log everywhere

* some minor cleanup

* finish debug logging
This commit is contained in:
CJ Pais 2025-11-17 12:55:24 +07:00 committed by GitHub
parent 315c32eab4
commit 18536aa81e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 701 additions and 222 deletions

387
src-tauri/Cargo.lock generated
View file

@ -8,6 +8,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.12"
@ -72,6 +83,23 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "android_log-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
dependencies = [
"android_log-sys",
"env_filter",
"log",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -81,56 +109,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
]
[[package]]
name = "anyhow"
version = "1.0.100"
@ -492,6 +470,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block"
version = "0.1.6"
@ -538,6 +528,29 @@ dependencies = [
"piper",
]
[[package]]
name = "borsh"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
dependencies = [
"borsh-derive",
"cfg_aliases",
]
[[package]]
name = "borsh-derive"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3"
dependencies = [
"once_cell",
"proc-macro-crate 3.4.0",
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "brotli"
version = "8.0.2"
@ -565,6 +578,39 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byte-unit"
version = "5.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174"
dependencies = [
"rust_decimal",
"serde",
"utf8-width",
]
[[package]]
name = "bytecheck"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [
"bytecheck_derive",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.24.0"
@ -790,12 +836,6 @@ dependencies = [
"objc",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
@ -1506,19 +1546,6 @@ dependencies = [
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "epoll"
version = "4.4.0"
@ -1668,6 +1695,15 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fern"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"log",
]
[[package]]
name = "field-offset"
version = "0.3.6"
@ -1792,6 +1828,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futf"
version = "0.1.5"
@ -2292,7 +2334,7 @@ dependencies = [
"chrono",
"cpal",
"enigo",
"env_logger 0.11.8",
"env_filter",
"flate2",
"futures-util",
"hound",
@ -2315,6 +2357,7 @@ dependencies = [
"tauri-plugin-clipboard-manager",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-log",
"tauri-plugin-macos-permissions",
"tauri-plugin-opener",
"tauri-plugin-os",
@ -2334,6 +2377,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.8",
]
[[package]]
name = "hashbrown"
@ -2341,7 +2387,7 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"ahash 0.8.12",
]
[[package]]
@ -2856,12 +2902,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.12.1"
@ -2900,30 +2940,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "jni"
version = "0.21.1"
@ -3126,6 +3142,9 @@ name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
dependencies = [
"value-bag",
]
[[package]]
name = "lru-slab"
@ -3565,6 +3584,15 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "objc"
version = "0.2.7"
@ -3893,12 +3921,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "open"
version = "5.3.2"
@ -4486,6 +4508,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pxfm"
version = "0.1.25"
@ -4589,6 +4631,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
@ -4824,6 +4872,15 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.12.24"
@ -4909,6 +4966,35 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53"
[[package]]
name = "rkyv"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
dependencies = [
"bitvec",
"bytecheck",
"bytes",
"hashbrown 0.12.3",
"ptr_meta",
"rend",
"rkyv_derive",
"seahash",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rodio"
version = "0.20.1"
@ -4976,6 +5062,22 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "rust_decimal"
version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282"
dependencies = [
"arrayvec",
"borsh",
"bytes",
"num-traits",
"rand 0.8.5",
"rkyv",
"serde",
"serde_json",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@ -5180,6 +5282,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "secrecy"
version = "0.10.3"
@ -5477,6 +5585,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@ -6169,6 +6283,12 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.44"
@ -6385,6 +6505,28 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "tauri-plugin-log"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6"
dependencies = [
"android_logger",
"byte-unit",
"fern",
"log",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"serde",
"serde_json",
"serde_repr",
"swift-rs",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
"time",
]
[[package]]
name = "tauri-plugin-macos-permissions"
version = "2.3.0"
@ -6727,7 +6869,9 @@ checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
@ -7027,7 +7171,7 @@ dependencies = [
"async-openai 0.29.6",
"async-trait",
"derive_builder",
"env_logger 0.10.2",
"env_logger",
"hound",
"log",
"ndarray",
@ -7259,18 +7403,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
@ -7294,6 +7438,12 @@ dependencies = [
"ringbuffer",
]
[[package]]
name = "value-bag"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -8359,6 +8509,15 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x11"
version = "2.21.0"

View file

@ -34,6 +34,7 @@ tauri = { version = "2.9.1", features = [
"tray-icon",
'image-png',
] }
tauri-plugin-log = "2.7.1"
tauri-plugin-opener = "2.5.2"
tauri-plugin-store = "2.4.1"
tauri-plugin-os = "2.3.2"
@ -49,8 +50,8 @@ cpal = "0.16.0"
anyhow = "1.0.95"
rubato = "0.16.2"
hound = "3.5.1"
env_logger = "0.11.6"
log = "0.4.25"
env_filter = "0.1.0"
tokio = "1.43.0"
vad-rs = { git = "https://github.com/cjpais/vad-rs", default-features = false }
enigo = "0.6.1"

View file

@ -318,14 +318,14 @@ impl ShortcutAction for TranscribeAction {
"Text pasted successfully in {:?}",
paste_time.elapsed()
),
Err(e) => eprintln!("Failed to paste transcription: {}", e),
Err(e) => error!("Failed to paste transcription: {}", e),
}
// Hide the overlay after transcription is complete
utils::hide_recording_overlay(&ah_clone);
change_tray_icon(&ah_clone, TrayIconState::Idle);
})
.unwrap_or_else(|e| {
eprintln!("Failed to run paste on main thread: {:?}", e);
error!("Failed to run paste on main thread: {:?}", e);
utils::hide_recording_overlay(&ah);
change_tray_icon(&ah, TrayIconState::Idle);
});
@ -359,7 +359,7 @@ struct TestAction;
impl ShortcutAction for TestAction {
fn start(&self, app: &AppHandle, binding_id: &str, shortcut_str: &str) {
println!(
log::info!(
"Shortcut ID '{}': Started - {} (App: {})", // Changed "Pressed" to "Started" for consistency
binding_id,
shortcut_str,
@ -368,7 +368,7 @@ impl ShortcutAction for TestAction {
}
fn stop(&self, app: &AppHandle, binding_id: &str, shortcut_str: &str) {
println!(
log::info!(
"Shortcut ID '{}': Stopped - {} (App: {})", // Changed "Released" to "Stopped" for consistency
binding_id,
shortcut_str,

View file

@ -1,5 +1,6 @@
use crate::settings;
use cpal::traits::{DeviceTrait, HostTrait};
use log::{debug, error, warn};
use rodio::OutputStreamBuilder;
use std::fs::File;
use std::io::BufReader;
@ -21,7 +22,7 @@ fn play_sound(app: &AppHandle, resource_path: &str, base_dir: tauri::path::BaseD
let audio_path = match app_handle.path().resolve(&resource_path, base_dir) {
Ok(path) => path.to_path_buf(),
Err(e) => {
eprintln!(
error!(
"Failed to resolve audio file path '{}': {}",
resource_path, e
);
@ -33,7 +34,7 @@ fn play_sound(app: &AppHandle, resource_path: &str, base_dir: tauri::path::BaseD
let selected_device = settings.selected_output_device.clone();
if let Err(e) = play_audio_file(&audio_path, selected_device, volume) {
eprintln!("Failed to play sound '{}': {}", resource_path, e);
error!("Failed to play sound '{}': {}", resource_path, e);
}
});
}
@ -87,7 +88,7 @@ fn play_audio_file(
) -> Result<(), Box<dyn std::error::Error>> {
let stream_builder = if let Some(device_name) = selected_device {
if device_name == "Default" {
println!("Using default device");
debug!("Using default device");
OutputStreamBuilder::from_default_device()?
} else {
let host = crate::audio_toolkit::get_cpal_host();
@ -104,13 +105,13 @@ fn play_audio_file(
match found_device {
Some(device) => OutputStreamBuilder::from_device(device)?,
None => {
eprintln!("Device '{}' not found, using default device", device_name);
warn!("Device '{}' not found, using default device", device_name);
OutputStreamBuilder::from_default_device()?
}
}
}
} else {
println!("Using default device");
debug!("Using default device");
OutputStreamBuilder::from_default_device()?
};

View file

@ -82,7 +82,7 @@ impl AudioRecorder {
let sample_rate = config.sample_rate().0;
let channels = config.channels() as usize;
println!(
log::info!(
"Using device: {:?}\nSample rate: {}\nChannels: {}\nFormat: {:?}",
thread_device.name(),
sample_rate,
@ -188,14 +188,14 @@ impl AudioRecorder {
}
if sample_tx.send(output_buffer.clone()).is_err() {
eprintln!("Failed to send samples");
log::error!("Failed to send samples");
}
};
device.build_input_stream(
&config.clone().into(),
stream_cb,
|err| eprintln!("Stream error: {}", err),
|err| log::error!("Stream error: {}", err),
None,
)
}

View file

@ -55,7 +55,7 @@ impl FrameResampler {
.process(&[&self.in_buf[..]], None)
{
// let duration = start.elapsed();
// println!("Resampler took: {:?}", duration);
// log::debug!("Resampler took: {:?}", duration);
self.emit_frames(&out[0], &mut emit);
}
self.in_buf.clear();

View file

@ -43,8 +43,6 @@ impl VoiceActivityDetector for SileroVad {
.compute(frame)
.map_err(|e| anyhow::anyhow!("Silero VAD error: {e}"))?;
// println!("Silero VAD result: prob = {}", result.prob);
if result.prob > self.threshold {
Ok(VadFrame::Speech(frame))
} else {

View file

@ -47,7 +47,6 @@ impl VoiceActivityDetector for SmoothedVad {
// 2. Delegate to the wrapped boolean VAD
let is_voice = self.inner_vad.is_voice(frame)?;
// println!("Is Voice: {}", is_voice);
match (self.in_speech, is_voice) {
// Potential start of speech - need to accumulate onset frames

View file

@ -3,6 +3,7 @@ use enigo::Enigo;
use enigo::Key;
use enigo::Keyboard;
use enigo::Settings;
use log::info;
use tauri::AppHandle;
use tauri_plugin_clipboard_manager::ClipboardExt;
@ -138,7 +139,7 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
let settings = get_settings(&app_handle);
let paste_method = settings.paste_method;
println!("Using paste method: {:?}", paste_method);
info!("Using paste method: {:?}", paste_method);
// Perform the paste operation
match paste_method {

View file

@ -2,6 +2,7 @@ use crate::audio_feedback;
use crate::audio_toolkit::audio::{list_input_devices, list_output_devices};
use crate::managers::audio::{AudioRecordingManager, MicrophoneMode};
use crate::settings::{get_settings, write_settings};
use log::warn;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::{AppHandle, Manager};
@ -153,7 +154,7 @@ pub fn play_test_sound(app: AppHandle, sound_type: String) {
"start" => audio_feedback::SoundType::Start,
"stop" => audio_feedback::SoundType::Stop,
_ => {
eprintln!("Unknown sound type: {}", sound_type);
warn!("Unknown sound type: {}", sound_type);
return;
}
};

View file

@ -3,8 +3,9 @@ pub mod history;
pub mod models;
pub mod transcription;
use crate::utils::cancel_current_operation;
use crate::{settings, utils::cancel_current_operation};
use tauri::{AppHandle, Manager};
use tauri_plugin_log::LogLevel;
use tauri_plugin_opener::OpenerExt;
#[tauri::command]
@ -22,6 +23,32 @@ pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
Ok(app_data_dir.to_string_lossy().to_string())
}
#[tauri::command]
pub fn get_log_dir_path(app: AppHandle) -> Result<String, String> {
let log_dir = app
.path()
.app_log_dir()
.map_err(|e| format!("Failed to get log directory: {}", e))?;
Ok(log_dir.to_string_lossy().to_string())
}
#[tauri::command]
pub fn set_log_level(app: AppHandle, level: LogLevel) -> Result<(), String> {
let log_level: log::Level = level.clone().into();
// Update the file log level atomic so the filter picks up the new level
crate::FILE_LOG_LEVEL.store(
log_level.to_level_filter() as u8,
std::sync::atomic::Ordering::Relaxed,
);
let mut settings = settings::get_settings(&app);
settings.log_level = level;
settings::write_settings(&app, settings);
Ok(())
}
#[tauri::command]
pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> {
let app_data_dir = app
@ -38,3 +65,33 @@ pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> {
Ok(())
}
#[tauri::command]
pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
let log_dir = app
.path()
.app_log_dir()
.map_err(|e| format!("Failed to get log directory: {}", e))?;
let path = log_dir.to_string_lossy().as_ref().to_string();
app.opener()
.open_path(path, None::<String>)
.map_err(|e| format!("Failed to open log directory: {}", e))?;
Ok(())
}
#[tauri::command]
pub fn open_app_data_dir(app: AppHandle) -> Result<(), String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data directory: {}", e))?;
let path = app_data_dir.to_string_lossy().as_ref().to_string();
app.opener()
.open_path(path, None::<String>)
.map_err(|e| format!("Failed to open app data directory: {}", e))?;
Ok(())
}

View file

@ -79,7 +79,7 @@ mod tests {
// This will run on macOS and should not panic
let result = is_clamshell();
assert!(result.is_ok());
println!("Clamshell state: {:?}", result.unwrap());
let _ = result.unwrap();
}
#[test]
@ -87,8 +87,6 @@ mod tests {
fn test_has_builtin_display() {
let result = has_builtin_display();
assert!(result.is_ok());
if let Ok(has_builtin) = result {
println!("Has built-in display (is laptop): {}", has_builtin);
}
let _ = result.unwrap();
}
}

View file

@ -12,11 +12,13 @@ mod shortcut;
mod tray;
mod utils;
use env_filter::Builder as EnvFilterBuilder;
use managers::audio::AudioRecordingManager;
use managers::history::HistoryManager;
use managers::model::ModelManager;
use managers::transcription::TranscriptionManager;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex};
use tauri::image::Image;
@ -24,6 +26,45 @@ use tauri::tray::TrayIconBuilder;
use tauri::Emitter;
use tauri::{AppHandle, Manager};
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind};
// Global atomic to store the file log level filter
// We use u8 to store the log::LevelFilter as a number
pub static FILE_LOG_LEVEL: AtomicU8 = AtomicU8::new(log::LevelFilter::Debug as u8);
fn level_filter_from_u8(value: u8) -> log::LevelFilter {
match value {
0 => log::LevelFilter::Off,
1 => log::LevelFilter::Error,
2 => log::LevelFilter::Warn,
3 => log::LevelFilter::Info,
4 => log::LevelFilter::Debug,
5 => log::LevelFilter::Trace,
_ => log::LevelFilter::Trace,
}
}
fn build_console_filter() -> env_filter::Filter {
let mut builder = EnvFilterBuilder::new();
match std::env::var("RUST_LOG") {
Ok(spec) if !spec.trim().is_empty() => {
if let Err(err) = builder.try_parse(&spec) {
log::warn!(
"Ignoring invalid RUST_LOG value '{}': {}. Falling back to info-level console logging",
spec,
err
);
builder.filter_level(log::LevelFilter::Info);
}
}
_ => {
builder.filter_level(log::LevelFilter::Info);
}
}
builder.build()
}
#[derive(Default)]
struct ShortcutToggleStates {
@ -37,21 +78,21 @@ fn show_main_window(app: &AppHandle) {
if let Some(main_window) = app.get_webview_window("main") {
// First, ensure the window is visible
if let Err(e) = main_window.show() {
eprintln!("Failed to show window: {}", e);
log::error!("Failed to show window: {}", e);
}
// Then, bring it to the front and give it focus
if let Err(e) = main_window.set_focus() {
eprintln!("Failed to focus window: {}", e);
log::error!("Failed to focus window: {}", e);
}
// Optional: On macOS, ensure the app becomes active if it was an accessory
#[cfg(target_os = "macos")]
{
if let Err(e) = app.set_activation_policy(tauri::ActivationPolicy::Regular) {
eprintln!("Failed to set activation policy to Regular: {}", e);
log::error!("Failed to set activation policy to Regular: {}", e);
}
}
} else {
eprintln!("Main window not found.");
log::error!("Main window not found.");
}
}
@ -155,9 +196,34 @@ fn trigger_update_check(app: AppHandle) -> Result<(), String> {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
env_logger::init();
// Parse console logging directives from RUST_LOG, falling back to info-level logging
// when the variable is unset
let console_filter = build_console_filter();
tauri::Builder::default()
.plugin(
LogBuilder::new()
.level(log::LevelFilter::Trace) // Set to most verbose level globally
.max_file_size(500_000)
.rotation_strategy(RotationStrategy::KeepOne)
.clear_targets()
.targets([
// Console output respects RUST_LOG environment variable
Target::new(TargetKind::Stdout).filter({
let console_filter = console_filter.clone();
move |metadata| console_filter.enabled(metadata)
}),
// File logs respect the user's settings (stored in FILE_LOG_LEVEL atomic)
Target::new(TargetKind::LogDir {
file_name: Some("handy".into()),
})
.filter(|metadata| {
let file_level = FILE_LOG_LEVEL.load(Ordering::Relaxed);
metadata.level() <= level_filter_from_u8(file_level)
}),
])
.build(),
)
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
show_main_window(app);
}))
@ -185,6 +251,9 @@ pub fn run() {
.manage(Mutex::new(ShortcutToggleStates::default()))
.setup(move |app| {
let settings = settings::get_settings(&app.handle());
let file_log_level: log::Level = settings.log_level.clone().into();
// Store the file log level in the atomic for the filter to use
FILE_LOG_LEVEL.store(file_log_level.to_level_filter() as u8, Ordering::Relaxed);
let app_handle = app.handle().clone();
initialize_core_logic(&app_handle);
@ -209,12 +278,12 @@ pub fn run() {
.app_handle()
.set_activation_policy(tauri::ActivationPolicy::Accessory);
if let Err(e) = res {
println!("Failed to set activation policy: {}", e);
log::error!("Failed to set activation policy: {}", e);
}
}
}
tauri::WindowEvent::ThemeChanged(theme) => {
println!("Theme changed to: {:?}", theme);
log::info!("Theme changed to: {:?}", theme);
// Update tray icon to match new theme, maintaining idle state
utils::change_tray_icon(&window.app_handle(), utils::TrayIconState::Idle);
}
@ -253,7 +322,11 @@ pub fn run() {
trigger_update_check,
commands::cancel_operation,
commands::get_app_dir_path,
commands::get_log_dir_path,
commands::set_log_level,
commands::open_recordings_folder,
commands::open_log_dir,
commands::open_app_data_dir,
commands::models::get_available_models,
commands::models::get_model_info,
commands::models::download_model,

View file

@ -2,7 +2,7 @@ use crate::audio_toolkit::{list_input_devices, vad::SmoothedVad, AudioRecorder,
use crate::helpers::clamshell;
use crate::settings::{get_settings, AppSettings};
use crate::utils;
use log::{debug, info};
use log::{debug, error, info};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tauri::Manager;
@ -339,7 +339,7 @@ impl AudioRecordingManager {
// Ensure microphone is open in on-demand mode
if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) {
if let Err(e) = self.start_microphone_stream() {
eprintln!("Failed to open microphone stream: {e}");
error!("Failed to open microphone stream: {e}");
return false;
}
}
@ -354,7 +354,7 @@ impl AudioRecordingManager {
return true;
}
}
eprintln!("Recorder not available");
error!("Recorder not available");
false
} else {
false
@ -384,12 +384,12 @@ impl AudioRecordingManager {
match rec.stop() {
Ok(buf) => buf,
Err(e) => {
eprintln!("stop() failed: {e}");
error!("stop() failed: {e}");
Vec::new()
}
}
} else {
eprintln!("Recorder not available");
error!("Recorder not available");
Vec::new()
};
@ -402,7 +402,7 @@ impl AudioRecordingManager {
// Pad if very short
let s_len = samples.len();
// println!("Got {} samples", { s_len });
// debug!("Got {} samples", s_len);
if s_len < WHISPER_SAMPLE_RATE && s_len > 0 {
let mut padded = samples;
padded.resize(WHISPER_SAMPLE_RATE * 5 / 4, 0.0);

View file

@ -2,6 +2,7 @@ use crate::settings::{get_settings, write_settings};
use anyhow::Result;
use flate2::read::GzDecoder;
use futures_util::StreamExt;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
@ -225,9 +226,9 @@ impl ModelManager {
// Only copy if user doesn't already have the model
if !user_path.exists() {
println!("Migrating bundled model {} to user directory", filename);
info!("Migrating bundled model {} to user directory", filename);
fs::copy(&bundled_path, &user_path)?;
println!("Successfully migrated {}", filename);
info!("Successfully migrated {}", filename);
}
}
}
@ -250,7 +251,7 @@ impl ModelManager {
// Clean up any leftover .extracting directories from interrupted extractions
if extracting_path.exists() {
println!("Cleaning up interrupted extraction for model: {}", model.id);
warn!("Cleaning up interrupted extraction for model: {}", model.id);
let _ = fs::remove_dir_all(&extracting_path);
}
@ -292,7 +293,7 @@ impl ModelManager {
// Find the first available (downloaded) model
let models = self.available_models.lock().unwrap();
if let Some(available_model) = models.values().find(|model| model.is_downloaded) {
println!(
info!(
"Auto-selecting model: {} ({})",
available_model.id, available_model.name
);
@ -302,7 +303,7 @@ impl ModelManager {
updated_settings.selected_model = available_model.id.clone();
write_settings(&self.app_handle, updated_settings);
println!("Successfully auto-selected model: {}", available_model.id);
info!("Successfully auto-selected model: {}", available_model.id);
}
}
@ -339,10 +340,10 @@ impl ModelManager {
// 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);
info!("Resuming download of model {} from byte {}", model_id, size);
size
} else {
println!("Starting fresh download of model {} from {}", model_id, url);
info!("Starting fresh download of model {} from {}", model_id, url);
0
};
@ -456,7 +457,7 @@ impl ModelManager {
if model_info.is_directory {
// Emit extraction started event
let _ = self.app_handle.emit("model-extraction-started", model_id);
println!("Extracting archive for directory-based model: {}", model_id);
info!("Extracting archive for directory-based model: {}", model_id);
// Use a temporary extraction directory to ensure atomic operations
let temp_extract_dir = self
@ -515,7 +516,7 @@ impl ModelManager {
fs::rename(&temp_extract_dir, &final_model_dir)?;
}
println!("Successfully extracted archive for model: {}", model_id);
info!("Successfully extracted archive for model: {}", model_id);
// Emit extraction completed event
let _ = self.app_handle.emit("model-extraction-completed", model_id);
@ -539,7 +540,7 @@ impl ModelManager {
// Emit completion event
let _ = self.app_handle.emit("model-download-complete", model_id);
println!(
info!(
"Successfully downloaded model {} to {:?}",
model_id, model_path
);
@ -548,7 +549,7 @@ impl ModelManager {
}
pub fn delete_model(&self, model_id: &str) -> Result<()> {
println!("ModelManager: delete_model called for: {}", model_id);
debug!("ModelManager: delete_model called for: {}", model_id);
let model_info = {
let models = self.available_models.lock().unwrap();
@ -558,43 +559,40 @@ impl ModelManager {
let model_info =
model_info.ok_or_else(|| anyhow::anyhow!("Model not found: {}", model_id))?;
println!("ModelManager: Found model info: {:?}", model_info);
debug!("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);
debug!("ModelManager: Model path: {:?}", model_path);
debug!("ModelManager: Partial path: {:?}", partial_path);
let mut deleted_something = false;
if model_info.is_directory {
// Delete complete model directory if it exists
if model_path.exists() && model_path.is_dir() {
println!(
"ModelManager: Deleting model directory at: {:?}",
model_path
);
info!("Deleting model directory at: {:?}", model_path);
fs::remove_dir_all(&model_path)?;
println!("ModelManager: Model directory deleted successfully");
info!("Model directory deleted successfully");
deleted_something = true;
}
} else {
// Delete complete model file if it exists
if model_path.exists() {
println!("ModelManager: Deleting model file at: {:?}", model_path);
info!("Deleting model file at: {:?}", model_path);
fs::remove_file(&model_path)?;
println!("ModelManager: Model file deleted successfully");
info!("Model file deleted successfully");
deleted_something = true;
}
}
// Delete partial file if it exists (same for both types)
if partial_path.exists() {
println!("ModelManager: Deleting partial file at: {:?}", partial_path);
info!("Deleting partial file at: {:?}", partial_path);
fs::remove_file(&partial_path)?;
println!("ModelManager: Partial file deleted successfully");
info!("Partial file deleted successfully");
deleted_something = true;
}
@ -604,7 +602,7 @@ impl ModelManager {
// Update download status
self.update_download_status()?;
println!("ModelManager: Download status updated");
debug!("ModelManager: download status updated");
Ok(())
}
@ -655,7 +653,7 @@ impl ModelManager {
}
pub fn cancel_download(&self, model_id: &str) -> Result<()> {
println!("ModelManager: cancel_download called for: {}", model_id);
debug!("ModelManager: cancel_download called for: {}", model_id);
let _model_info = {
let models = self.available_models.lock().unwrap();
@ -680,7 +678,7 @@ impl ModelManager {
// Update download status to reflect current state
self.update_download_status()?;
println!("ModelManager: Download cancelled for: {}", model_id);
info!("Download cancelled for: {}", model_id);
Ok(())
}
}

View file

@ -2,7 +2,7 @@ use crate::audio_toolkit::apply_custom_words;
use crate::managers::model::{EngineType, ModelManager};
use crate::settings::{get_settings, ModelUnloadTimeout};
use anyhow::Result;
use log::debug;
use log::{debug, error, info, warn};
use serde::Serialize;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
@ -289,7 +289,7 @@ impl TranscriptionManager {
thread::spawn(move || {
let settings = get_settings(&self_clone.app_handle);
if let Err(e) = self_clone.load_model(&settings.selected_model) {
eprintln!("Failed to load model: {}", e);
error!("Failed to load model: {}", e);
}
let mut is_loading = self_clone.is_loading.lock().unwrap();
*is_loading = false;
@ -314,10 +314,10 @@ impl TranscriptionManager {
let st = std::time::Instant::now();
println!("Audio vector length: {}", audio.len());
debug!("Audio vector length: {}", audio.len());
if audio.len() == 0 {
println!("Empty audio vector");
debug!("Empty audio vector");
return Ok(String::new());
}
@ -393,17 +393,29 @@ impl TranscriptionManager {
} else {
""
};
println!("\ntook {}ms{}", (et - st).as_millis(), translation_note);
info!(
"Transcription completed in {}ms{}",
(et - st).as_millis(),
translation_note
);
let final_result = corrected_result.trim().to_string();
if final_result.is_empty() {
info!("Transcription result is empty");
} else {
info!("Transcription result: {}", final_result);
}
// Check if we should immediately unload the model after transcription
if settings.model_unload_timeout == ModelUnloadTimeout::Immediately {
println!("⚡ Immediately unloading model after transcription");
info!("Immediately unloading model after transcription");
if let Err(e) = self.unload_model() {
eprintln!("Failed to immediately unload model: {}", e);
error!("Failed to immediately unload model: {}", e);
}
}
Ok(corrected_result.trim().to_string())
Ok(final_result)
}
}
@ -417,7 +429,7 @@ impl Drop for TranscriptionManager {
// Wait for the thread to finish gracefully
if let Some(handle) = self.watcher_handle.lock().unwrap().take() {
if let Err(e) = handle.join() {
eprintln!("Failed to join idle watcher thread: {:?}", e);
warn!("Failed to join idle watcher thread: {:?}", e);
} else {
debug!("Idle watcher thread joined successfully");
}

View file

@ -1,6 +1,8 @@
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tauri::AppHandle;
use tauri_plugin_log::LogLevel;
use tauri_plugin_store::StoreExt;
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -181,6 +183,8 @@ pub struct AppSettings {
pub overlay_position: OverlayPosition,
#[serde(default = "default_debug_mode")]
pub debug_mode: bool,
#[serde(default = "default_log_level")]
pub log_level: LogLevel,
#[serde(default)]
pub custom_words: Vec<String>,
#[serde(default)]
@ -248,6 +252,10 @@ fn default_debug_mode() -> bool {
false
}
fn default_log_level() -> LogLevel {
LogLevel::Debug
}
fn default_word_correction_threshold() -> f64 {
0.18
}
@ -374,6 +382,7 @@ pub fn get_default_settings() -> AppSettings {
selected_language: "auto".to_string(),
overlay_position: OverlayPosition::Bottom,
debug_mode: false,
log_level: default_log_level(),
custom_words: Vec::new(),
model_unload_timeout: ModelUnloadTimeout::Never,
word_correction_threshold: default_word_correction_threshold(),
@ -425,11 +434,11 @@ pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings {
// Parse the entire settings object
match serde_json::from_value::<AppSettings>(settings_value) {
Ok(settings) => {
println!("Found existing settings: {:?}", settings);
debug!("Found existing settings: {:?}", settings);
settings
}
Err(e) => {
println!("Failed to parse settings: {}", e);
warn!("Failed to parse settings: {}", e);
// Fall back to default settings if parsing fails
let default_settings = get_default_settings();
store.set("settings", serde_json::to_value(&default_settings).unwrap());

View file

@ -1,3 +1,4 @@
use log::{error, warn};
use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_autostart::ManagerExt;
@ -16,7 +17,7 @@ pub fn init_shortcuts(app: &AppHandle) {
// Register shortcuts with the bindings from settings
for (_id, binding) in settings.bindings {
if let Err(e) = _register_shortcut(app, binding) {
eprintln!("Failed to register shortcut {} during init: {}", _id, e);
error!("Failed to register shortcut {} during init: {}", _id, e);
}
}
}
@ -41,7 +42,7 @@ pub fn change_binding(
Some(binding) => binding.clone(),
None => {
let error_msg = format!("Binding with id '{}' not found", id);
eprintln!("change_binding error: {}", error_msg);
warn!("change_binding error: {}", error_msg);
return Ok(BindingResponse {
success: false,
binding: None,
@ -53,12 +54,12 @@ pub fn change_binding(
// Unregister the existing binding
if let Err(e) = _unregister_shortcut(&app, binding_to_modify.clone()) {
let error_msg = format!("Failed to unregister shortcut: {}", e);
eprintln!("change_binding error: {}", error_msg);
error!("change_binding error: {}", error_msg);
}
// Validate the new shortcut before we touch the current registration
if let Err(e) = validate_shortcut_string(&binding) {
eprintln!("change_binding validation error: {}", e);
warn!("change_binding validation error: {}", e);
return Err(e);
}
@ -69,7 +70,7 @@ pub fn change_binding(
// Register the new binding
if let Err(e) = _register_shortcut(&app, updated_binding.clone()) {
let error_msg = format!("Failed to register shortcut: {}", e);
eprintln!("change_binding error: {}", error_msg);
error!("change_binding error: {}", error_msg);
return Ok(BindingResponse {
success: false,
binding: None,
@ -135,7 +136,7 @@ pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), S
"pop" => SoundTheme::Pop,
"custom" => SoundTheme::Custom,
other => {
eprintln!("Invalid sound theme '{}', defaulting to marimba", other);
warn!("Invalid sound theme '{}', defaulting to marimba", other);
SoundTheme::Marimba
}
};
@ -168,7 +169,7 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu
"top" => OverlayPosition::Top,
"bottom" => OverlayPosition::Bottom,
other => {
eprintln!("Invalid overlay position '{}', defaulting to bottom", other);
warn!("Invalid overlay position '{}', defaulting to bottom", other);
OverlayPosition::Bottom
}
};
@ -271,7 +272,7 @@ pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(),
#[cfg(not(target_os = "macos"))]
"shift_insert" => PasteMethod::ShiftInsert,
other => {
eprintln!("Invalid paste method '{}', defaulting to ctrl_v", other);
warn!("Invalid paste method '{}', defaulting to ctrl_v", other);
PasteMethod::CtrlV
}
};
@ -287,7 +288,7 @@ pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Re
"dont_modify" => ClipboardHandling::DontModify,
"copy_to_clipboard" => ClipboardHandling::CopyToClipboard,
other => {
eprintln!(
warn!(
"Invalid clipboard handling '{}', defaulting to dont_modify",
other
);
@ -644,7 +645,7 @@ fn validate_shortcut_string(raw: &str) -> Result<(), String> {
pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
if let Err(e) = _unregister_shortcut(&app, b) {
eprintln!("suspend_binding error for id '{}': {}", id, e);
error!("suspend_binding error for id '{}': {}", id, e);
return Err(e);
}
}
@ -656,7 +657,7 @@ pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
if let Err(e) = _register_shortcut(&app, b) {
eprintln!("resume_binding error for id '{}': {}", id, e);
error!("resume_binding error for id '{}': {}", id, e);
return Err(e);
}
}
@ -666,7 +667,7 @@ pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
// Validate human-level rules first
if let Err(e) = validate_shortcut_string(&binding.current_binding) {
eprintln!(
warn!(
"_register_shortcut validation error for binding '{}': {}",
binding.current_binding, e
);
@ -681,7 +682,7 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
"Failed to parse shortcut '{}': {}",
binding.current_binding, e
);
eprintln!("_register_shortcut parse error: {}", error_msg);
error!("_register_shortcut parse error: {}", error_msg);
return Err(error_msg);
}
};
@ -689,7 +690,7 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
// Prevent duplicate registrations that would silently shadow one another
if app.global_shortcut().is_registered(shortcut) {
let error_msg = format!("Shortcut '{}' is already in use", binding.current_binding);
eprintln!("_register_shortcut duplicate error: {}", error_msg);
warn!("_register_shortcut duplicate error: {}", error_msg);
return Err(error_msg);
}
@ -733,8 +734,8 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
}
}
} else {
println!(
"Warning: No action defined in ACTION_MAP for shortcut ID '{}'. Shortcut: '{}', State: {:?}",
warn!(
"No action defined in ACTION_MAP for shortcut ID '{}'. Shortcut: '{}', State: {:?}",
binding_id_for_closure, shortcut_string, event.state
);
}
@ -742,7 +743,7 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S
})
.map_err(|e| {
let error_msg = format!("Couldn't register shortcut '{}': {}", binding.current_binding, e);
eprintln!("_register_shortcut registration error: {}", error_msg);
error!("_register_shortcut registration error: {}", error_msg);
error_msg
})?;
@ -757,7 +758,7 @@ fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(),
"Failed to parse shortcut '{}' for unregistration: {}",
binding.current_binding, e
);
eprintln!("_unregister_shortcut parse error: {}", error_msg);
error!("_unregister_shortcut parse error: {}", error_msg);
return Err(error_msg);
}
};
@ -767,7 +768,7 @@ fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(),
"Failed to unregister shortcut '{}': {}",
binding.current_binding, e
);
eprintln!("_unregister_shortcut error: {}", error_msg);
error!("_unregister_shortcut error: {}", error_msg);
error_msg
})?;

View file

@ -1,6 +1,7 @@
use crate::actions::ACTION_MAP;
use crate::managers::audio::AudioRecordingManager;
use crate::ManagedToggleState;
use log::{info, warn};
use std::sync::Arc;
use tauri::{AppHandle, Manager};
@ -13,7 +14,7 @@ pub use crate::tray::*;
/// Centralized cancellation function that can be called from anywhere in the app.
/// Handles cancelling both recording and transcription operations and updates UI state.
pub fn cancel_current_operation(app: &AppHandle) {
println!("Initiating operation cancellation...");
info!("Initiating operation cancellation...");
// First, reset all shortcut toggle states and call stop actions
// This is critical for non-push-to-talk mode where shortcuts toggle on/off
@ -28,7 +29,7 @@ pub fn cancel_current_operation(app: &AppHandle) {
.collect();
for binding_id in active_bindings {
println!("Stopping active action for binding: {}", binding_id);
info!("Stopping active action for binding: {}", binding_id);
// Call the action's stop method to ensure proper cleanup
if let Some(action) = ACTION_MAP.get(&binding_id) {
@ -41,7 +42,7 @@ pub fn cancel_current_operation(app: &AppHandle) {
}
}
} else {
eprintln!("Warning: Failed to lock toggle state manager during cancellation");
warn!("Failed to lock toggle state manager during cancellation");
}
// Cancel any ongoing recording
@ -51,5 +52,5 @@ pub fn cancel_current_operation(app: &AppHandle) {
// Update tray icon and menu to idle state
change_tray_icon(app, crate::tray::TrayIconState::Idle);
println!("Operation cancellation completed - returned to idle state");
info!("Operation cancellation completed - returned to idle state");
}

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { TextDisplay } from "../ui";
import { SettingContainer } from "../ui/SettingContainer";
import { Button } from "../ui/Button";
interface AppDataDirectoryProps {
descriptionMode?: "tooltip" | "inline";
@ -32,9 +33,13 @@ export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
loadAppDirectory();
}, []);
const handleCopy = (value: string) => {
// Could add a toast notification here if desired
console.log("Copied to clipboard:", value);
const handleOpen = async () => {
if (!appDirPath) return;
try {
await invoke("open_app_data_dir");
} catch (openError) {
console.error("Failed to open app data directory:", openError);
}
};
if (loading) {
@ -57,15 +62,27 @@ export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
}
return (
<TextDisplay
label="App Data Directory"
<SettingContainer
title="App Data Directory"
description="Main directory where application data, settings, and models are stored"
value={appDirPath}
descriptionMode={descriptionMode}
grouped={grouped}
copyable={true}
monospace={true}
onCopy={handleCopy}
/>
layout="stacked"
>
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 px-2 py-2 bg-mid-gray/10 border border-mid-gray/80 rounded text-xs font-mono break-all">
{appDirPath}
</div>
<Button
onClick={handleOpen}
variant="secondary"
size="sm"
disabled={!appDirPath}
className="px-3 py-2"
>
Open
</Button>
</div>
</SettingContainer>
);
};

View file

@ -1,5 +1,7 @@
import React from "react";
import { WordCorrectionThreshold } from "./WordCorrectionThreshold";
import { LogDirectory } from "./LogDirectory";
import { LogLevelSelector } from "./LogLevelSelector";
import { SettingsGroup } from "../../ui/SettingsGroup";
import { HistoryLimit } from "../HistoryLimit";
import { AlwaysOnMicrophone } from "../AlwaysOnMicrophone";
@ -13,6 +15,8 @@ export const DebugSettings: React.FC = () => {
return (
<div className="max-w-3xl w-full mx-auto space-y-6">
<SettingsGroup title="Debug">
<LogDirectory grouped={true} />
<LogLevelSelector grouped={true} />
<SoundPicker
label="Sound Theme"
description="Choose a sound theme for recording start and stop feedback"

View file

@ -0,0 +1,81 @@
import React, { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { SettingContainer } from "../../ui/SettingContainer";
import { Button } from "../../ui/Button";
interface LogDirectoryProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const LogDirectory: React.FC<LogDirectoryProps> = ({
descriptionMode = "tooltip",
grouped = false,
}) => {
const [logDir, setLogDir] = useState<string>("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadLogDirectory = async () => {
try {
const result = await invoke<string>("get_log_dir_path");
setLogDir(result);
} catch (err) {
const errorMessage =
err && typeof err === "object" && "message" in err
? String(err.message)
: "Failed to load log directory";
setError(errorMessage);
} finally {
setLoading(false);
}
};
loadLogDirectory();
}, []);
const handleOpen = async () => {
if (!logDir) return;
try {
await invoke("open_log_dir");
} catch (openError) {
console.error("Failed to open log directory:", openError);
}
};
return (
<SettingContainer
title="Log Directory"
description="Location on disk where Handy writes rotated log files"
descriptionMode={descriptionMode}
grouped={grouped}
layout="stacked"
>
{loading ? (
<div className="animate-pulse">
<div className="h-8 bg-gray-100 rounded" />
</div>
) : error ? (
<div className="p-3 bg-red-50 border border-red-200 rounded text-xs text-red-600">
Error loading log directory: {error}
</div>
) : (
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 px-2 py-2 bg-mid-gray/10 border border-mid-gray/80 rounded text-xs font-mono break-all">
{logDir}
</div>
<Button
onClick={handleOpen}
variant="secondary"
size="sm"
disabled={!logDir}
className="px-3 py-2"
>
Open
</Button>
</div>
)}
</SettingContainer>
);
};

View file

@ -0,0 +1,60 @@
import React from "react";
import { SettingContainer } from "../../ui/SettingContainer";
import { Dropdown, type DropdownOption } from "../../ui/Dropdown";
import { useSettings } from "../../../hooks/useSettings";
const LOG_LEVEL_OPTIONS: DropdownOption[] = [
{ value: "5", label: "Error" },
{ value: "4", label: "Warn" },
{ value: "3", label: "Info" },
{ value: "2", label: "Debug" },
{ value: "1", label: "Trace" },
];
interface LogLevelSelectorProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
descriptionMode = "tooltip",
grouped = false,
}) => {
const { settings, updateSetting, isUpdating } = useSettings();
const currentLevel = settings?.log_level ?? 2;
const isLevelUpdating = isUpdating("log_level");
const selectedValue = currentLevel.toString();
const handleSelect = async (value: string) => {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed === currentLevel) {
return;
}
try {
await updateSetting("log_level", parsed as typeof currentLevel);
} catch (error) {
console.error("Failed to update log level:", error);
}
};
return (
<SettingContainer
title="Log Level"
description="Choose how verbose Handy should be while logging to disk"
descriptionMode={descriptionMode}
grouped={grouped}
layout="horizontal"
>
<div className="space-y-1">
<Dropdown
options={LOG_LEVEL_OPTIONS}
selectedValue={selectedValue}
onSelect={handleSelect}
disabled={!settings || isLevelUpdating}
/>
</div>
</SettingContainer>
);
};

View file

@ -1 +1,3 @@
export { WordCorrectionThreshold } from "./WordCorrectionThreshold";
export { LogDirectory } from "./LogDirectory";
export { LogLevelSelector } from "./LogLevelSelector";

View file

@ -43,6 +43,9 @@ export const ClipboardHandlingSchema = z.enum([
]);
export type ClipboardHandling = z.infer<typeof ClipboardHandlingSchema>;
export const LogLevelSchema = z.number().int().min(1).max(5).default(2);
export type LogLevelValue = z.infer<typeof LogLevelSchema>;
export const RecordingRetentionPeriodSchema = z.enum([
"never",
"preserve_limit",
@ -96,6 +99,7 @@ export const SettingsSchema = z.object({
selected_language: z.string(),
overlay_position: OverlayPositionSchema,
debug_mode: z.boolean(),
log_level: LogLevelSchema.optional().default(2),
custom_words: z.array(z.string()).optional().default([]),
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
word_correction_threshold: z.number().optional().default(0.18),

View file

@ -72,6 +72,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
selected_language: "auto",
overlay_position: "bottom",
debug_mode: false,
log_level: 2,
custom_words: [],
history_limit: 5,
recording_retention_period: "preserve_limit",
@ -136,6 +137,7 @@ const settingUpdaters: {
invoke("set_post_process_selected_prompt", { id: value }),
mute_while_recording: (value) =>
invoke("change_mute_while_recording_setting", { enabled: value }),
log_level: (value) => invoke("set_log_level", { level: value }),
};
export const useSettingsStore = create<SettingsStore>()(