Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions apps/whispering/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub async fn run() {
// Register command handlers (same for all platforms now)
let builder = builder.invoke_handler(tauri::generate_handler![
write_text,
press_enter,
// Audio recorder commands
get_current_recording_id,
enumerate_recording_devices,
Expand Down Expand Up @@ -170,3 +171,31 @@ async fn write_text(app: tauri::AppHandle, text: String) -> Result<(), String> {
Ok(())
}

/// Simulates pressing the Enter key
///
/// This function simulates pressing the Enter key to trigger actions like submitting
/// forms or sending messages. It's useful for automating user actions after
/// transcription completes.
#[tauri::command]
async fn press_enter() -> Result<(), String> {
let mut enigo = Enigo::new(&Settings::default()).map_err(|e| e.to_string())?;

// Use virtual key codes for Enter/Return to work with any keyboard layout
#[cfg(target_os = "macos")]
let enter_key = Key::Other(36); // Virtual key code for Return on macOS
#[cfg(target_os = "windows")]
let enter_key = Key::Other(0x0D); // VK_RETURN on Windows
#[cfg(target_os = "linux")]
let enter_key = Key::Return; // Return key on Linux

// Press and release Enter
enigo
.key(enter_key, Direction::Press)
.map_err(|e| format!("Failed to press Enter key: {}", e))?;
enigo
.key(enter_key, Direction::Release)
.map_err(|e| format!("Failed to release Enter key: {}", e))?;

Ok(())
}

37 changes: 29 additions & 8 deletions apps/whispering/src/lib/query/delivery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { WHISPERING_RECORDINGS_PATHNAME } from '$lib/constants/app';
import { settings } from '$lib/stores/settings.svelte';
import { shouldTriggerEnterOnOver, removeOverFromEnd } from '$lib/utils/transcription-triggers';
import { Ok } from 'wellcrafted/result';
import { defineMutation } from './_client';
import { rpc } from './index';
Expand Down Expand Up @@ -42,6 +43,12 @@ export const delivery = {
text: string;
toastId: string;
}) => {
// Check if text ends with "Over" to trigger Enter after delivery
const shouldPressEnter = shouldTriggerEnterOnOver(text);

// Remove "Over" from the text if present before delivering
const cleanedText = shouldPressEnter ? removeOverFromEnd(text) : text;

// Track what operations succeeded
let copied = false;
let written = false;
Expand All @@ -51,13 +58,13 @@ export const delivery = {
rpc.notify.success.execute({
id: toastId,
title: '📝 Recording transcribed!',
description: text,
description: cleanedText,
action: {
type: 'button',
label: 'Copy to clipboard',
onClick: async () => {
const { error } = await rpc.text.copyToClipboard.execute({
text,
text: cleanedText,
});
if (error) {
// Report that manual copy attempt failed
Expand All @@ -72,7 +79,7 @@ export const delivery = {
rpc.notify.success.execute({
id: toastId,
title: 'Copied transcribed text to clipboard!',
description: text,
description: cleanedText,
});
},
},
Expand Down Expand Up @@ -112,7 +119,7 @@ export const delivery = {
rpc.notify.success.execute({
id: toastId,
title: '📝 Recording transcribed, copied to clipboard, and written to cursor!',
description: text,
description: cleanedText,
action: {
type: 'link',
label: 'Go to recordings',
Expand All @@ -124,7 +131,7 @@ export const delivery = {
rpc.notify.success.execute({
id: toastId,
title: '📝 Recording transcribed and copied to clipboard!',
description: text,
description: cleanedText,
action: {
type: 'link',
label: 'Go to recordings',
Expand All @@ -136,7 +143,7 @@ export const delivery = {
rpc.notify.success.execute({
id: toastId,
title: '📝 Recording transcribed and written to cursor!',
description: text,
description: cleanedText,
action: {
type: 'link',
label: 'Go to recordings',
Expand All @@ -154,7 +161,7 @@ export const delivery = {
// Check if user wants to copy to clipboard
if (settings.value['transcription.copyToClipboardOnSuccess']) {
const { error: copyError } = await rpc.text.copyToClipboard.execute({
text,
text: cleanedText,
});
if (!copyError) {
copied = true;
Expand All @@ -166,7 +173,7 @@ export const delivery = {
// Check if user wants to write to cursor (independent of copy)
if (settings.value['transcription.writeToCursorOnSuccess']) {
const { error: writeError } = await rpc.text.writeToCursor.execute({
text,
text: cleanedText,
});
if (!writeError) {
written = true;
Expand All @@ -178,6 +185,20 @@ export const delivery = {
// Show appropriate notification
showSuccessNotification();

// Press Enter if "Over" was detected at the end of the original text
if (shouldPressEnter) {
// Small delay to ensure any clipboard operations complete
await new Promise(resolve => setTimeout(resolve, 200));

const { error: pressEnterError } = await rpc.text.pressEnter.execute(undefined);
if (pressEnterError) {
// Log the error but don't disrupt the main flow
console.warn('Failed to automatically press Enter after "Over" detection:', pressEnterError);
} else {
console.info('Automatically pressed Enter after detecting and removing "Over" from transcription');
}
}

return Ok(undefined);
},
}),
Expand Down
7 changes: 7 additions & 0 deletions apps/whispering/src/lib/query/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ export const text = {
return await services.text.writeToCursor(text);
},
}),
pressEnter: defineMutation({
mutationKey: ['text', 'pressEnter'],
resultMutationFn: async () => {
// Simulates pressing the Enter key
return await services.text.pressEnter();
},
}),
};
12 changes: 12 additions & 0 deletions apps/whispering/src/lib/services/text/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,17 @@ export function createTextServiceDesktop(): TextService {
cause: error,
}),
}),

pressEnter: () =>
tryAsync({
try: () => invoke<void>('press_enter'),
catch: (error) =>
TextServiceErr({
message:
'There was an error simulating the Enter key press. Please try pressing Enter manually.',
context: {},
cause: error,
}),
}),
};
}
11 changes: 9 additions & 2 deletions apps/whispering/src/lib/services/text/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ export type TextService = {
/**
* Writes the provided text at the current cursor position.
* Uses the clipboard sandwich technique to preserve the user's existing clipboard content.
*
*
* This method:
* 1. Saves the current clipboard
* 2. Writes the text to clipboard
* 3. Simulates paste (Cmd+V on macOS, Ctrl+V elsewhere)
* 4. Restores the original clipboard
*
*
* @param text The text to write at the cursor position.
*/
writeToCursor: (
text: string,
) => MaybePromise<Result<void, TextServiceError | WhisperingError>>;

/**
* Simulates pressing the Enter key.
* This is useful for automatically submitting forms or sending messages
* after transcription completes.
*/
pressEnter: () => Promise<Result<void, TextServiceError>>;
};
10 changes: 10 additions & 0 deletions apps/whispering/src/lib/services/text/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,15 @@ export function createTextServiceWeb(): TextService {
cause: undefined,
});
},

pressEnter: async () => {
// In web browsers, we cannot programmatically simulate key presses for security reasons
return TextServiceErr({
message:
'Enter key simulation is not supported in web browsers for security reasons. Please press Enter manually.',
context: {},
cause: undefined,
});
},
};
}