Skip to content

Commit 71b3e61

Browse files
committed
Bugfix: Fix ⌘, to refocus Settings window if open
- Add missing allow-set-focus capability for the settings window - Settings window self-focuses via focus-self event (cross-window setFocus doesn't reliably bring windows to front on macOS) - Use getByLabel() instead of stale module-level JS window ref - Add capabilities/CLAUDE.md to prevent similar permission gaps
1 parent a0911c7 commit 71b3e61

5 files changed

Lines changed: 41 additions & 38 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Tauri capabilities
2+
3+
Each window has its own capability file controlling which Tauri APIs it can call.
4+
When adding a Tauri API call to a window (setFocus, setTitle, show, etc.),
5+
you must add the corresponding permission here or the call will be silently
6+
rejected at runtime with a "not allowed" error.
7+
8+
- `default.json` — main window
9+
- `desktop.json` — desktop-wide permissions
10+
- `settings.json` — settings window
11+
- `viewer.json` — file viewer windows
12+
13+
Check the [Tauri permissions reference](https://tauri.app/security/permissions/)
14+
for available permission identifiers.

apps/desktop/src-tauri/capabilities/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
],
88
"permissions": [
99
"core:window:allow-close",
10+
"core:window:allow-set-focus",
1011
"core:event:default",
1112
"core:app:allow-set-app-theme",
1213
"core:webview:allow-internal-toggle-devtools",

apps/desktop/src/lib/settings/settings-window.ts

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
*/
55

66
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
7+
import { emitTo } from '@tauri-apps/api/event'
78
import { getAppLogger } from '$lib/logging/logger'
89

910
const log = getAppLogger('settings')
1011

11-
let settingsWindow: WebviewWindow | null = null
12-
1312
const SETTINGS_WIDTH = 800
1413
const SETTINGS_HEIGHT = 600
1514
const SETTINGS_MAX_WIDTH = 852
@@ -18,29 +17,21 @@ const SETTINGS_MIN_HEIGHT = 400
1817

1918
/**
2019
* Opens the settings window, or focuses it if already open.
21-
* Window always opens centered on screen.
20+
* Uses `WebviewWindow.getByLabel` to reliably detect an existing window
21+
* instead of a module-level JS reference that can go stale.
2222
*/
2323
export async function openSettingsWindow(): Promise<void> {
24-
log.debug('openSettingsWindow called')
25-
26-
// Check if window already exists
27-
if (settingsWindow) {
28-
log.debug('Settings window already exists, attempting to focus')
29-
try {
30-
await settingsWindow.setFocus()
31-
log.debug('Focused existing settings window')
32-
return
33-
} catch (error) {
34-
// Window was closed, create a new one
35-
log.debug('Failed to focus existing window (likely closed), creating new: {error}', { error })
36-
settingsWindow = null
37-
}
24+
const existing = await WebviewWindow.getByLabel('settings')
25+
if (existing) {
26+
// Emit to the settings window so it can self-focus. Cross-window setFocus()
27+
// doesn't reliably bring a window to front on macOS.
28+
await emitTo('settings', 'focus-self')
29+
return
3830
}
3931

40-
log.info('Creating new settings window with url=/settings')
32+
log.info('Creating new settings window')
4133

42-
// Create new settings window, centered on screen
43-
settingsWindow = new WebviewWindow('settings', {
34+
new WebviewWindow('settings', {
4435
url: '/settings',
4536
title: 'Settings',
4637
width: SETTINGS_WIDTH,
@@ -51,22 +42,6 @@ export async function openSettingsWindow(): Promise<void> {
5142
center: true,
5243
resizable: true,
5344
decorations: true,
54-
})
55-
56-
// Listen for window creation success
57-
void settingsWindow.once('tauri://created', () => {
58-
log.info('Settings window created successfully')
59-
})
60-
61-
// Listen for window close to clean up reference
62-
void settingsWindow.once('tauri://destroyed', () => {
63-
log.debug('Settings window destroyed, cleaning up reference')
64-
settingsWindow = null
65-
})
66-
67-
// Handle any creation errors
68-
void settingsWindow.once('tauri://error', (e) => {
69-
log.error('Failed to create settings window: {error}', { error: e })
70-
settingsWindow = null
45+
focus: true,
7146
})
7247
}

apps/desktop/src/routes/settings/+page.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { onMount, onDestroy, tick } from 'svelte'
33
import { getCurrentWindow } from '@tauri-apps/api/window'
4+
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
45
import SettingsSidebar from '$lib/settings/components/SettingsSidebar.svelte'
56
import SettingsContent from '$lib/settings/components/SettingsContent.svelte'
67
import { initializeSettings, forceSave as forceSettingsSave } from '$lib/settings'
@@ -23,6 +24,7 @@
2324
let selectedSection = $state<string[]>(['General', 'Appearance'])
2425
let initialized = $state(false)
2526
let contentElement: HTMLElement | null = $state(null)
27+
let unlistenFocusSelf: UnlistenFn | undefined
2628
2729
// Log page script initialization
2830
log.debug('Settings page script loaded')
@@ -127,6 +129,15 @@
127129
// Focus will be handled naturally by the browser's tab order
128130
await tick()
129131
132+
// Listen for focus-self events (from ⌘, when window is already open).
133+
// Self-focusing is needed because cross-window setFocus() doesn't reliably
134+
// bring a window to front on macOS.
135+
unlistenFocusSelf = await listen('focus-self', () => {
136+
// setTimeout(0) defers past the originating keydown handler —
137+
// without it, macOS restores focus to the main window.
138+
setTimeout(() => void getCurrentWindow().setFocus(), 0)
139+
})
140+
130141
// Set up MCP event listeners and sync initial state
131142
await setupMcpEventListeners(handleSectionSelect, handleSettingChanged)
132143
await notifySettingsWindowOpen(true)
@@ -143,7 +154,8 @@
143154
log.debug('Settings page destroying, flushing pending saves')
144155
// Fire and forget - we can't await in onDestroy
145156
void Promise.all([forceSettingsSave(), flushShortcutsSave()])
146-
// Clean up MCP event listeners and notify backend
157+
// Clean up event listeners and notify backend
158+
unlistenFocusSelf?.()
147159
cleanupMcpEventListeners()
148160
cleanupAccentColor()
149161
void notifySettingsWindowOpen(false)

docs/architecture.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ All under `apps/desktop/src-tauri/src/`.
5656
| `drag_image_detection.rs` | macOS method swizzle for drag image size detection |
5757
| `drag_image_swap.rs` | Rich/transparent drag image swap for self-drags |
5858
| `commands/` | Tauri command definitions (IPC entry points) |
59+
| `capabilities/` | Per-window Tauri API permissions — must be updated when using new Tauri APIs from a window |
5960

6061
## Other apps
6162

0 commit comments

Comments
 (0)