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
3 changes: 3 additions & 0 deletions apps/whispering/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"recorder-state-icons/*"
],
"longDescription": "Seamlessly integrate speech-to-text transcriptions anywhere on your desktop. Powered by OpenAI's Whisper API.",
"shortDescription": "Press shortcut → speak → get text. Free and open source ❤️",
"createUpdaterArtifacts": true,
Expand Down
190 changes: 134 additions & 56 deletions apps/whispering/src/lib/services/tray.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu';
import { resolveResource } from '@tauri-apps/api/path';
import { TrayIcon } from '@tauri-apps/api/tray';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { exit } from '@tauri-apps/plugin-process';
import { createTaggedError } from 'wellcrafted/error';
// import { commandCallbacks } from '$lib/commands';
import { type Err, Ok, tryAsync } from 'wellcrafted/result';
import { goto } from '$app/navigation';
// import { extension } from '@repo/extension';
import { commandCallbacks } from '$lib/commands';
import type { WhisperingRecordingState } from '$lib/constants/audio';

const TRAY_ID = 'whispering-tray';
Expand All @@ -21,34 +20,35 @@ type SetTrayIconService = {
setTrayIcon: (
icon: WhisperingRecordingState,
) => Promise<Ok<void> | Err<SetTrayIconServiceError>>;
updateMenu: (
recorderState: WhisperingRecordingState,
) => Promise<Ok<void> | Err<SetTrayIconServiceError>>;
};

export function createTrayIconWebService(): SetTrayIconService {
return {
setTrayIcon: async (icon: WhisperingRecordingState) => {
// const { error: setRecorderStateError } = await extension.setRecorderState(
// { recorderState: icon },
// );
// if (setRecorderStateError)
// return SetTrayIconServiceErr({
// message: 'Failed to set recorder state',
// context: { icon },
// cause: setRecorderStateError,
// });
return Ok(undefined);
},
updateMenu: async (recorderState: WhisperingRecordingState) => {
return Ok(undefined);
},
};
}

export function createTrayIconDesktopService(): SetTrayIconService {
const trayPromise = initTray();

return {
setTrayIcon: (recorderState: WhisperingRecordingState) =>
tryAsync({
try: async () => {
const iconPath = await getIconPath(recorderState);
const tray = await trayPromise;
return tray.setIcon(iconPath);
await tray.setIcon(iconPath);
// Update menu when icon changes
const menu = await createTrayMenu(recorderState);
await tray.setMenu(menu);
},
catch: (error) =>
SetTrayIconServiceErr({
Expand All @@ -57,63 +57,113 @@ export function createTrayIconDesktopService(): SetTrayIconService {
cause: error,
}),
}),
updateMenu: (recorderState: WhisperingRecordingState) =>
tryAsync({
try: async () => {
const tray = await trayPromise;
const menu = await createTrayMenu(recorderState);
await tray.setMenu(menu);
},
catch: (error) =>
SetTrayIconServiceErr({
message: 'Failed to update tray menu',
context: { icon: recorderState },
cause: error,
}),
}),
};
}

async function initTray() {
const existingTray = await TrayIcon.getById(TRAY_ID);
if (existingTray) return existingTray;

const trayMenu = await Menu.new({
items: [
// Window Controls Section
await MenuItem.new({
id: 'show',
text: 'Show Window',
action: () => getCurrentWindow().show(),
}),
async function createTrayMenu(recorderState: WhisperingRecordingState) {
const items = [];

// Recording Controls Section
if (recorderState === 'RECORDING') {
items.push(
await MenuItem.new({
id: 'hide',
text: 'Hide Window',
action: () => getCurrentWindow().hide(),
id: 'stop-recording',
text: 'Stop Recording',
action: () => commandCallbacks.stopManualRecording(),
}),

// Settings Section
);
items.push(
await MenuItem.new({
id: 'settings',
text: 'Settings',
id: 'view-recording',
text: 'View Recording',
action: () => {
goto('/settings');
goto('/');
return getCurrentWindow().show();
},
}),

// Quit Section
);
items.push(await PredefinedMenuItem.new({ item: 'Separator' }));
} else {
items.push(
await MenuItem.new({
id: 'quit',
text: 'Quit',
action: () => void exit(0),
id: 'start-recording',
text: 'Start Recording',
action: () => commandCallbacks.startManualRecording(),
}),
],
});
);
items.push(await PredefinedMenuItem.new({ item: 'Separator' }));
}

// Window Controls Section
items.push(
await MenuItem.new({
id: 'show',
text: 'Show Window',
action: () => getCurrentWindow().show(),
}),
);
items.push(
await MenuItem.new({
id: 'hide',
text: 'Hide Window',
action: () => getCurrentWindow().hide(),
}),
);
items.push(await PredefinedMenuItem.new({ item: 'Separator' }));

// Settings Section
items.push(
await MenuItem.new({
id: 'settings',
text: 'Settings',
action: () => {
goto('/settings');
return getCurrentWindow().show();
},
}),
);
items.push(await PredefinedMenuItem.new({ item: 'Separator' }));

// Quit Section
items.push(
await MenuItem.new({
id: 'quit',
text: 'Quit',
action: () => void exit(0),
}),
);

return Menu.new({ items });
}

async function initTray() {
const existingTray = await TrayIcon.getById(TRAY_ID);
if (existingTray) {
return existingTray;
}

const trayMenu = await createTrayMenu('IDLE');
const iconPath = await getIconPath('IDLE');

const tray = await TrayIcon.new({
id: TRAY_ID,
icon: await getIconPath('IDLE'),
icon: iconPath,
menu: trayMenu,
menuOnLeftClick: false,
action: (e) => {
if (
e.type === 'Click' &&
e.button === 'Left' &&
e.buttonState === 'Down'
) {
// commandCallbacks.toggleManualRecording();
return true;
}
return false;
},
menuOnLeftClick: true,
});

return tray;
Expand All @@ -127,6 +177,34 @@ async function getIconPath(recorderState: WhisperingRecordingState) {
return await resolveResource(iconPaths[recorderState]);
}

export const TrayIconServiceLive = window.__TAURI_INTERNALS__
? createTrayIconDesktopService()
: createTrayIconWebService();
export const TrayIconServiceLive = {
setTrayIcon: async (icon: WhisperingRecordingState) => {
if (window.__TAURI_INTERNALS__) {
if (!desktopServiceInstance) {
desktopServiceInstance = createTrayIconDesktopService();
}
return await desktopServiceInstance.setTrayIcon(icon);
} else {
if (!webServiceInstance) {
webServiceInstance = createTrayIconWebService();
}
return await webServiceInstance.setTrayIcon(icon);
}
},
updateMenu: async (recorderState: WhisperingRecordingState) => {
if (window.__TAURI_INTERNALS__) {
if (!desktopServiceInstance) {
desktopServiceInstance = createTrayIconDesktopService();
}
return await desktopServiceInstance.updateMenu(recorderState);
} else {
if (!webServiceInstance) {
webServiceInstance = createTrayIconWebService();
}
return await webServiceInstance.updateMenu(recorderState);
}
},
};

let desktopServiceInstance: SetTrayIconService | null = null;
let webServiceInstance: SetTrayIconService | null = null;
3 changes: 3 additions & 0 deletions apps/whispering/src/routes/(app)/_components/AppLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
if (window.__TAURI_INTERNALS__) {
syncWindowAlwaysOnTopWithRecorderState();
syncIconWithRecorderState();

// Initialize tray icon immediately
services.tray.setTrayIcon('IDLE');
}

$effect(() => {
Expand Down
Loading