Skip to content

Commit 16c789d

Browse files
committed
Merge branch 'main' into post_process_hotkey
- Resolved conflicts in src-tauri/src/actions.rs - Kept both TranscribeWithPostProcessAction and new CancelAction - Resolved conflicts in src-tauri/src/lib.rs - Accepted specta_builder and typescript bindings export from main - Resolved conflicts in src-tauri/src/settings.rs - Kept both transcribe_with_post_process and cancel bindings
2 parents 15e9441 + 244a99e commit 16c789d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1993
-702
lines changed

.github/workflows/prettier.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: "prettier"
2+
on: [pull_request]
3+
4+
jobs:
5+
prettier:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v4
9+
10+
- uses: oven-sh/setup-bun@v1
11+
with:
12+
bun-version: latest
13+
14+
- name: Install dependencies
15+
run: bun install --frozen-lockfile
16+
17+
- name: Run prettier
18+
run: bun run format:check

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ This project is actively being developed and has some [known issues](https://git
8282
**Wayland Support (Linux):**
8383

8484
- Limited or no support for Wayland display server
85+
- On Wayland the clipboard-based paste options (`Clipboard (CTRL+V)` / `Clipboard (Shift+Insert)`) copy the transcription once, then try to run [`wtype`](https://github.com/atx/wtype) (preferred) or [`dotool`](https://sr.ht/~geb/dotool/) to fire the paste keystroke. Install one of these tools to let Handy drive the compositor-friendly paste shortcut; otherwise it falls back to Enigo-generated key events, which may not work on Wayland.
86+
87+
### Linux Notes
88+
89+
- The recording overlay is disabled by default on Linux (`Overlay Position: None`) because certain compositors treat it as the active window. When the overlay is visible it can steal focus, which prevents Handy from pasting back into the application that triggered transcription. If you enable the overlay anyway, be aware that clipboard-based pasting might fail or end up in the wrong window.
90+
- You can manage global shortcuts outside of Handy and still control the app via signals. Sending `SIGUSR2` to the Handy process toggles recording on/off, which lets Wayland window managers or other hotkey daemons keep ownership of keybindings. Example (Sway):
91+
92+
```ini
93+
bindsym $mod+o exec pkill -USR2 -n handy
94+
```
95+
96+
`pkill` here simply delivers the signal—it does not terminate the process.
8597

8698
### Platform Support
8799

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "handy-app",
33
"private": true,
4-
"version": "0.6.2",
4+
"version": "0.6.4",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 89 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "handy"
3-
version = "0.6.2"
3+
version = "0.6.4"
44
description = "Handy"
55
authors = ["cjpais"]
66
edition = "2021"
@@ -68,6 +68,9 @@ tar = "0.4.44"
6868
flate2 = "1.0"
6969
transcribe-rs = "0.1.4"
7070
ferrous-opencc = "0.2.3"
71+
specta = "=2.0.0-rc.22"
72+
specta-typescript = "0.0.9"
73+
tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
7174

7275
[target.'cfg(unix)'.dependencies]
7376
signal-hook = "0.3"
@@ -83,6 +86,8 @@ windows = { version = "0.61.3", features = [
8386
"Win32_Media_Audio_Endpoints",
8487
"Win32_System_Com_StructuredStorage",
8588
"Win32_System_Variant",
89+
"Win32_Foundation",
90+
"Win32_UI_WindowsAndMessaging",
8691
] }
8792

8893
[target.'cfg(target_os = "macos")'.dependencies]

src-tauri/src/actions.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use crate::audio_feedback::{play_feedback_sound, play_feedback_sound_blocking, S
22
use crate::managers::audio::AudioRecordingManager;
33
use crate::managers::history::HistoryManager;
44
use crate::managers::transcription::TranscriptionManager;
5-
use crate::overlay::{show_recording_overlay, show_transcribing_overlay};
65
use crate::settings::{get_settings, AppSettings};
6+
use crate::shortcut;
77
use crate::tray::{change_tray_icon, TrayIconState};
8-
use crate::utils;
8+
use crate::utils::{self, show_recording_overlay, show_transcribing_overlay};
99
use async_openai::types::{
1010
ChatCompletionRequestMessage, ChatCompletionRequestUserMessageArgs,
1111
CreateChatCompletionRequestArgs,
@@ -226,6 +226,7 @@ impl ShortcutAction for TranscribeAction {
226226
let is_always_on = settings.always_on_microphone;
227227
debug!("Microphone mode - always_on: {}", is_always_on);
228228

229+
let mut recording_started = false;
229230
if is_always_on {
230231
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
231232
debug!("Always-on mode: Playing audio feedback immediately");
@@ -238,14 +239,15 @@ impl ShortcutAction for TranscribeAction {
238239
rm_clone.apply_mute();
239240
});
240241

241-
let recording_started = rm.try_start_recording(&binding_id);
242+
recording_started = rm.try_start_recording(&binding_id);
242243
debug!("Recording started: {}", recording_started);
243244
} else {
244245
// On-demand mode: Start recording first, then play audio feedback, then apply mute
245246
// This allows the microphone to be activated before playing the sound
246247
debug!("On-demand mode: Starting recording first, then audio feedback");
247248
let recording_start_time = Instant::now();
248249
if rm.try_start_recording(&binding_id) {
250+
recording_started = true;
249251
debug!("Recording started in {:?}", recording_start_time.elapsed());
250252
// Small delay to ensure microphone stream is active
251253
let app_clone = app.clone();
@@ -263,13 +265,21 @@ impl ShortcutAction for TranscribeAction {
263265
}
264266
}
265267

268+
if recording_started {
269+
// Dynamically register the cancel shortcut in a separate task to avoid deadlock
270+
shortcut::register_cancel_shortcut(app);
271+
}
272+
266273
debug!(
267274
"TranscribeAction::start completed in {:?}",
268275
start_time.elapsed()
269276
);
270277
}
271278

272279
fn stop(&self, app: &AppHandle, binding_id: &str, _shortcut_str: &str) {
280+
// Unregister the cancel shortcut when transcription stops
281+
shortcut::unregister_cancel_shortcut(app);
282+
273283
let stop_time = Instant::now();
274284
debug!("TranscribeAction::stop called for binding: {}", binding_id);
275285

@@ -609,6 +619,19 @@ impl ShortcutAction for TranscribeWithPostProcessAction {
609619
}
610620
}
611621

622+
// Cancel Action
623+
struct CancelAction;
624+
625+
impl ShortcutAction for CancelAction {
626+
fn start(&self, app: &AppHandle, _binding_id: &str, _shortcut_str: &str) {
627+
utils::cancel_current_operation(app);
628+
}
629+
630+
fn stop(&self, _app: &AppHandle, _binding_id: &str, _shortcut_str: &str) {
631+
// Nothing to do on stop for cancel
632+
}
633+
}
634+
612635
// Test Action
613636
struct TestAction;
614637

@@ -643,6 +666,10 @@ pub static ACTION_MAP: Lazy<HashMap<String, Arc<dyn ShortcutAction>>> = Lazy::ne
643666
"transcribe_with_post_process".to_string(),
644667
Arc::new(TranscribeWithPostProcessAction) as Arc<dyn ShortcutAction>,
645668
);
669+
map.insert(
670+
"cancel".to_string(),
671+
Arc::new(CancelAction) as Arc<dyn ShortcutAction>,
672+
);
646673
map.insert(
647674
"test".to_string(),
648675
Arc::new(TestAction) as Arc<dyn ShortcutAction>,

src-tauri/src/audio_feedback.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub fn play_feedback_sound_blocking(app: &AppHandle, sound_type: SoundType) {
6363
pub fn play_test_sound(app: &AppHandle, sound_type: SoundType) {
6464
let settings = settings::get_settings(app);
6565
if let Some(path) = resolve_sound_path(app, &settings, sound_type) {
66-
play_sound_async(app, path);
66+
play_sound_blocking(app, &path);
6767
}
6868
}
6969

src-tauri/src/audio_toolkit/audio/recorder.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,36 @@ impl AudioRecorder {
204204
device: &cpal::Device,
205205
) -> Result<cpal::SupportedStreamConfig, Box<dyn std::error::Error>> {
206206
let supported_configs = device.supported_input_configs()?;
207+
let mut best_config: Option<cpal::SupportedStreamConfigRange> = None;
207208

208-
// Try to find a config that supports 16kHz
209+
// Try to find a config that supports 16kHz, prioritizing better formats
209210
for config_range in supported_configs {
210211
if config_range.min_sample_rate().0 <= constants::WHISPER_SAMPLE_RATE
211212
&& config_range.max_sample_rate().0 >= constants::WHISPER_SAMPLE_RATE
212213
{
213-
// Found a config that supports 16kHz, use it
214-
return Ok(
215-
config_range.with_sample_rate(cpal::SampleRate(constants::WHISPER_SAMPLE_RATE))
216-
);
214+
match best_config {
215+
None => best_config = Some(config_range),
216+
Some(ref current) => {
217+
// Prioritize F32 > I16 > I32 > others
218+
let score = |fmt: cpal::SampleFormat| match fmt {
219+
cpal::SampleFormat::F32 => 4,
220+
cpal::SampleFormat::I16 => 3,
221+
cpal::SampleFormat::I32 => 2,
222+
_ => 1,
223+
};
224+
225+
if score(config_range.sample_format()) > score(current.sample_format()) {
226+
best_config = Some(config_range);
227+
}
228+
}
229+
}
217230
}
218231
}
219232

233+
if let Some(config) = best_config {
234+
return Ok(config.with_sample_rate(cpal::SampleRate(constants::WHISPER_SAMPLE_RATE)));
235+
}
236+
220237
// If no config supports 16kHz, fall back to default
221238
Ok(device.default_input_config()?)
222239
}

0 commit comments

Comments
 (0)