Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
db4885d
chore: add daily-driver worktree plan; add macOS auto pause/resume st…
unlox775-code-dot-org Oct 1, 2025
34f92fe
First draft
unlox775-code-dot-org Oct 1, 2025
121bb12
i
unlox775-code-dot-org Oct 1, 2025
d23222e
iter
unlox775-code-dot-org Oct 1, 2025
c13f3b7
iter
unlox775-code-dot-org Oct 1, 2025
800c83c
Update +page.svelte
unlox775-code-dot-org Oct 1, 2025
15b5097
Move settings
unlox775-code-dot-org Oct 1, 2025
ab45802
Update +page.svelte
unlox775-code-dot-org Oct 1, 2025
3ee524a
Update +page.svelte
unlox775-code-dot-org Oct 1, 2025
46459fb
Delete 20251001T120000-daily-driver-combined-branches.md
unlox775-code-dot-org Oct 22, 2025
8f511bb
Update commands.ts
unlox775-code-dot-org Oct 23, 2025
d2f3c3b
Also pause Books app
unlox775-code-dot-org Oct 25, 2025
a261b16
Update macos_media.rs
unlox775-code-dot-org Oct 25, 2025
6e2c181
Update macos_media.rs
unlox775-code-dot-org Oct 25, 2025
5558684
debug
unlox775-code-dot-org Oct 30, 2025
7ffc3ef
Update macos_media.rs
unlox775-code-dot-org Oct 30, 2025
9a99ea1
debug
unlox775-code-dot-org Oct 31, 2025
faa9f88
Update macos_media.rs
unlox775-code-dot-org Oct 31, 2025
0a6a1e2
Attempt async
unlox775-code-dot-org Oct 31, 2025
9f3d65a
debug
unlox775-code-dot-org Oct 31, 2025
d51341a
cleaning up
unlox775-code-dot-org Oct 31, 2025
7c1aab1
Update macos_media.rs
unlox775-code-dot-org Oct 31, 2025
d073110
-c
unlox775-code-dot-org Oct 31, 2025
32ae3e7
Merge remote-tracking branch 'upstream/main' into dave-auto-pause-res…
unlox775-code-dot-org Dec 5, 2025
b255a93
Update tauri.conf.json
unlox775-code-dot-org Dec 5, 2025
47cd070
Update bun.lock
unlox775-code-dot-org Dec 5, 2025
0574729
Lint Fixes
unlox775-code-dot-org Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/whispering/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ use transcription::{transcribe_audio_parakeet, transcribe_audio_whisper, ModelMa
pub mod windows_path;
use windows_path::fix_windows_path;

pub mod graceful_shutdown;
use graceful_shutdown::send_sigint;

pub mod command;
use command::{execute_command, spawn_command};

pub mod markdown_reader;
use markdown_reader::{bulk_delete_files, count_markdown_files, read_markdown_files};
pub mod graceful_shutdown;
use graceful_shutdown::send_sigint;
// macOS media control (functions are defined for all targets with internal cfg handling)
pub mod macos_media;
use macos_media::{macos_pause_active_media, macos_resume_media};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
#[tokio::main]
Expand Down Expand Up @@ -163,6 +165,9 @@ pub async fn run() {
read_markdown_files,
count_markdown_files,
bulk_delete_files,
// macOS media control
macos_pause_active_media,
macos_resume_media,
]);

let app = builder
Expand Down
159 changes: 159 additions & 0 deletions apps/whispering/src-tauri/src/macos_media.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@

#[derive(serde::Serialize, serde::Deserialize)]
pub struct PausedPlayers {
pub players: Vec<String>,
}

#[tauri::command]
pub async fn macos_pause_active_media() -> Result<PausedPlayers, String> {
#[cfg(target_os = "macos")]
{
// Run Music and Spotify checks concurrently with short AppleScript timeouts
let music_script = r#"
try
with timeout of 1.0 seconds
tell application "Music"
if it is running then
if player state is playing then
pause
return "Music"
end if
end if
end tell
end timeout
end try
return ""
"#;

let spotify_script = r#"
try
with timeout of 1.0 seconds
tell application "Spotify"
if it is running then
if player state is playing then
pause
return "Spotify"
end if
end if
end tell
end timeout
end try
return ""
"#;

let (music_out, spotify_out) = tokio::join!(
async {
run_osascript(music_script).await
},
async {
run_osascript(spotify_script).await
}
);

let mut paused_players = Vec::new();
if let Ok(output) = music_out {
if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); }
}
if let Ok(output) = spotify_out {
if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); }
}

// Only check Books if nothing else was paused
if paused_players.is_empty() {
let books_result = run_osascript(r#"
try
with timeout of 0.3 seconds
tell application "System Events"
set booksProcessExists to exists process "Books"
end tell
if booksProcessExists then
tell application "System Events"
tell process "Books"
if exists menu item "Pause" of menu "Controls" of menu bar 1 then
click menu item "Pause" of menu "Controls" of menu bar 1
return "Books"
end if
end tell
end tell
end if
end timeout
end try
return ""
"#).await;

if let Ok(output) = books_result {
if !output.trim().is_empty() {
paused_players.push(output.trim().to_string());
}
}
}

return Ok(PausedPlayers { players: paused_players });
}

#[cfg(not(target_os = "macos"))]
{
Ok(PausedPlayers { players: vec![] })
}
}

#[tauri::command]
pub async fn macos_resume_media(players: Vec<String>) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
// Build AppleScript dynamically based on players
let mut script = String::new();
for p in players {
match p.as_str() {
"Music" => {
script.push_str(
"try\n tell application \"Music\"\n if it is running then play\n end tell\nend try\n",
);
}
"Spotify" => {
script.push_str(
"try\n tell application \"Spotify\"\n if it is running then play\n end tell\nend try\n",
);
}
"Books" => {
script.push_str(
"try\n with timeout of 1 seconds\n tell application \"System Events\"\n tell process \"Books\"\n click menu item \"Play\" of menu \"Controls\" of menu bar 1\n end tell\n end tell\n end timeout\nend try\n",
);
}
_ => {}
}
}

if script.is_empty() {
return Ok(());
}

run_osascript(&script).await.map(|_| ())
}

#[cfg(not(target_os = "macos"))]
{
Ok(())
}
}

#[cfg(target_os = "macos")]
async fn run_osascript(script: &str) -> Result<String, String> {
use tokio::process::Command;

let output = Command::new("osascript")
.arg("-e")
.arg(script)
.output()
.await
.map_err(|e| e.to_string())?;

if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(stdout)
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Err(stderr)
}
}

1 change: 0 additions & 1 deletion apps/whispering/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"type": "embedBootstrapper"
},
"wix": {
"fipsCompliant": true,
"language": "en-US"
},
"nsis": {
Expand Down
58 changes: 54 additions & 4 deletions apps/whispering/src/lib/query/actions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { nanoid } from 'nanoid/non-secure';
import { Err, Ok } from 'wellcrafted/result';
import { fromTaggedError, WhisperingErr } from '$lib/result';
import {
fromTaggedError,
WhisperingErr,
type WhisperingError,
} from '$lib/result';
import { DbServiceErr } from '$lib/services/db';
import { settings } from '$lib/stores/settings.svelte';
import * as transformClipboardWindow from '../../routes/transform-clipboard/transformClipboardWindow.tauri';
import { rpc } from './';
import { defineMutation } from './_client';
import { db } from './db';
import { delivery } from './delivery';
import { media } from './media';
import { notify } from './notify';
import { recorder } from './recorder';
import { sound } from './sound';
Expand All @@ -27,6 +32,7 @@ import { vadRecorder } from './vad.svelte';

// Track manual recording start time for duration calculation
let manualRecordingStartTime: number | null = null;
let currentMediaSessionId: string | null = null;

/**
* Mutex flag to prevent concurrent recording operations.
Expand Down Expand Up @@ -61,6 +67,9 @@ const startManualRecording = defineMutation({
title: '🎙️ Preparing to record...',
description: 'Setting up your recording environment...',
});
// Background media pause: do not await; track by sessionId for later resume
currentMediaSessionId = nanoid();
void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId });

const { data: deviceAcquisitionOutcome, error: startRecordingError } =
await recorder.startRecording.execute({ toastId });
Expand Down Expand Up @@ -152,6 +161,11 @@ const stopManualRecording = defineMutation({
// This allows new recordings to start while pipeline runs
isRecordingOperationBusy = false;

// Resume media for this session
if (currentMediaSessionId) {
await media.resumePaused.execute({ sessionId: currentMediaSessionId });
currentMediaSessionId = null;
}
if (stopRecordingError) {
notify.error.execute({ id: toastId, ...stopRecordingError });
return Ok(undefined);
Expand Down Expand Up @@ -206,6 +220,10 @@ const startVadRecording = defineMutation({
title: '🎙️ Starting voice activated capture',
description: 'Your voice activated capture is starting...',
});
// Pause media before starting VAD
currentMediaSessionId = nanoid();
void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId });

const { data: deviceAcquisitionOutcome, error: startActiveListeningError } =
await vadRecorder.startActiveListening({
onSpeechStart: () => {
Expand Down Expand Up @@ -241,11 +259,22 @@ const startVadRecording = defineMutation({
},
});
if (startActiveListeningError) {
notify.error.execute({ id: toastId, ...startActiveListeningError });
const error = startActiveListeningError as WhisperingError;
notify.error.execute({
id: toastId,
title: error?.title ?? '❌ Failed to start VAD',
description:
error?.description ??
'Voice activity detection could not be started.',
action: { type: 'more-details', error: startActiveListeningError },
});
return Ok(undefined);
}

// Handle device acquisition outcome
if (!deviceAcquisitionOutcome) {
return Ok(undefined);
}
switch (deviceAcquisitionOutcome.outcome) {
case 'success': {
notify.success.execute({
Expand Down Expand Up @@ -309,8 +338,21 @@ const stopVadRecording = defineMutation({
description: 'Finalizing your voice activated capture...',
});
const { error: stopVadError } = await vadRecorder.stopActiveListening();
// Resume media for this session
if (currentMediaSessionId) {
await media.resumePaused.execute({ sessionId: currentMediaSessionId });
currentMediaSessionId = null;
}
if (stopVadError) {
notify.error.execute({ id: toastId, ...stopVadError });
const error = stopVadError as WhisperingError;
notify.error.execute({
id: toastId,
title: error?.title ?? '❌ Failed to stop VAD',
description:
error?.description ??
'Voice activity detection could not be stopped.',
action: { type: 'more-details', error: stopVadError },
});
return Ok(undefined);
}
notify.success.execute({
Expand Down Expand Up @@ -371,6 +413,11 @@ export const commands = {
// Release mutex after the actual cancel operation completes
isRecordingOperationBusy = false;

// Resume media for this session
if (currentMediaSessionId) {
await media.resumePaused.execute({ sessionId: currentMediaSessionId });
currentMediaSessionId = null;
}
if (cancelRecordingError) {
notify.error.execute({ id: toastId, ...cancelRecordingError });
return Ok(undefined);
Expand Down Expand Up @@ -406,7 +453,10 @@ export const commands = {
toggleVadRecording: defineMutation({
mutationKey: ['commands', 'toggleVadRecording'] as const,
resultMutationFn: async () => {
if (vadRecorder.state === 'LISTENING' || vadRecorder.state === 'SPEECH_DETECTED') {
if (
vadRecorder.state === 'LISTENING' ||
vadRecorder.state === 'SPEECH_DETECTED'
) {
return await stopVadRecording.execute(undefined);
}
return await startVadRecording.execute(undefined);
Expand Down
2 changes: 2 additions & 0 deletions apps/whispering/src/lib/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { db } from './db';
import { delivery } from './delivery';
import { download } from './download';
import { ffmpeg } from './ffmpeg';
import { media } from './media';
import { notify } from './notify';
import { recorder } from './recorder';
import { shortcuts } from './shortcuts';
Expand Down Expand Up @@ -34,4 +35,5 @@ export const rpc = {
transformer,
notify,
delivery,
media,
};
Loading