From f118fb704ccf640893ca0fa21d357293ffa609b2 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 12:01:06 -0700 Subject: [PATCH 01/17] feat: add global hotkey overlay panel --- GhosttyTabs.xcodeproj/project.pbxproj | 8 + README.md | 1 + Resources/Localizable.xcstrings | 63 ++++-- Sources/App/GlobalHotkeyPanelController.swift | 210 ++++++++++++++++++ Sources/AppDelegate.swift | 40 +++- Sources/KeyboardShortcutSettings.swift | 2 +- Sources/SettingsNavigation.swift | 4 +- Sources/SettingsSearchAliases.swift | 6 +- Sources/cmuxApp.swift | 6 +- .../GlobalHotkeyPanelControllerTests.swift | 58 +++++ web/data/cmux-shortcuts.ts | 2 +- 11 files changed, 361 insertions(+), 39 deletions(-) create mode 100644 Sources/App/GlobalHotkeyPanelController.swift create mode 100644 cmuxTests/GlobalHotkeyPanelControllerTests.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index d11940b9ec11..e86e767ff4c5 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ A11EB0000000000000000000 /* GhosttyConfigPathResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11EB0000000000000000001 /* GhosttyConfigPathResolverTests.swift */; }; 1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */; }; 17C9F5BA0DD14EDC8C3E5001 /* MainWindowVisibilityControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C9F5BA0DD14EDC8C3E5002 /* MainWindowVisibilityControllerTests.swift */; }; + 2758A0032758A0032758A003 /* GlobalHotkeyPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */; }; 1A1B2C3D4E5F607180000001 /* WorkspacePromptSubmit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1B2C3D4E5F607180000002 /* WorkspacePromptSubmit.swift */; }; 1A1B2C3D4E5F607180000003 /* WorkspacePromptSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1B2C3D4E5F607180000004 /* WorkspacePromptSubmitTests.swift */; }; 1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */; }; @@ -153,6 +154,7 @@ 76027A12C93B4538BF22C71E /* ShortcutBareStartRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */; }; E5C0F1A0E5C0F1A0E5C0F1A0 /* TerminalFindEscapeRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */; }; 1A8BEE693C9E4C3190CB7F20 /* MenuBarExtraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */; }; + 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */; }; 2F0C07000000000000000002 /* CmuxMainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0C07000000000000000001 /* CmuxMainWindow.swift */; }; 2F0C06000000000000000002 /* MainWindowVisibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0C06000000000000000001 /* MainWindowVisibilityController.swift */; }; 2F0C05000000000000000002 /* MainWindowFocusController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0C05000000000000000001 /* MainWindowFocusController.swift */; }; @@ -520,6 +522,7 @@ 1A1B2C3D4E5F607180000004 /* WorkspacePromptSubmitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePromptSubmitTests.swift; sourceTree = ""; }; 1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceManualUnreadTests.swift; sourceTree = ""; }; 2F0C05000000000000000001 /* MainWindowFocusController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindowFocusController.swift; sourceTree = ""; }; + 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalHotkeyPanelControllerTests.swift; sourceTree = ""; }; 3023B1003023B1003023B100 /* ConfigSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings/ConfigSource.swift; sourceTree = ""; }; 3023B1013023B1013023B101 /* ConfigSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings/ConfigSettingsView.swift; sourceTree = ""; }; 312DE7503B4658DD173121B8 /* AuthManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = ""; }; @@ -629,6 +632,7 @@ AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/ShortcutBareStartRouting.swift; sourceTree = ""; }; E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/TerminalFindEscapeRouting.swift; sourceTree = ""; }; C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/MenuBarExtraController.swift; sourceTree = ""; }; + 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelController.swift; sourceTree = ""; }; 2F0C07000000000000000001 /* CmuxMainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CmuxMainWindow.swift; sourceTree = ""; }; 2F0C06000000000000000001 /* MainWindowVisibilityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/MainWindowVisibilityController.swift; sourceTree = ""; }; A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/DebugLogging.swift; sourceTree = ""; }; @@ -1139,6 +1143,7 @@ AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */, E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */, + 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */, 2F0C07000000000000000001 /* CmuxMainWindow.swift */, 2F0C06000000000000000001 /* MainWindowVisibilityController.swift */, A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */, @@ -1440,6 +1445,7 @@ C0DE31390000000000000104 /* FilePreviewReviewFeedbackTests.swift */, D7AB00000000000000000010 /* AppDelegateMoveTabToNewWorkspaceTests.swift */, 17C9F5BA0DD14EDC8C3E5002 /* MainWindowVisibilityControllerTests.swift */, + 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */, A11EAA000000000000000001 /* AppearanceSettingsTests.swift */, 6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */, C3408A000000000000000004 /* RightSidebarCommandPaletteTests.swift */, @@ -1775,6 +1781,7 @@ 76027A12C93B4538BF22C71E /* ShortcutBareStartRouting.swift in Sources */, E5C0F1A0E5C0F1A0E5C0F1A0 /* TerminalFindEscapeRouting.swift in Sources */, 1A8BEE693C9E4C3190CB7F20 /* MenuBarExtraController.swift in Sources */, + 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */, 2F0C07000000000000000002 /* CmuxMainWindow.swift in Sources */, 2F0C06000000000000000002 /* MainWindowVisibilityController.swift in Sources */, A500D010A1B2C3D4E5F60718 /* DebugLogging.swift in Sources */, @@ -2118,6 +2125,7 @@ C0DE31390000000000000103 /* FilePreviewReviewFeedbackTests.swift in Sources */, D7AB0000000000000000000F /* AppDelegateMoveTabToNewWorkspaceTests.swift in Sources */, 17C9F5BA0DD14EDC8C3E5001 /* MainWindowVisibilityControllerTests.swift in Sources */, + 2758A0032758A0032758A003 /* GlobalHotkeyPanelControllerTests.swift in Sources */, A11EAA000000000000000000 /* AppearanceSettingsTests.swift in Sources */, 1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */, C3408A000000000000000003 /* RightSidebarCommandPaletteTests.swift in Sources */, diff --git a/README.md b/README.md index a583b7f11c07..ee7479176318 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ Command palette navigation shortcuts, including ⌃ P, are also customizable and | Shortcut | Action | |----------|--------| +| ⌃ ⌥ ⌘ . | Show/hide hotkey overlay | | ⌘ ⇧ N | New window | | ⌘ ⇧ O | Reopen previous session | | ⌘ , | Settings | diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index c9fc552c0c53..6ba20095fd14 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -69494,6 +69494,29 @@ } } }, + "globalHotkey.window.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux Hotkey Window" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux ホットキーウィンドウ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux 핫키 윈도우" + } + } + } + }, "settings.globalHotkey.enable": { "extractionState": "manual", "localizations": { @@ -69523,19 +69546,19 @@ "en": { "stringUnit": { "state": "translated", - "value": "Turn this on to show or hide all cmux windows from any app." + "value": "Turn this on to show or hide a cmux overlay from any app, including full-screen spaces." } }, "ja": { "stringUnit": { "state": "translated", - "value": "どのアプリからでも cmux のすべてのウィンドウを表示または非表示にするにはオンにします。" + "value": "フルスクリーンスペースを含むどのアプリからでも cmux オーバーレイを表示または非表示にするにはオンにします。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 옵션을 켜면 어떤 앱에서든 모든 cmux 윈도우를 표시하거나 숨길 수 있습니다." + "value": "이 옵션을 켜면 전체 화면 Spaces를 포함해 어떤 앱에서든 cmux 오버레이를 표시하거나 숨길 수 있습니다." } } } @@ -69546,19 +69569,19 @@ "en": { "stringUnit": { "state": "translated", - "value": "Press the shortcut from any app to show or hide all cmux windows." + "value": "Press the shortcut from any app to show or hide the cmux hotkey window." } }, "ja": { "stringUnit": { "state": "translated", - "value": "どのアプリからでもこのショートカットで cmux のすべてのウィンドウを表示または非表示にできます。" + "value": "どのアプリからでもこのショートカットで cmux ホットキーウィンドウを表示または非表示にできます。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "어떤 앱에서든 단축키를 눌러 모든 cmux 윈도우를 표시하거나 숨깁니다." + "value": "어떤 앱에서든 단축키를 눌러 cmux 핫키 윈도우를 표시하거나 숨깁니다." } } } @@ -69569,19 +69592,19 @@ "en": { "stringUnit": { "state": "translated", - "value": "Use Command, Option, or Control with another key. No extra macOS permission is required." + "value": "Use Command, Option, or Control with another key. The overlay uses a native panel that can appear over full-screen apps." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Command、Option、または Control と別のキーを組み合わせてください。追加の macOS 権限は不要です。" + "value": "Command、Option、または Control と別のキーを組み合わせてください。このオーバーレイはフルスクリーンアプリの上に表示できるネイティブパネルを使用します。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Command, Option 또는 Control과 다른 키를 함께 사용하세요. 추가 macOS 권한이 필요하지 않습니다." + "value": "Command, Option 또는 Control과 다른 키를 함께 사용하세요. 이 오버레이는 전체 화면 앱 위에 표시될 수 있는 네이티브 패널을 사용합니다." } } } @@ -69592,19 +69615,19 @@ "en": { "stringUnit": { "state": "translated", - "value": "Show/Hide All Windows" + "value": "Show/Hide Hotkey Window" } }, "ja": { "stringUnit": { "state": "translated", - "value": "すべてのウィンドウを表示/非表示" + "value": "ホットキーウィンドウを表示/非表示" } }, "ko": { "stringUnit": { "state": "translated", - "value": "모든 윈도우 표시/숨기기" + "value": "핫키 윈도우 표시/숨기기" } } } @@ -70993,13 +71016,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window" } }, "ja": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward システム ショートカット グローバル ホットキー キーボード 表示 非表示 前面" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window システム ショートカット グローバル ホットキー キーボード 表示 非表示 オーバーレイ フルスクリーン" } }, "ar": { @@ -79102,13 +79125,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen" } }, "ja": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows グローバルホットキー 有効 システム全体 表示 非表示 全ウィンドウ" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen グローバルホットキー 有効 システム全体 表示 非表示 オーバーレイ フルスクリーン" } }, "ar": { @@ -79156,7 +79179,7 @@ "ko": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows 키보드 단축키 핫키 명령 키 기록기 외관 테마 색상 색 구성표 라이트 다크 시스템 모드 비밀번호 비밀 토큰 자격 증명 접근 허용 비활성화 활성화 권한 안전하지 않음 전역 글로벌 모든 윈도우 표시/숨기기" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen 키보드 단축키 핫키 명령 키 기록기 외관 테마 색상 색 구성표 라이트 다크 시스템 모드 비밀번호 비밀 토큰 자격 증명 접근 허용 비활성화 활성화 권한 안전하지 않음 전역 글로벌 오버레이 전체 화면" } }, "nb": { @@ -79221,13 +79244,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control" + "value": "global hotkey overlay shortcut recorder key command option control" } }, "ja": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control グローバルホットキー ショートカット レコーダー command option control" + "value": "global hotkey overlay shortcut recorder key command option control グローバルホットキー オーバーレイ ショートカット レコーダー command option control" } }, "ar": { @@ -79275,7 +79298,7 @@ "ko": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control 자동화 api cli 제어 소켓 에이전트 훅 포트 환경 키보드 단축키 핫키 명령 키 기록기 전역 글로벌" + "value": "global hotkey overlay shortcut recorder key command option control 자동화 api cli 제어 소켓 에이전트 훅 포트 환경 키보드 단축키 핫키 명령 키 기록기 전역 글로벌 오버레이" } }, "nb": { diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift new file mode 100644 index 000000000000..75430007e091 --- /dev/null +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -0,0 +1,210 @@ +import AppKit +import SwiftUI + +enum GlobalHotkeyPanelLayout { + static func panelFrame(in screenFrame: NSRect) -> NSRect { + let margin = max(20, min(56, screenFrame.height * 0.045)) + let width = min(max(960, screenFrame.width * 0.88), screenFrame.width - (margin * 2)) + let height = min(max(560, screenFrame.height * 0.78), screenFrame.height - (margin * 2)) + let origin = NSPoint( + x: screenFrame.midX - (width / 2), + y: screenFrame.maxY - height - margin + ) + return NSRect(origin: origin, size: NSSize(width: width, height: height)).integral + } + + static func preferredScreen(for point: NSPoint = NSEvent.mouseLocation) -> NSScreen? { + NSScreen.screens.first { $0.frame.contains(point) } ?? NSScreen.main ?? NSScreen.screens.first + } +} + +enum GlobalHotkeyPanelConfiguration { + static let styleMask: NSWindow.StyleMask = [ + .nonactivatingPanel, + .titled, + .resizable, + .fullSizeContentView, + ] + + static let collectionBehavior: NSWindow.CollectionBehavior = [ + .canJoinAllSpaces, + .fullScreenAuxiliary, + .transient, + .ignoresCycle, + ] + + static var windowLevel: NSWindow.Level { + NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.mainMenuWindow)) - 2) + } + + static func apply(to panel: NSPanel) { + panel.level = windowLevel + panel.collectionBehavior = collectionBehavior + panel.isFloatingPanel = true + panel.becomesKeyOnlyIfNeeded = true + panel.hidesOnDeactivate = false + panel.isExcludedFromWindowsMenu = true + panel.animationBehavior = .utilityWindow + panel.titleVisibility = .hidden + panel.titlebarAppearsTransparent = true + panel.isMovableByWindowBackground = false + panel.isMovable = true + panel.isRestorable = false + panel.isReleasedWhenClosed = false + } +} + +@MainActor +final class GlobalHotkeyPanel: NSPanel { + var onCancel: (() -> Void)? + + override var canBecomeKey: Bool { true } + override var canBecomeMain: Bool { false } + + override func keyDown(with event: NSEvent) { + if ShortcutStroke.isEscapeCancelEvent(event) { + onCancel?() + return + } + super.keyDown(with: event) + } + + override func cancelOperation(_ sender: Any?) { + onCancel?() + } +} + +@MainActor +final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { + private weak var appDelegate: AppDelegate? + private var windowController: NSWindowController? + private var windowId: UUID? + private var tabManager: TabManager? + private var sidebarState: SidebarState? + private var sidebarSelectionState: SidebarSelectionState? + private var fileExplorerState: FileExplorerState? + private var cmuxConfigStore: CmuxConfigStore? + + init(appDelegate: AppDelegate) { + self.appDelegate = appDelegate + super.init() + } + + var isVisible: Bool { + panel?.isVisible == true + } + + func toggle() { + if isVisible { + hide() + } else { + show() + } + } + + func show() { + guard let panel = ensurePanel() else { + NSSound.beep() + return + } + + position(panel) + panel.orderFrontRegardless() + panel.makeKey() + appDelegate?.setActiveMainWindow(panel) + +#if DEBUG + cmuxDebugLog("globalHotkey.panel.show window=\(panel.identifier?.rawValue ?? "")") +#endif + } + + func hide() { + guard let panel else { return } + panel.orderOut(nil) +#if DEBUG + cmuxDebugLog("globalHotkey.panel.hide window=\(panel.identifier?.rawValue ?? "")") +#endif + } + + func windowShouldClose(_ sender: NSWindow) -> Bool { + hide() + return false + } + + private var panel: GlobalHotkeyPanel? { + windowController?.window as? GlobalHotkeyPanel + } + + private func ensurePanel() -> GlobalHotkeyPanel? { + if let panel { + return panel + } + + guard let appDelegate else { return nil } + + let windowId = UUID() + let tabManager = TabManager(autoWelcomeIfNeeded: true) + let notificationStore = TerminalNotificationStore.shared + let sidebarState = SidebarState() + let sidebarSelectionState = SidebarSelectionState() + let fileExplorerState = FileExplorerState() + let cmuxConfigStore = CmuxConfigStore() + cmuxConfigStore.wireDirectoryTracking(tabManager: tabManager) + cmuxConfigStore.loadAll() + + let root = ContentView(updateViewModel: appDelegate.updateViewModel, windowId: windowId) + .environmentObject(tabManager) + .environmentObject(notificationStore) + .environmentObject(sidebarState) + .environmentObject(sidebarSelectionState) + .environmentObject(fileExplorerState) + .environmentObject(cmuxConfigStore) + + let initialFrame = GlobalHotkeyPanelLayout.preferredScreen() + .map { GlobalHotkeyPanelLayout.panelFrame(in: $0.frame) } + ?? NSRect(x: 120, y: 120, width: 1_000, height: 700) + let panel = GlobalHotkeyPanel( + contentRect: initialFrame, + styleMask: GlobalHotkeyPanelConfiguration.styleMask, + backing: .buffered, + defer: false + ) + panel.identifier = NSUserInterfaceItemIdentifier("cmux.main.\(windowId.uuidString)") + panel.title = String(localized: "globalHotkey.window.title", defaultValue: "cmux Hotkey Window") + panel.contentView = MainWindowHostingView(rootView: root) + panel.delegate = self + panel.onCancel = { [weak self] in + self?.hide() + } + GlobalHotkeyPanelConfiguration.apply(to: panel) + + appDelegate.attachUpdateAccessory(to: panel) + appDelegate.applyWindowDecorations(to: panel) + appDelegate.registerMainWindow( + panel, + windowId: windowId, + tabManager: tabManager, + sidebarState: sidebarState, + sidebarSelectionState: sidebarSelectionState, + fileExplorerState: fileExplorerState, + cmuxConfigStore: cmuxConfigStore, + role: .globalHotkeyPanel + ) + appDelegate.publishCmuxWindowLifecycle(name: "window.created", windowId: windowId, origin: "global_hotkey") + installFileDropOverlay(on: panel, tabManager: tabManager) + + self.windowId = windowId + self.tabManager = tabManager + self.sidebarState = sidebarState + self.sidebarSelectionState = sidebarSelectionState + self.fileExplorerState = fileExplorerState + self.cmuxConfigStore = cmuxConfigStore + windowController = NSWindowController(window: panel) + return panel + } + + private func position(_ panel: NSPanel) { + guard let screen = GlobalHotkeyPanelLayout.preferredScreen() else { return } + panel.setFrame(GlobalHotkeyPanelLayout.panelFrame(in: screen.frame), display: false) + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index a36f373a905c..0befbb81ee41 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -536,12 +536,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent Self.detectRunningUnderXCTest(env) } + @MainActor + enum MainWindowContextRole { + case standard + case globalHotkeyPanel + + var isSessionRestorable: Bool { + switch self { + case .standard: + return true + case .globalHotkeyPanel: + return false + } + } + } + @MainActor final class MainWindowContext { let windowId: UUID let tabManager: TabManager let sidebarState: SidebarState let sidebarSelectionState: SidebarSelectionState + let role: MainWindowContextRole var fileExplorerState: FileExplorerState? let keyboardFocusCoordinator: MainWindowFocusController var cmuxConfigStore: CmuxConfigStore? @@ -552,6 +568,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent tabManager: TabManager, sidebarState: SidebarState, sidebarSelectionState: SidebarSelectionState, + role: MainWindowContextRole = .standard, fileExplorerState: FileExplorerState?, cmuxConfigStore: CmuxConfigStore?, window: NSWindow? @@ -560,6 +577,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent self.tabManager = tabManager self.sidebarState = sidebarState self.sidebarSelectionState = sidebarSelectionState + self.role = role self.fileExplorerState = fileExplorerState self.cmuxConfigStore = cmuxConfigStore self.window = window @@ -689,6 +707,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private var shortcutDefaultsObserver: NSObjectProtocol? private var menuBarVisibilityObserver: NSObjectProtocol? private var splitButtonTooltipRefreshScheduled = false + private lazy var globalHotkeyPanelController = GlobalHotkeyPanelController(appDelegate: self) private struct PendingConfiguredShortcutChord { let firstStroke: ShortcutStroke let windowNumber: Int? @@ -3193,7 +3212,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent guard !includeScrollback else { return nil } var hasher = Hasher() - let contexts = mainWindowContexts.values.sorted { lhs, rhs in + let contexts = mainWindowContexts.values.filter { $0.role.isSessionRestorable }.sorted { lhs, rhs in lhs.windowId.uuidString < rhs.windowId.uuidString } hasher.combine(contexts.count) @@ -3518,7 +3537,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func sortedMainWindowContextsForSessionSnapshot() -> [MainWindowContext] { - mainWindowContexts.values.sorted { lhs, rhs in + mainWindowContexts.values.filter { $0.role.isSessionRestorable }.sorted { lhs, rhs in let lhsWindow = lhs.window ?? windowForMainWindowId(lhs.windowId) let rhsWindow = rhs.window ?? windowForMainWindowId(rhs.windowId) let lhsIsKey = lhsWindow?.isKeyWindow ?? false @@ -3627,7 +3646,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent sidebarState: SidebarState, sidebarSelectionState: SidebarSelectionState, fileExplorerState: FileExplorerState? = nil, - cmuxConfigStore: CmuxConfigStore? = nil + cmuxConfigStore: CmuxConfigStore? = nil, + role: MainWindowContextRole = .standard ) { let key = ObjectIdentifier(window) forgetRecoverableMainWindowRoute(windowId: windowId) @@ -3691,6 +3711,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent tabManager: tabManager, sidebarState: sidebarState, sidebarSelectionState: sidebarSelectionState, + role: role, fileExplorerState: fileExplorerState, cmuxConfigStore: cmuxConfigStore, window: window @@ -5003,7 +5024,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent for key in removedKeys { mainWindowContexts.removeValue(forKey: key) } - rememberRecoverableMainWindowRoute(windowId: removed.windowId, tabManager: removed.tabManager, window: removed.window) + if removed.role.isSessionRestorable { + rememberRecoverableMainWindowRoute(windowId: removed.windowId, tabManager: removed.tabManager, window: removed.window) + } notifyMainWindowContextsDidChange() return removed } @@ -5015,7 +5038,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent for key in contextKeys { mainWindowContexts.removeValue(forKey: key) } - rememberRecoverableMainWindowRoute(windowId: context.windowId, tabManager: context.tabManager, window: context.window) + if context.role.isSessionRestorable { + rememberRecoverableMainWindowRoute(windowId: context.windowId, tabManager: context.tabManager, window: context.window) + } notifyMainWindowContextsDidChange() commandPaletteVisibilityByWindowId.removeValue(forKey: context.windowId) @@ -7361,10 +7386,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } func toggleApplicationVisibilityFromGlobalHotkey() { - mainWindowVisibilityController.toggleApplicationVisibility( - windows: mainWindowsForVisibilityController(), - reason: .globalHotkey - ) + globalHotkeyPanelController.toggle() } @discardableResult diff --git a/Sources/KeyboardShortcutSettings.swift b/Sources/KeyboardShortcutSettings.swift index f34776d4fe18..666c1356b3e4 100644 --- a/Sources/KeyboardShortcutSettings.swift +++ b/Sources/KeyboardShortcutSettings.swift @@ -143,7 +143,7 @@ enum KeyboardShortcutSettings { switch self { case .openSettings: return String(localized: "menu.app.settings", defaultValue: "Settings…") case .reloadConfiguration: return String(localized: "menu.app.reloadConfiguration", defaultValue: "Reload Configuration") - case .showHideAllWindows: return String(localized: "settings.globalHotkey.shortcut", defaultValue: "Show/Hide All Windows") + case .showHideAllWindows: return String(localized: "settings.globalHotkey.shortcut", defaultValue: "Show/Hide Hotkey Window") case .newWindow: return String(localized: "shortcut.newWindow.label", defaultValue: "New Window") case .closeWindow: return String(localized: "shortcut.closeWindow.label", defaultValue: "Close Window") case .toggleFullScreen: return String(localized: "command.toggleFullScreen.title", defaultValue: "Toggle Full Screen") diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index 6d2c4943b768..c892d73705b4 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -352,8 +352,8 @@ enum SettingsSearchIndex { setting(.browserImport, "import-hint", String(localized: "settings.browser.import.hint.show", defaultValue: "Show import hint on blank browser tabs"), "blank tab browser import"), setting(.browser, "react-grab", String(localized: "settings.browser.reactGrabVersion", defaultValue: "React Grab Version"), "npm react grab toolbar"), setting(.browser, "history", String(localized: "settings.browser.history", defaultValue: "Browsing History"), "clear visited suggestions"), - setting(.globalHotkey, "enable-hotkey", String(localized: "settings.globalHotkey.enable", defaultValue: "Enable System-Wide Hotkey"), "global shortcut show hide windows"), - setting(.globalHotkey, "shortcut", String(localized: "settings.section.globalHotkey", defaultValue: "Global Hotkey"), "keyboard recorder command option control"), + setting(.globalHotkey, "enable-hotkey", String(localized: "settings.globalHotkey.enable", defaultValue: "Enable System-Wide Hotkey"), "global shortcut overlay hotkey window full screen"), + setting(.globalHotkey, "shortcut", String(localized: "settings.section.globalHotkey", defaultValue: "Global Hotkey"), "keyboard recorder command option control overlay"), setting(.keyboardShortcuts, "shortcut-chords", String(localized: "settings.shortcuts.chords", defaultValue: "Shortcut Chords"), "tmux multi step keybindings"), setting(.keyboardShortcuts, "shortcuts", String(localized: "settings.section.keyboardShortcuts", defaultValue: "Keyboard Shortcuts"), "keybindings commands"), setting(.workspaceColors, "indicator", String(localized: "settings.workspaceColors.indicator", defaultValue: "Workspace Color Indicator"), "tab color indicator"), diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index 35a435cabe44..089305c98b0e 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -18,7 +18,7 @@ enum SettingsSearchAliasIndex { case .browserImport: return localized("settings.search.alias.section.browserImport", defaultValue: "chrome safari firefox brave edge arc bookmarks history cookies profiles") case .globalHotkey: - return localized("settings.search.alias.section.globalHotkey", defaultValue: "system shortcut global keyboard show hide bring forward") + return localized("settings.search.alias.section.globalHotkey", defaultValue: "system shortcut global keyboard show hide overlay full screen hotkey window") case .keyboardShortcuts: return localized("settings.search.alias.section.keyboardShortcuts", defaultValue: "keybinds key bindings hotkeys chords accelerators commands") case .workspaceColors: @@ -101,8 +101,8 @@ enum SettingsSearchAliasIndex { "browserImport:import-hint": localized("settings.search.alias.setting.browserImport.import-hint", defaultValue: "browser.showImportHintOnBlankTabs blank tab onboarding hint import prompt dismiss"), "browser:react-grab": localized("settings.search.alias.setting.browser.react-grab", defaultValue: "browser.reactGrabVersion react grab npm version toolbar cmd-shift-g inspect component"), "browser:history": localized("settings.search.alias.setting.browser.history", defaultValue: "clear browser history visited pages suggestions omnibar"), - "globalHotkey:enable-hotkey": localized("settings.search.alias.setting.globalHotkey.enable-hotkey", defaultValue: "global hotkey enable system wide show hide all windows"), - "globalHotkey:shortcut": localized("settings.search.alias.setting.globalHotkey.shortcut", defaultValue: "global hotkey shortcut recorder key command option control"), + "globalHotkey:enable-hotkey": localized("settings.search.alias.setting.globalHotkey.enable-hotkey", defaultValue: "global hotkey enable system wide show hide overlay hotkey window full screen"), + "globalHotkey:shortcut": localized("settings.search.alias.setting.globalHotkey.shortcut", defaultValue: "global hotkey overlay shortcut recorder key command option control"), "keyboardShortcuts:shortcut-chords": localized("settings.search.alias.setting.keyboardShortcuts.shortcut-chords", defaultValue: "tmux prefix ctrl-b control-b multi key sequence chord cmux json"), "keyboardShortcuts:shortcuts": localized("settings.search.alias.setting.keyboardShortcuts.shortcuts", defaultValue: "hotkeys keybindings key bindings commands keyboard accelerators shortcuts cmux json"), "workspaceColors:indicator": localized("settings.search.alias.setting.workspaceColors.indicator", defaultValue: "workspaceColors.indicatorStyle tab indicator active workspace style color stripe dot"), diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index b7b8e94170cd..f57777d0385c 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -7932,12 +7932,12 @@ private struct GlobalHotkeySection: View { if isEnabled { return String( localized: "settings.globalHotkey.enable.subtitleOn", - defaultValue: "Press the shortcut from any app to show or hide all cmux windows." + defaultValue: "Press the shortcut from any app to show or hide the cmux hotkey window." ) } return String( localized: "settings.globalHotkey.enable.subtitleOff", - defaultValue: "Turn this on to show or hide all cmux windows from any app." + defaultValue: "Turn this on to show or hide a cmux overlay from any app, including full-screen spaces." ) } @@ -7982,7 +7982,7 @@ private struct GlobalHotkeySection: View { SettingsCardNote( String( localized: "settings.globalHotkey.note", - defaultValue: "Use Command, Option, or Control with another key. No extra macOS permission is required." + defaultValue: "Use Command, Option, or Control with another key. The overlay uses a native panel that can appear over full-screen apps." ) ) .accessibilityIdentifier("SettingsGlobalHotkeyNote") diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift new file mode 100644 index 000000000000..7372fa7fb56b --- /dev/null +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -0,0 +1,58 @@ +import XCTest +import AppKit + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +final class GlobalHotkeyPanelControllerTests: XCTestCase { + func testPanelConfigurationUsesFullscreenAuxiliaryNonActivatingPanel() { + let panel = GlobalHotkeyPanel( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: GlobalHotkeyPanelConfiguration.styleMask, + backing: .buffered, + defer: false + ) + defer { + panel.orderOut(nil) + panel.close() + } + + GlobalHotkeyPanelConfiguration.apply(to: panel) + + XCTAssertTrue(panel.styleMask.contains(.nonactivatingPanel)) + XCTAssertTrue(panel.collectionBehavior.contains(.canJoinAllSpaces)) + XCTAssertTrue(panel.collectionBehavior.contains(.fullScreenAuxiliary)) + XCTAssertTrue(panel.collectionBehavior.contains(.transient)) + XCTAssertTrue(panel.collectionBehavior.contains(.ignoresCycle)) + XCTAssertTrue(panel.isFloatingPanel) + XCTAssertTrue(panel.becomesKeyOnlyIfNeeded) + XCTAssertFalse(panel.hidesOnDeactivate) + XCTAssertTrue(panel.isExcludedFromWindowsMenu) + XCTAssertEqual(panel.level, GlobalHotkeyPanelConfiguration.windowLevel) + XCTAssertTrue(panel.canBecomeKey) + XCTAssertFalse(panel.canBecomeMain) + } + + func testPanelLayoutCreatesTopOverlayInsideScreenBounds() { + let screenFrame = NSRect(x: 0, y: 0, width: 1_512, height: 982) + let frame = GlobalHotkeyPanelLayout.panelFrame(in: screenFrame) + + XCTAssertGreaterThanOrEqual(frame.minX, screenFrame.minX) + XCTAssertGreaterThanOrEqual(frame.minY, screenFrame.minY) + XCTAssertLessThanOrEqual(frame.maxX, screenFrame.maxX) + XCTAssertLessThanOrEqual(frame.maxY, screenFrame.maxY) + XCTAssertGreaterThan(frame.width, screenFrame.width * 0.85) + XCTAssertGreaterThan(frame.height, screenFrame.height * 0.75) + XCTAssertEqual(frame.midX, screenFrame.midX, accuracy: 1) + XCTAssertEqual(frame.maxY, screenFrame.maxY - max(20, min(56, screenFrame.height * 0.045)), accuracy: 1) + } + + func testHotkeyPanelContextRoleIsTransient() { + XCTAssertTrue(AppDelegate.MainWindowContextRole.standard.isSessionRestorable) + XCTAssertFalse(AppDelegate.MainWindowContextRole.globalHotkeyPanel.isSessionRestorable) + } +} diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 7b915dc20916..2acfd47fbd83 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -28,7 +28,7 @@ export const shortcutCategories: ShortcutCategory[] = [ { id: "showHideAllWindows", combos: [["⌃", "⌥", "⌘", "."]], - description: { en: "Show/hide all cmux windows", ja: "すべてのcmuxウインドウを表示/非表示" }, + description: { en: "Show/hide the cmux hotkey window", ja: "cmuxホットキーウインドウを表示/非表示" }, note: { en: "system-wide hotkey", ja: "システム全体のホットキー" }, }, { id: "commandPalette", combos: [["⌘", "⇧", "P"]], description: { en: "Command palette", ja: "コマンドパレット" } }, From 47e70c1331869812c25f5c21e2d1fc2d978b333b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 12:05:59 -0700 Subject: [PATCH 02/17] fix: register hotkey panel as auxiliary window --- Sources/App/GlobalHotkeyPanelController.swift | 2 +- Sources/AppDelegate.swift | 3 +++ Sources/cmuxApp.swift | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index 75430007e091..043a686f62c1 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -169,7 +169,7 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { backing: .buffered, defer: false ) - panel.identifier = NSUserInterfaceItemIdentifier("cmux.main.\(windowId.uuidString)") + panel.identifier = NSUserInterfaceItemIdentifier("cmux.hotkeyPanel") panel.title = String(localized: "globalHotkey.window.title", defaultValue: "cmux Hotkey Window") panel.contentView = MainWindowHostingView(rootView: root) panel.delegate = self diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 0befbb81ee41..4be56ad3e702 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4950,6 +4950,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } func mainWindowId(from window: NSWindow) -> UUID? { + if let context = mainWindowContexts[ObjectIdentifier(window)] { + return context.windowId + } guard let raw = window.identifier?.rawValue else { return nil } let prefix = "cmux.main." guard raw.hasPrefix(prefix) else { return nil } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 8809f8a6488f..314c929b2a0a 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -1156,6 +1156,7 @@ private let cmuxAuxiliaryWindowIdentifiers: Set = [ "cmux.feedTextEditorDebug", "cmux.fileExplorerStyleDebug", "cmux.folderDragIcon", + "cmux.hotkeyPanel", "cmux.pdfPreviewChromeDebug", "cmux.splitButtonLayoutDebug", "cmux.tabBarBackdropLab", From 2c6d848570102d169ea106bd90d4a64d7f7b0c68 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 12:15:16 -0700 Subject: [PATCH 03/17] fix: isolate hotkey panel helpers on main actor --- Sources/App/GlobalHotkeyPanelController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index 043a686f62c1..2280c4863da0 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -1,6 +1,7 @@ import AppKit import SwiftUI +@MainActor enum GlobalHotkeyPanelLayout { static func panelFrame(in screenFrame: NSRect) -> NSRect { let margin = max(20, min(56, screenFrame.height * 0.045)) @@ -18,6 +19,7 @@ enum GlobalHotkeyPanelLayout { } } +@MainActor enum GlobalHotkeyPanelConfiguration { static let styleMask: NSWindow.StyleMask = [ .nonactivatingPanel, @@ -63,7 +65,7 @@ final class GlobalHotkeyPanel: NSPanel { override func keyDown(with event: NSEvent) { if ShortcutStroke.isEscapeCancelEvent(event) { - onCancel?() + cancelOperation(nil) return } super.keyDown(with: event) From 386fd00711a31f5b19353ef645f1c8a13d7fdde7 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:08:57 -0700 Subject: [PATCH 04/17] fix: preserve hotkey panel window identity --- Sources/App/GlobalHotkeyPanelController.swift | 9 +++++- Sources/AppDelegate.swift | 13 +++++--- Sources/ContentView.swift | 30 +++++++++++++++++-- .../GlobalHotkeyPanelControllerTests.swift | 2 ++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index 2280c4863da0..e558686fa93d 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -21,6 +21,8 @@ enum GlobalHotkeyPanelLayout { @MainActor enum GlobalHotkeyPanelConfiguration { + static let windowIdentifier = "cmux.hotkeyPanel" + static let styleMask: NSWindow.StyleMask = [ .nonactivatingPanel, .titled, @@ -60,6 +62,7 @@ enum GlobalHotkeyPanelConfiguration { final class GlobalHotkeyPanel: NSPanel { var onCancel: (() -> Void)? + override var acceptsFirstResponder: Bool { true } override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { false } @@ -110,6 +113,9 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { return } + if NSApp.isHidden { + NSApp.unhide(nil) + } position(panel) panel.orderFrontRegardless() panel.makeKey() @@ -155,6 +161,7 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { cmuxConfigStore.loadAll() let root = ContentView(updateViewModel: appDelegate.updateViewModel, windowId: windowId) + .mainWindowContextRole(.globalHotkeyPanel) .environmentObject(tabManager) .environmentObject(notificationStore) .environmentObject(sidebarState) @@ -171,7 +178,7 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { backing: .buffered, defer: false ) - panel.identifier = NSUserInterfaceItemIdentifier("cmux.hotkeyPanel") + panel.identifier = NSUserInterfaceItemIdentifier(GlobalHotkeyPanelConfiguration.windowIdentifier) panel.title = String(localized: "globalHotkey.window.title", defaultValue: "cmux Hotkey Window") panel.contentView = MainWindowHostingView(rootView: root) panel.delegate = self diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 1afa562f5ed7..0bd354877d9c 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -576,7 +576,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent Self.detectRunningUnderXCTest(env) } - @MainActor enum MainWindowContextRole { case standard case globalHotkeyPanel @@ -6515,6 +6514,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private func hasVisibleMainTerminalWindow() -> Bool { mainWindowContexts.values.contains { context in + guard context.role.isSessionRestorable else { return false } guard let window = resolvedWindow(for: context) else { return false } return window.isVisible && !window.isMiniaturized && window.alphaValue > 0.001 } @@ -7907,11 +7907,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private func preferredMainWindowForVisibilityActivation() -> NSWindow? { if let keyWindow = NSApp.keyWindow, - isMainTerminalWindow(keyWindow) { + isSessionRestorableMainTerminalWindow(keyWindow) { return keyWindow } if let mainWindow = NSApp.mainWindow, - isMainTerminalWindow(mainWindow) { + isSessionRestorableMainTerminalWindow(mainWindow) { return mainWindow } if let visibleContext = sortedMainWindowContextsForSessionSnapshot().first(where: { context in @@ -7959,7 +7959,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent windows.append(window) } } - for window in NSApp.windows where isMainTerminalWindow(window) { + for window in NSApp.windows where isSessionRestorableMainTerminalWindow(window) { if !windows.contains(where: { $0 === window }) { windows.append(window) } @@ -14648,6 +14648,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return raw == "cmux.main" || raw.hasPrefix("cmux.main.") } + private func isSessionRestorableMainTerminalWindow(_ window: NSWindow) -> Bool { + guard isMainTerminalWindow(window) else { return false } + return contextForMainTerminalWindow(window)?.role.isSessionRestorable ?? true + } + private func workspaceForMainActor(tabId: UUID) -> Workspace? { tabManagerFor(tabId: tabId)?.tabs.first(where: { $0.id == tabId }) } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index bd6e401debff..7a3fb012cbf5 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1047,6 +1047,23 @@ func titlebarShortcutHintShouldShow( !shortcut.isUnbound && (alwaysShowShortcutHints || (shortcut.command && modifierPressed)) } +private struct MainWindowContextRoleEnvironmentKey: EnvironmentKey { + static let defaultValue: AppDelegate.MainWindowContextRole = .standard +} + +extension EnvironmentValues { + var mainWindowContextRole: AppDelegate.MainWindowContextRole { + get { self[MainWindowContextRoleEnvironmentKey.self] } + set { self[MainWindowContextRoleEnvironmentKey.self] = newValue } + } +} + +extension View { + func mainWindowContextRole(_ role: AppDelegate.MainWindowContextRole) -> some View { + environment(\.mainWindowContextRole, role) + } +} + struct ContentView: View { @ObservedObject var updateViewModel: UpdateViewModel let windowId: UUID @@ -1056,6 +1073,7 @@ struct ContentView: View { @EnvironmentObject var sidebarSelectionState: SidebarSelectionState @EnvironmentObject var cmuxConfigStore: CmuxConfigStore @EnvironmentObject var fileExplorerState: FileExplorerState + @Environment(\.mainWindowContextRole) private var mainWindowContextRole @Environment(\.colorScheme) private var colorScheme @AppStorage("titlebarControlsStyle") private var titlebarControlsStyleRawValue = TitlebarControlsStyle.classic.rawValue @State private var sidebarWidth: CGFloat = 200 @@ -2232,7 +2250,14 @@ struct ContentView: View { @AppStorage("bgGlassTintOpacity") private var bgGlassTintOpacity = 0.03 @AppStorage("bgGlassEnabled") private var bgGlassEnabled = false @State private var titlebarLeadingInset: CGFloat = 12 - private var windowIdentifier: String { "cmux.main.\(windowId.uuidString)" } + private var windowIdentifier: String { + switch mainWindowContextRole { + case .standard: + return "cmux.main.\(windowId.uuidString)" + case .globalHotkeyPanel: + return GlobalHotkeyPanelConfiguration.windowIdentifier + } + } private var windowAppearanceSnapshot: WindowAppearanceSnapshot { _ = titlebarThemeGeneration return WindowAppearanceSnapshot.current( @@ -3259,7 +3284,8 @@ struct ContentView: View { sidebarState: sidebarState, sidebarSelectionState: sidebarSelectionState, fileExplorerState: fileExplorerState, - cmuxConfigStore: cmuxConfigStore + cmuxConfigStore: cmuxConfigStore, + role: mainWindowContextRole ) installFileDropOverlayWhenReady(on: window, tabManager: tabManager) })) diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift index 7372fa7fb56b..80b08ddfadc1 100644 --- a/cmuxTests/GlobalHotkeyPanelControllerTests.swift +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -33,6 +33,8 @@ final class GlobalHotkeyPanelControllerTests: XCTestCase { XCTAssertFalse(panel.hidesOnDeactivate) XCTAssertTrue(panel.isExcludedFromWindowsMenu) XCTAssertEqual(panel.level, GlobalHotkeyPanelConfiguration.windowLevel) + XCTAssertEqual(GlobalHotkeyPanelConfiguration.windowIdentifier, "cmux.hotkeyPanel") + XCTAssertTrue(panel.acceptsFirstResponder) XCTAssertTrue(panel.canBecomeKey) XCTAssertFalse(panel.canBecomeMain) } From 727e36d583d87e685ca52ee731dadfb4d6041e1b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:12:34 -0700 Subject: [PATCH 05/17] fix: address hotkey panel review feedback --- Sources/App/GlobalHotkeyPanelController.swift | 8 -------- Sources/AppDelegate.swift | 17 ++++++++++------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index e558686fa93d..60ebf439ece4 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -66,14 +66,6 @@ final class GlobalHotkeyPanel: NSPanel { override var canBecomeKey: Bool { true } override var canBecomeMain: Bool { false } - override func keyDown(with event: NSEvent) { - if ShortcutStroke.isEscapeCancelEvent(event) { - cancelOperation(nil) - return - } - super.keyDown(with: event) - } - override func cancelOperation(_ sender: Any?) { onCancel?() } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 0bd354877d9c..39f4339492a3 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4168,13 +4168,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent setActiveMainWindow(window) } - let didApplyStartupSessionRestore = attemptStartupSessionRestoreIfNeeded(primaryWindow: window) - if Self.shouldSaveSessionSnapshotAfterMainWindowRegistration( - isTerminatingApp: isTerminatingApp, - didApplyStartupSessionRestore: didApplyStartupSessionRestore, - isApplyingSessionRestore: isApplyingSessionRestore - ) { - saveSessionSnapshotAfterLoadingProcessDetectedIndexes(includeScrollback: false) + let registeredContextRole = mainWindowContexts[key]?.role ?? role + if registeredContextRole.isSessionRestorable { + let didApplyStartupSessionRestore = attemptStartupSessionRestoreIfNeeded(primaryWindow: window) + if Self.shouldSaveSessionSnapshotAfterMainWindowRegistration( + isTerminatingApp: isTerminatingApp, + didApplyStartupSessionRestore: didApplyStartupSessionRestore, + isApplyingSessionRestore: isApplyingSessionRestore + ) { + saveSessionSnapshotAfterLoadingProcessDetectedIndexes(includeScrollback: false) + } } } From ea3c98a0341cc323b5ddb21ef5f125cebd4a0453 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:19:23 -0700 Subject: [PATCH 06/17] fix: localize hotkey window title --- Resources/Localizable.xcstrings | 102 ++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 98cbafd18857..a26af8944145 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -72617,6 +72617,108 @@ "state": "translated", "value": "cmux 핫키 윈도우" } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نافذة الاختصار في cmux" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux prozor prečice" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux-hotkeyvindue" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-Hotkey-Fenster" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ventana de tecla rápida de cmux" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fenêtre de raccourci cmux" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Finestra tasto rapido cmux" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បង្អួចគ្រាប់ចុចផ្លូវកាត់ cmux" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "cmux-hurtigtastvindu" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Okno skrótu cmux" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Janela de tecla de atalho do cmux" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Окно горячей клавиши cmux" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "หน้าต่างปุ่มลัด cmux" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux Kısayol Penceresi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Вікно гарячої клавіші cmux" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux 热键窗口" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux 熱鍵視窗" + } } } }, From 454d53c300046c4d77df89edfc5d0e9acd3d874c Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:24:17 -0700 Subject: [PATCH 07/17] fix: complete global hotkey localizations --- Resources/Localizable.xcstrings | 132 +++++++++++++++++- .../GlobalHotkeyPanelControllerTests.swift | 4 +- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index a26af8944145..7cfc3f274d98 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -72044,7 +72044,24 @@ "state": "translated", "value": "글로벌 단축키" } - } + }, + "ar": { "stringUnit": { "state": "translated", "value": "الاختصار العام" } }, + "bs": { "stringUnit": { "state": "translated", "value": "Globalna prečica" } }, + "da": { "stringUnit": { "state": "translated", "value": "Global genvejstast" } }, + "de": { "stringUnit": { "state": "translated", "value": "Globaler Hotkey" } }, + "es": { "stringUnit": { "state": "translated", "value": "Tecla rápida global" } }, + "fr": { "stringUnit": { "state": "translated", "value": "Raccourci global" } }, + "it": { "stringUnit": { "state": "translated", "value": "Tasto rapido globale" } }, + "km": { "stringUnit": { "state": "translated", "value": "គ្រាប់ចុចផ្លូវកាត់សកល" } }, + "nb": { "stringUnit": { "state": "translated", "value": "Global hurtigtast" } }, + "pl": { "stringUnit": { "state": "translated", "value": "Globalny skrót" } }, + "pt-BR": { "stringUnit": { "state": "translated", "value": "Tecla de atalho global" } }, + "ru": { "stringUnit": { "state": "translated", "value": "Глобальная горячая клавиша" } }, + "th": { "stringUnit": { "state": "translated", "value": "ปุ่มลัดส่วนกลาง" } }, + "tr": { "stringUnit": { "state": "translated", "value": "Genel kısayol" } }, + "uk": { "stringUnit": { "state": "translated", "value": "Глобальна гаряча клавіша" } }, + "zh-Hans": { "stringUnit": { "state": "translated", "value": "全局热键" } }, + "zh-Hant": { "stringUnit": { "state": "translated", "value": "全域熱鍵" } } } }, "settings.section.keyboardShortcuts": { @@ -72742,7 +72759,24 @@ "state": "translated", "value": "시스템 전체 단축키 활성화" } - } + }, + "ar": { "stringUnit": { "state": "translated", "value": "تفعيل الاختصار على مستوى النظام" } }, + "bs": { "stringUnit": { "state": "translated", "value": "Omogući sistemsku prečicu" } }, + "da": { "stringUnit": { "state": "translated", "value": "Slå systemdækkende genvejstast til" } }, + "de": { "stringUnit": { "state": "translated", "value": "Systemweiten Hotkey aktivieren" } }, + "es": { "stringUnit": { "state": "translated", "value": "Activar tecla rápida en todo el sistema" } }, + "fr": { "stringUnit": { "state": "translated", "value": "Activer le raccourci système" } }, + "it": { "stringUnit": { "state": "translated", "value": "Abilita tasto rapido a livello di sistema" } }, + "km": { "stringUnit": { "state": "translated", "value": "បើកគ្រាប់ចុចផ្លូវកាត់ទូទាំងប្រព័ន្ធ" } }, + "nb": { "stringUnit": { "state": "translated", "value": "Aktiver systemomfattende hurtigtast" } }, + "pl": { "stringUnit": { "state": "translated", "value": "Włącz skrót systemowy" } }, + "pt-BR": { "stringUnit": { "state": "translated", "value": "Ativar tecla de atalho em todo o sistema" } }, + "ru": { "stringUnit": { "state": "translated", "value": "Включить системную горячую клавишу" } }, + "th": { "stringUnit": { "state": "translated", "value": "เปิดใช้ปุ่มลัดทั้งระบบ" } }, + "tr": { "stringUnit": { "state": "translated", "value": "Sistem genelinde kısayolu etkinleştir" } }, + "uk": { "stringUnit": { "state": "translated", "value": "Увімкнути загальносистемну гарячу клавішу" } }, + "zh-Hans": { "stringUnit": { "state": "translated", "value": "启用系统级热键" } }, + "zh-Hant": { "stringUnit": { "state": "translated", "value": "啟用全系統熱鍵" } } } }, "settings.globalHotkey.enable.subtitleOff": { @@ -72765,7 +72799,24 @@ "state": "translated", "value": "이 옵션을 켜면 전체 화면 Spaces를 포함해 어떤 앱에서든 cmux 오버레이를 표시하거나 숨길 수 있습니다." } - } + }, + "ar": { "stringUnit": { "state": "translated", "value": "شغّل هذا الخيار لإظهار أو إخفاء تراكب cmux من أي تطبيق، بما في ذلك مساحات ملء الشاشة." } }, + "bs": { "stringUnit": { "state": "translated", "value": "Uključite ovo da prikažete ili sakrijete cmux preklop iz bilo koje aplikacije, uključujući prostore preko cijelog ekrana." } }, + "da": { "stringUnit": { "state": "translated", "value": "Slå dette til for at vise eller skjule en cmux-overlejring fra enhver app, også fuldskærmsområder." } }, + "de": { "stringUnit": { "state": "translated", "value": "Aktivieren, um ein cmux-Overlay aus jeder App ein- oder auszublenden, auch in Vollbild-Spaces." } }, + "es": { "stringUnit": { "state": "translated", "value": "Activa esto para mostrar u ocultar una superposición de cmux desde cualquier app, incluidos los espacios de pantalla completa." } }, + "fr": { "stringUnit": { "state": "translated", "value": "Activez cette option pour afficher ou masquer une superposition cmux depuis n'importe quelle app, y compris les espaces plein écran." } }, + "it": { "stringUnit": { "state": "translated", "value": "Attivalo per mostrare o nascondere un overlay cmux da qualsiasi app, inclusi gli spazi a schermo intero." } }, + "km": { "stringUnit": { "state": "translated", "value": "បើកជម្រើសនេះ ដើម្បីបង្ហាញ ឬលាក់ស្រទាប់ cmux ពីកម្មវិធីណាមួយ រួមទាំង Spaces ពេញអេក្រង់។" } }, + "nb": { "stringUnit": { "state": "translated", "value": "Slå dette på for å vise eller skjule et cmux-overlegg fra hvilken som helst app, også fullskjermsområder." } }, + "pl": { "stringUnit": { "state": "translated", "value": "Włącz, aby pokazywać lub ukrywać nakładkę cmux z dowolnej aplikacji, także w przestrzeniach pełnoekranowych." } }, + "pt-BR": { "stringUnit": { "state": "translated", "value": "Ative isto para mostrar ou ocultar uma sobreposição do cmux a partir de qualquer app, incluindo espaços em tela cheia." } }, + "ru": { "stringUnit": { "state": "translated", "value": "Включите, чтобы показывать или скрывать оверлей cmux из любого приложения, включая полноэкранные пространства." } }, + "th": { "stringUnit": { "state": "translated", "value": "เปิดตัวเลือกนี้เพื่อแสดงหรือซ่อนโอเวอร์เลย์ cmux จากแอปใดก็ได้ รวมถึง Spaces แบบเต็มหน้าจอ" } }, + "tr": { "stringUnit": { "state": "translated", "value": "Tam ekran alanlar dahil herhangi bir uygulamadan cmux katmanını göstermek veya gizlemek için bunu açın." } }, + "uk": { "stringUnit": { "state": "translated", "value": "Увімкніть, щоб показувати або приховувати накладку cmux з будь-якої програми, зокрема в повноекранних просторах." } }, + "zh-Hans": { "stringUnit": { "state": "translated", "value": "开启后可从任何 App 显示或隐藏 cmux 覆盖层,包括全屏空间。" } }, + "zh-Hant": { "stringUnit": { "state": "translated", "value": "開啟後可從任何 App 顯示或隱藏 cmux 覆蓋層,包括全螢幕空間。" } } } }, "settings.globalHotkey.enable.subtitleOn": { @@ -72788,7 +72839,24 @@ "state": "translated", "value": "어떤 앱에서든 단축키를 눌러 cmux 핫키 윈도우를 표시하거나 숨깁니다." } - } + }, + "ar": { "stringUnit": { "state": "translated", "value": "اضغط على الاختصار من أي تطبيق لإظهار أو إخفاء نافذة اختصار cmux." } }, + "bs": { "stringUnit": { "state": "translated", "value": "Pritisnite prečicu iz bilo koje aplikacije da prikažete ili sakrijete cmux prozor prečice." } }, + "da": { "stringUnit": { "state": "translated", "value": "Tryk på genvejen fra enhver app for at vise eller skjule cmux-hotkeyvinduet." } }, + "de": { "stringUnit": { "state": "translated", "value": "Drücken Sie den Kurzbefehl aus jeder App, um das cmux-Hotkey-Fenster ein- oder auszublenden." } }, + "es": { "stringUnit": { "state": "translated", "value": "Pulsa el atajo desde cualquier app para mostrar u ocultar la ventana de tecla rápida de cmux." } }, + "fr": { "stringUnit": { "state": "translated", "value": "Appuyez sur le raccourci depuis n'importe quelle app pour afficher ou masquer la fenêtre de raccourci cmux." } }, + "it": { "stringUnit": { "state": "translated", "value": "Premi la scorciatoia da qualsiasi app per mostrare o nascondere la finestra tasto rapido cmux." } }, + "km": { "stringUnit": { "state": "translated", "value": "ចុចគ្រាប់ចុចផ្លូវកាត់ពីកម្មវិធីណាមួយ ដើម្បីបង្ហាញ ឬលាក់បង្អួចគ្រាប់ចុចផ្លូវកាត់ cmux។" } }, + "nb": { "stringUnit": { "state": "translated", "value": "Trykk på snarveien fra hvilken som helst app for å vise eller skjule cmux-hurtigtastvinduet." } }, + "pl": { "stringUnit": { "state": "translated", "value": "Naciśnij skrót w dowolnej aplikacji, aby pokazać lub ukryć okno skrótu cmux." } }, + "pt-BR": { "stringUnit": { "state": "translated", "value": "Pressione o atalho em qualquer app para mostrar ou ocultar a janela de tecla de atalho do cmux." } }, + "ru": { "stringUnit": { "state": "translated", "value": "Нажмите сочетание в любом приложении, чтобы показать или скрыть окно горячей клавиши cmux." } }, + "th": { "stringUnit": { "state": "translated", "value": "กดปุ่มลัดจากแอปใดก็ได้เพื่อแสดงหรือซ่อนหน้าต่างปุ่มลัด cmux" } }, + "tr": { "stringUnit": { "state": "translated", "value": "cmux kısayol penceresini göstermek veya gizlemek için herhangi bir uygulamada kısayola basın." } }, + "uk": { "stringUnit": { "state": "translated", "value": "Натисніть комбінацію в будь-якій програмі, щоб показати або приховати вікно гарячої клавіші cmux." } }, + "zh-Hans": { "stringUnit": { "state": "translated", "value": "在任何 App 中按下快捷键即可显示或隐藏 cmux 热键窗口。" } }, + "zh-Hant": { "stringUnit": { "state": "translated", "value": "在任何 App 中按下快捷鍵即可顯示或隱藏 cmux 熱鍵視窗。" } } } }, "settings.globalHotkey.note": { @@ -72811,7 +72879,24 @@ "state": "translated", "value": "Command, Option 또는 Control과 다른 키를 함께 사용하세요. 이 오버레이는 전체 화면 앱 위에 표시될 수 있는 네이티브 패널을 사용합니다." } - } + }, + "ar": { "stringUnit": { "state": "translated", "value": "استخدم Command أو Option أو Control مع مفتاح آخر. يستخدم التراكب لوحة أصلية يمكن أن تظهر فوق تطبيقات ملء الشاشة." } }, + "bs": { "stringUnit": { "state": "translated", "value": "Koristite Command, Option ili Control s drugom tipkom. Preklop koristi izvorni panel koji se može pojaviti iznad aplikacija preko cijelog ekrana." } }, + "da": { "stringUnit": { "state": "translated", "value": "Brug Command, Option eller Control sammen med en anden tast. Overlejringen bruger et indbygget panel, der kan vises over fuldskærmsapps." } }, + "de": { "stringUnit": { "state": "translated", "value": "Verwenden Sie Command, Option oder Control mit einer weiteren Taste. Das Overlay nutzt ein natives Panel, das über Vollbild-Apps erscheinen kann." } }, + "es": { "stringUnit": { "state": "translated", "value": "Usa Command, Option o Control con otra tecla. La superposición usa un panel nativo que puede aparecer sobre apps de pantalla completa." } }, + "fr": { "stringUnit": { "state": "translated", "value": "Utilisez Command, Option ou Control avec une autre touche. La superposition utilise un panneau natif qui peut apparaître au-dessus des apps plein écran." } }, + "it": { "stringUnit": { "state": "translated", "value": "Usa Command, Option o Control con un altro tasto. L'overlay usa un pannello nativo che può apparire sopra le app a schermo intero." } }, + "km": { "stringUnit": { "state": "translated", "value": "ប្រើ Command, Option ឬ Control ជាមួយគ្រាប់ចុចមួយទៀត។ ស្រទាប់នេះប្រើផ្ទាំងដើម ដែលអាចបង្ហាញលើកម្មវិធីពេញអេក្រង់។" } }, + "nb": { "stringUnit": { "state": "translated", "value": "Bruk Command, Option eller Control sammen med en annen tast. Overlegget bruker et innebygd panel som kan vises over fullskjermsapper." } }, + "pl": { "stringUnit": { "state": "translated", "value": "Użyj Command, Option lub Control z innym klawiszem. Nakładka używa natywnego panelu, który może pojawić się nad aplikacjami pełnoekranowymi." } }, + "pt-BR": { "stringUnit": { "state": "translated", "value": "Use Command, Option ou Control com outra tecla. A sobreposição usa um painel nativo que pode aparecer sobre apps em tela cheia." } }, + "ru": { "stringUnit": { "state": "translated", "value": "Используйте Command, Option или Control с другой клавишей. Оверлей использует нативную панель, которая может появляться поверх полноэкранных приложений." } }, + "th": { "stringUnit": { "state": "translated", "value": "ใช้ Command, Option หรือ Control ร่วมกับปุ่มอื่น โอเวอร์เลย์นี้ใช้แผงแบบเนทีฟที่แสดงเหนือแอปเต็มหน้าจอได้" } }, + "tr": { "stringUnit": { "state": "translated", "value": "Command, Option veya Control tuşunu başka bir tuşla kullanın. Katman, tam ekran uygulamaların üzerinde görünebilen yerel bir panel kullanır." } }, + "uk": { "stringUnit": { "state": "translated", "value": "Використовуйте Command, Option або Control з іншою клавішею. Накладка використовує нативну панель, яка може з'являтися поверх повноекранних програм." } }, + "zh-Hans": { "stringUnit": { "state": "translated", "value": "将 Command、Option 或 Control 与另一个按键组合使用。该覆盖层使用原生面板,可显示在全屏 App 上方。" } }, + "zh-Hant": { "stringUnit": { "state": "translated", "value": "將 Command、Option 或 Control 與另一個按鍵組合使用。此覆蓋層使用原生面板,可顯示在全螢幕 App 上方。" } } } }, "settings.globalHotkey.shortcut": { @@ -72834,7 +72919,24 @@ "state": "translated", "value": "핫키 윈도우 표시/숨기기" } - } + }, + "ar": { "stringUnit": { "state": "translated", "value": "إظهار/إخفاء نافذة الاختصار" } }, + "bs": { "stringUnit": { "state": "translated", "value": "Prikaži/sakrij prozor prečice" } }, + "da": { "stringUnit": { "state": "translated", "value": "Vis/skjul hotkeyvindue" } }, + "de": { "stringUnit": { "state": "translated", "value": "Hotkey-Fenster ein-/ausblenden" } }, + "es": { "stringUnit": { "state": "translated", "value": "Mostrar/ocultar ventana de tecla rápida" } }, + "fr": { "stringUnit": { "state": "translated", "value": "Afficher/masquer la fenêtre de raccourci" } }, + "it": { "stringUnit": { "state": "translated", "value": "Mostra/nascondi finestra tasto rapido" } }, + "km": { "stringUnit": { "state": "translated", "value": "បង្ហាញ/លាក់បង្អួចគ្រាប់ចុចផ្លូវកាត់" } }, + "nb": { "stringUnit": { "state": "translated", "value": "Vis/skjul hurtigtastvindu" } }, + "pl": { "stringUnit": { "state": "translated", "value": "Pokaż/ukryj okno skrótu" } }, + "pt-BR": { "stringUnit": { "state": "translated", "value": "Mostrar/ocultar janela de tecla de atalho" } }, + "ru": { "stringUnit": { "state": "translated", "value": "Показать/скрыть окно горячей клавиши" } }, + "th": { "stringUnit": { "state": "translated", "value": "แสดง/ซ่อนหน้าต่างปุ่มลัด" } }, + "tr": { "stringUnit": { "state": "translated", "value": "Kısayol penceresini göster/gizle" } }, + "uk": { "stringUnit": { "state": "translated", "value": "Показати/приховати вікно гарячої клавіші" } }, + "zh-Hans": { "stringUnit": { "state": "translated", "value": "显示/隐藏热键窗口" } }, + "zh-Hant": { "stringUnit": { "state": "translated", "value": "顯示/隱藏熱鍵視窗" } } } }, "settings.shortcuts.chords": { @@ -74278,6 +74380,12 @@ "value": "system shortcut global keyboard show hide bring forward 키보드 단축키 핫키 명령 키 기록기 외관 테마 색상 색 구성표 라이트 다크 시스템 모드 전역 앞으로" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "system shortcut global keyboard show hide overlay full screen hotkey window ប្រព័ន្ធ ផ្លូវកាត់ សកល ក្តារចុច បង្ហាញ លាក់ ស្រទាប់ ពេញអេក្រង់ បង្អួច" + } + }, "nb": { "stringUnit": { "state": "translated", @@ -82743,6 +82851,12 @@ "value": "global hotkey enable system wide show hide overlay hotkey window full screen 키보드 단축키 핫키 명령 키 기록기 외관 테마 색상 색 구성표 라이트 다크 시스템 모드 비밀번호 비밀 토큰 자격 증명 접근 허용 비활성화 활성화 권한 안전하지 않음 전역 글로벌 오버레이 전체 화면" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "global hotkey enable system wide show hide overlay hotkey window full screen ក្តារចុច ផ្លូវកាត់ ហត់ឃី បង្ហាញ លាក់ ស្រទាប់ បង្អួច ពេញអេក្រង់ បើក អនុញ្ញាត ប្រព័ន្ធ" + } + }, "nb": { "stringUnit": { "state": "translated", @@ -82862,6 +82976,12 @@ "value": "global hotkey overlay shortcut recorder key command option control 자동화 api cli 제어 소켓 에이전트 훅 포트 환경 키보드 단축키 핫키 명령 키 기록기 전역 글로벌 오버레이" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "global hotkey overlay shortcut recorder key command option control សកល ហត់ឃី ស្រទាប់ ផ្លូវកាត់ កត់ត្រា គ្រាប់ចុច command option control ក្តារចុច" + } + }, "nb": { "stringUnit": { "state": "translated", diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift index 80b08ddfadc1..61dbc197c3e7 100644 --- a/cmuxTests/GlobalHotkeyPanelControllerTests.swift +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -47,8 +47,8 @@ final class GlobalHotkeyPanelControllerTests: XCTestCase { XCTAssertGreaterThanOrEqual(frame.minY, screenFrame.minY) XCTAssertLessThanOrEqual(frame.maxX, screenFrame.maxX) XCTAssertLessThanOrEqual(frame.maxY, screenFrame.maxY) - XCTAssertGreaterThan(frame.width, screenFrame.width * 0.85) - XCTAssertGreaterThan(frame.height, screenFrame.height * 0.75) + XCTAssertGreaterThanOrEqual(frame.width, screenFrame.width * 0.85) + XCTAssertGreaterThanOrEqual(frame.height, screenFrame.height * 0.75) XCTAssertEqual(frame.midX, screenFrame.midX, accuracy: 1) XCTAssertEqual(frame.maxY, screenFrame.maxY - max(20, min(56, screenFrame.height * 0.045)), accuracy: 1) } From 13f0b387b07ae09fa1dcb95a2941834ba133ae53 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:30:42 -0700 Subject: [PATCH 08/17] fix: localize hotkey shortcut web metadata --- web/app/[locale]/docs/configuration/page.tsx | 9 +++- web/app/[locale]/keyboard-shortcuts.tsx | 9 +++- web/data/cmux-shortcuts.ts | 47 +++++++++++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index afd993c4be2d..23005c5a9e5f 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -8,6 +8,12 @@ import settingsSchema from "../../../../data/cmux.schema.json"; import { shortcutCategories, type LocalizedText } from "../../../../data/cmux-shortcuts"; import { DocsHeading } from "../../components/docs-heading"; +const localeAliases: Record = { + nb: "no", + "zh-Hans": "zh-CN", + "zh-Hant": "zh-TW", +}; + type SchemaProperty = { title?: string; description?: string; @@ -108,7 +114,8 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s } function localizedText(text: LocalizedText, locale: string) { - return locale.startsWith("ja") ? text.ja : text.en; + const normalizedLocale = localeAliases[locale] ?? locale; + return text[normalizedLocale] ?? text[locale.split("-")[0]] ?? text.en; } function shortcutComboToConfig(combo: string[]) { diff --git a/web/app/[locale]/keyboard-shortcuts.tsx b/web/app/[locale]/keyboard-shortcuts.tsx index 6072d4e037f7..49279d372e8d 100644 --- a/web/app/[locale]/keyboard-shortcuts.tsx +++ b/web/app/[locale]/keyboard-shortcuts.tsx @@ -4,8 +4,15 @@ import { useMemo, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; import { shortcutCategories, type LocalizedText, type Shortcut } from "../../data/cmux-shortcuts"; +const localeAliases: Record = { + nb: "no", + "zh-Hans": "zh-CN", + "zh-Hant": "zh-TW", +}; + function localizedText(text: LocalizedText, locale: string) { - return locale.startsWith("ja") ? text.ja : text.en; + const normalizedLocale = localeAliases[locale] ?? locale; + return text[normalizedLocale] ?? text[locale.split("-")[0]] ?? text.en; } function normalize(s: string) { diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 9d6d0799b415..6f35434a558f 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -1,6 +1,7 @@ export type LocalizedText = { en: string; ja: string; + [locale: string]: string | undefined; }; export type Shortcut = { @@ -28,8 +29,50 @@ export const shortcutCategories: ShortcutCategory[] = [ { id: "showHideAllWindows", combos: [["⌃", "⌥", "⌘", "."]], - description: { en: "Show/hide the cmux hotkey window", ja: "cmuxホットキーウインドウを表示/非表示" }, - note: { en: "system-wide hotkey", ja: "システム全体のホットキー" }, + description: { + en: "Show/hide the cmux hotkey window", + ja: "cmuxホットキーウインドウを表示/非表示", + ar: "إظهار/إخفاء نافذة اختصار cmux", + bs: "Prikaži/sakrij cmux prozor prečice", + da: "Vis/skjul cmux-hotkeyvinduet", + de: "cmux-Hotkey-Fenster ein-/ausblenden", + es: "Mostrar/ocultar la ventana de tecla rápida de cmux", + fr: "Afficher/masquer la fenêtre de raccourci cmux", + it: "Mostra/nascondi la finestra tasto rapido cmux", + km: "បង្ហាញ/លាក់បង្អួចគ្រាប់ចុចផ្លូវកាត់ cmux", + ko: "cmux 핫키 윈도우 표시/숨기기", + no: "Vis/skjul cmux-hurtigtastvinduet", + pl: "Pokaż/ukryj okno skrótu cmux", + "pt-BR": "Mostrar/ocultar a janela de tecla de atalho do cmux", + ru: "Показать/скрыть окно горячей клавиши cmux", + th: "แสดง/ซ่อนหน้าต่างปุ่มลัด cmux", + tr: "cmux kısayol penceresini göster/gizle", + uk: "Показати/приховати вікно гарячої клавіші cmux", + "zh-CN": "显示/隐藏 cmux 热键窗口", + "zh-TW": "顯示/隱藏 cmux 熱鍵視窗", + }, + note: { + en: "system-wide hotkey", + ja: "システム全体のホットキー", + ar: "اختصار على مستوى النظام", + bs: "sistemska prečica", + da: "systemdækkende genvejstast", + de: "systemweiter Hotkey", + es: "tecla rápida en todo el sistema", + fr: "raccourci système", + it: "tasto rapido a livello di sistema", + km: "គ្រាប់ចុចផ្លូវកាត់ទូទាំងប្រព័ន្ធ", + ko: "시스템 전체 단축키", + no: "systemomfattende hurtigtast", + pl: "skrót systemowy", + "pt-BR": "tecla de atalho em todo o sistema", + ru: "системная горячая клавиша", + th: "ปุ่มลัดทั้งระบบ", + tr: "sistem genelinde kısayol", + uk: "загальносистемна гаряча клавіша", + "zh-CN": "系统级热键", + "zh-TW": "全系統熱鍵", + }, }, { id: "globalSearch", From d23ff6f0613cd7a9a830409bc56610957bb97a95 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:37:08 -0700 Subject: [PATCH 09/17] fix: split hotkey panel helper boundaries --- .../App/GlobalHotkeyPanelConfiguration.swift | 40 ++++++ .../App/GlobalHotkeyPanelContentState.swift | 36 ++++++ Sources/App/GlobalHotkeyPanelController.swift | 117 +++--------------- Sources/App/GlobalHotkeyPanelLayout.swift | 19 +++ Sources/AppDelegate.swift | 5 +- cmux.xcodeproj/project.pbxproj | 12 ++ 6 files changed, 131 insertions(+), 98 deletions(-) create mode 100644 Sources/App/GlobalHotkeyPanelConfiguration.swift create mode 100644 Sources/App/GlobalHotkeyPanelContentState.swift create mode 100644 Sources/App/GlobalHotkeyPanelLayout.swift diff --git a/Sources/App/GlobalHotkeyPanelConfiguration.swift b/Sources/App/GlobalHotkeyPanelConfiguration.swift new file mode 100644 index 000000000000..b873728a27a6 --- /dev/null +++ b/Sources/App/GlobalHotkeyPanelConfiguration.swift @@ -0,0 +1,40 @@ +import AppKit + +@MainActor +enum GlobalHotkeyPanelConfiguration { + static let windowIdentifier = "cmux.hotkeyPanel" + + static let styleMask: NSWindow.StyleMask = [ + .nonactivatingPanel, + .titled, + .resizable, + .fullSizeContentView, + ] + + static let collectionBehavior: NSWindow.CollectionBehavior = [ + .canJoinAllSpaces, + .fullScreenAuxiliary, + .transient, + .ignoresCycle, + ] + + static var windowLevel: NSWindow.Level { + NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.mainMenuWindow)) - 2) + } + + static func apply(to panel: NSPanel) { + panel.level = windowLevel + panel.collectionBehavior = collectionBehavior + panel.isFloatingPanel = true + panel.becomesKeyOnlyIfNeeded = true + panel.hidesOnDeactivate = false + panel.isExcludedFromWindowsMenu = true + panel.animationBehavior = .utilityWindow + panel.titleVisibility = .hidden + panel.titlebarAppearsTransparent = true + panel.isMovableByWindowBackground = false + panel.isMovable = true + panel.isRestorable = false + panel.isReleasedWhenClosed = false + } +} diff --git a/Sources/App/GlobalHotkeyPanelContentState.swift b/Sources/App/GlobalHotkeyPanelContentState.swift new file mode 100644 index 000000000000..0279b1a0187a --- /dev/null +++ b/Sources/App/GlobalHotkeyPanelContentState.swift @@ -0,0 +1,36 @@ +import Foundation + +@MainActor +struct GlobalHotkeyPanelContentState { + let windowId: UUID + let tabManager: TabManager + let notificationStore: TerminalNotificationStore + let sidebarState: SidebarState + let sidebarSelectionState: SidebarSelectionState + let fileExplorerState: FileExplorerState + let cmuxConfigStore: CmuxConfigStore + + init( + windowId: UUID = UUID(), + tabManager: TabManager? = nil, + notificationStore: TerminalNotificationStore? = nil, + sidebarState: SidebarState? = nil, + sidebarSelectionState: SidebarSelectionState? = nil, + fileExplorerState: FileExplorerState? = nil, + cmuxConfigStore: CmuxConfigStore? = nil + ) { + let resolvedTabManager = tabManager ?? TabManager(autoWelcomeIfNeeded: true) + let resolvedConfigStore = cmuxConfigStore ?? CmuxConfigStore() + + self.windowId = windowId + self.tabManager = resolvedTabManager + self.notificationStore = notificationStore ?? TerminalNotificationStore.shared + self.sidebarState = sidebarState ?? SidebarState() + self.sidebarSelectionState = sidebarSelectionState ?? SidebarSelectionState() + self.fileExplorerState = fileExplorerState ?? FileExplorerState() + self.cmuxConfigStore = resolvedConfigStore + + resolvedConfigStore.wireDirectoryTracking(tabManager: resolvedTabManager) + resolvedConfigStore.loadAll() + } +} diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index 60ebf439ece4..5802b0ce94ea 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -1,63 +1,6 @@ import AppKit import SwiftUI -@MainActor -enum GlobalHotkeyPanelLayout { - static func panelFrame(in screenFrame: NSRect) -> NSRect { - let margin = max(20, min(56, screenFrame.height * 0.045)) - let width = min(max(960, screenFrame.width * 0.88), screenFrame.width - (margin * 2)) - let height = min(max(560, screenFrame.height * 0.78), screenFrame.height - (margin * 2)) - let origin = NSPoint( - x: screenFrame.midX - (width / 2), - y: screenFrame.maxY - height - margin - ) - return NSRect(origin: origin, size: NSSize(width: width, height: height)).integral - } - - static func preferredScreen(for point: NSPoint = NSEvent.mouseLocation) -> NSScreen? { - NSScreen.screens.first { $0.frame.contains(point) } ?? NSScreen.main ?? NSScreen.screens.first - } -} - -@MainActor -enum GlobalHotkeyPanelConfiguration { - static let windowIdentifier = "cmux.hotkeyPanel" - - static let styleMask: NSWindow.StyleMask = [ - .nonactivatingPanel, - .titled, - .resizable, - .fullSizeContentView, - ] - - static let collectionBehavior: NSWindow.CollectionBehavior = [ - .canJoinAllSpaces, - .fullScreenAuxiliary, - .transient, - .ignoresCycle, - ] - - static var windowLevel: NSWindow.Level { - NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.mainMenuWindow)) - 2) - } - - static func apply(to panel: NSPanel) { - panel.level = windowLevel - panel.collectionBehavior = collectionBehavior - panel.isFloatingPanel = true - panel.becomesKeyOnlyIfNeeded = true - panel.hidesOnDeactivate = false - panel.isExcludedFromWindowsMenu = true - panel.animationBehavior = .utilityWindow - panel.titleVisibility = .hidden - panel.titlebarAppearsTransparent = true - panel.isMovableByWindowBackground = false - panel.isMovable = true - panel.isRestorable = false - panel.isReleasedWhenClosed = false - } -} - @MainActor final class GlobalHotkeyPanel: NSPanel { var onCancel: (() -> Void)? @@ -74,16 +17,12 @@ final class GlobalHotkeyPanel: NSPanel { @MainActor final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { private weak var appDelegate: AppDelegate? + private let contentState: GlobalHotkeyPanelContentState private var windowController: NSWindowController? - private var windowId: UUID? - private var tabManager: TabManager? - private var sidebarState: SidebarState? - private var sidebarSelectionState: SidebarSelectionState? - private var fileExplorerState: FileExplorerState? - private var cmuxConfigStore: CmuxConfigStore? - - init(appDelegate: AppDelegate) { + + init(appDelegate: AppDelegate, contentState: GlobalHotkeyPanelContentState) { self.appDelegate = appDelegate + self.contentState = contentState super.init() } @@ -142,24 +81,14 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { guard let appDelegate else { return nil } - let windowId = UUID() - let tabManager = TabManager(autoWelcomeIfNeeded: true) - let notificationStore = TerminalNotificationStore.shared - let sidebarState = SidebarState() - let sidebarSelectionState = SidebarSelectionState() - let fileExplorerState = FileExplorerState() - let cmuxConfigStore = CmuxConfigStore() - cmuxConfigStore.wireDirectoryTracking(tabManager: tabManager) - cmuxConfigStore.loadAll() - - let root = ContentView(updateViewModel: appDelegate.updateViewModel, windowId: windowId) + let root = ContentView(updateViewModel: appDelegate.updateViewModel, windowId: contentState.windowId) .mainWindowContextRole(.globalHotkeyPanel) - .environmentObject(tabManager) - .environmentObject(notificationStore) - .environmentObject(sidebarState) - .environmentObject(sidebarSelectionState) - .environmentObject(fileExplorerState) - .environmentObject(cmuxConfigStore) + .environmentObject(contentState.tabManager) + .environmentObject(contentState.notificationStore) + .environmentObject(contentState.sidebarState) + .environmentObject(contentState.sidebarSelectionState) + .environmentObject(contentState.fileExplorerState) + .environmentObject(contentState.cmuxConfigStore) let initialFrame = GlobalHotkeyPanelLayout.preferredScreen() .map { GlobalHotkeyPanelLayout.panelFrame(in: $0.frame) } @@ -183,23 +112,17 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { appDelegate.applyWindowDecorations(to: panel) appDelegate.registerMainWindow( panel, - windowId: windowId, - tabManager: tabManager, - sidebarState: sidebarState, - sidebarSelectionState: sidebarSelectionState, - fileExplorerState: fileExplorerState, - cmuxConfigStore: cmuxConfigStore, + windowId: contentState.windowId, + tabManager: contentState.tabManager, + sidebarState: contentState.sidebarState, + sidebarSelectionState: contentState.sidebarSelectionState, + fileExplorerState: contentState.fileExplorerState, + cmuxConfigStore: contentState.cmuxConfigStore, role: .globalHotkeyPanel ) - appDelegate.publishCmuxWindowLifecycle(name: "window.created", windowId: windowId, origin: "global_hotkey") - installFileDropOverlay(on: panel, tabManager: tabManager) - - self.windowId = windowId - self.tabManager = tabManager - self.sidebarState = sidebarState - self.sidebarSelectionState = sidebarSelectionState - self.fileExplorerState = fileExplorerState - self.cmuxConfigStore = cmuxConfigStore + appDelegate.publishCmuxWindowLifecycle(name: "window.created", windowId: contentState.windowId, origin: "global_hotkey") + installFileDropOverlay(on: panel, tabManager: contentState.tabManager) + windowController = NSWindowController(window: panel) return panel } diff --git a/Sources/App/GlobalHotkeyPanelLayout.swift b/Sources/App/GlobalHotkeyPanelLayout.swift new file mode 100644 index 000000000000..86e2af7e062c --- /dev/null +++ b/Sources/App/GlobalHotkeyPanelLayout.swift @@ -0,0 +1,19 @@ +import AppKit + +@MainActor +enum GlobalHotkeyPanelLayout { + static func panelFrame(in screenFrame: NSRect) -> NSRect { + let margin = max(20, min(56, screenFrame.height * 0.045)) + let width = min(max(960, screenFrame.width * 0.88), screenFrame.width - (margin * 2)) + let height = min(max(560, screenFrame.height * 0.78), screenFrame.height - (margin * 2)) + let origin = NSPoint( + x: screenFrame.midX - (width / 2), + y: screenFrame.maxY - height - margin + ) + return NSRect(origin: origin, size: NSSize(width: width, height: height)).integral + } + + static func preferredScreen(for point: NSPoint = NSEvent.mouseLocation) -> NSScreen? { + NSScreen.screens.first { $0.frame.contains(point) } ?? NSScreen.main ?? NSScreen.screens.first + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 39f4339492a3..d09aabe2ff81 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -734,7 +734,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private var shortcutDefaultsObserver: NSObjectProtocol? private var menuBarVisibilityObserver: NSObjectProtocol? private var splitButtonTooltipRefreshScheduled = false - private lazy var globalHotkeyPanelController = GlobalHotkeyPanelController(appDelegate: self) + private lazy var globalHotkeyPanelController = GlobalHotkeyPanelController( + appDelegate: self, + contentState: GlobalHotkeyPanelContentState() + ) private var didScheduleGhosttyCrashBreadcrumbCheck = false private var ghosttyCrashBreadcrumbTask: Task? private struct PendingConfiguredShortcutChord { diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 3297ee6a5650..6330217945de 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -19,6 +19,9 @@ 1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */; }; 17C9F5BA0DD14EDC8C3E5001 /* MainWindowVisibilityControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C9F5BA0DD14EDC8C3E5002 /* MainWindowVisibilityControllerTests.swift */; }; 2758A0032758A0032758A003 /* GlobalHotkeyPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */; }; + 2758B0012758B0012758B001 /* GlobalHotkeyPanelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */; }; + 2758B0032758B0032758B003 /* GlobalHotkeyPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */; }; + 2758B0052758B0052758B005 /* GlobalHotkeyPanelContentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */; }; 1A1B2C3D4E5F607180000001 /* WorkspacePromptSubmit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1B2C3D4E5F607180000002 /* WorkspacePromptSubmit.swift */; }; 1A1B2C3D4E5F607180000003 /* WorkspacePromptSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1B2C3D4E5F607180000004 /* WorkspacePromptSubmitTests.swift */; }; 1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */; }; @@ -723,6 +726,9 @@ E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/TerminalFindEscapeRouting.swift; sourceTree = ""; }; C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/MenuBarExtraController.swift; sourceTree = ""; }; 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelController.swift; sourceTree = ""; }; + 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelConfiguration.swift; sourceTree = ""; }; + 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelLayout.swift; sourceTree = ""; }; + 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelContentState.swift; sourceTree = ""; }; 2F0C07000000000000000001 /* CmuxMainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CmuxMainWindow.swift; sourceTree = ""; }; 2F0C06000000000000000001 /* MainWindowVisibilityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/MainWindowVisibilityController.swift; sourceTree = ""; }; A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/DebugLogging.swift; sourceTree = ""; }; @@ -1291,6 +1297,9 @@ E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */, 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */, + 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */, + 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */, + 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */, 2F0C07000000000000000001 /* CmuxMainWindow.swift */, 2F0C06000000000000000001 /* MainWindowVisibilityController.swift */, A500D011A1B2C3D4E5F60718 /* DebugLogging.swift */, @@ -2044,6 +2053,9 @@ E5C0F1A0E5C0F1A0E5C0F1A0 /* TerminalFindEscapeRouting.swift in Sources */, 1A8BEE693C9E4C3190CB7F20 /* MenuBarExtraController.swift in Sources */, 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */, + 2758B0052758B0052758B005 /* GlobalHotkeyPanelContentState.swift in Sources */, + 2758B0012758B0012758B001 /* GlobalHotkeyPanelConfiguration.swift in Sources */, + 2758B0032758B0032758B003 /* GlobalHotkeyPanelLayout.swift in Sources */, 2F0C07000000000000000002 /* CmuxMainWindow.swift in Sources */, 2F0C06000000000000000002 /* MainWindowVisibilityController.swift in Sources */, A500D010A1B2C3D4E5F60718 /* DebugLogging.swift in Sources */, From 935687ee8855f040103c47cc67dc79600629c357 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:39:42 -0700 Subject: [PATCH 10/17] fix: share shortcut localization helper --- web/app/[locale]/docs/configuration/page.tsx | 17 +++------------- web/app/[locale]/keyboard-shortcuts.tsx | 21 +++++--------------- web/data/cmux-shortcuts.ts | 11 ++++++++++ 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 23005c5a9e5f..feba71373b78 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -5,15 +5,9 @@ import { Link } from "../../../../i18n/navigation"; import { CodeBlock } from "../../components/code-block"; import { Callout } from "../../components/callout"; import settingsSchema from "../../../../data/cmux.schema.json"; -import { shortcutCategories, type LocalizedText } from "../../../../data/cmux-shortcuts"; +import { localizedShortcutText, shortcutCategories } from "../../../../data/cmux-shortcuts"; import { DocsHeading } from "../../components/docs-heading"; -const localeAliases: Record = { - nb: "no", - "zh-Hans": "zh-CN", - "zh-Hant": "zh-TW", -}; - type SchemaProperty = { title?: string; description?: string; @@ -113,11 +107,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s }; } -function localizedText(text: LocalizedText, locale: string) { - const normalizedLocale = localeAliases[locale] ?? locale; - return text[normalizedLocale] ?? text[locale.split("-")[0]] ?? text.en; -} - function shortcutComboToConfig(combo: string[]) { const modifierMap: Record = { "⌘": "cmd", @@ -405,10 +394,10 @@ working-directory = ~/code`} {shortcut.id}

- {localizedText(shortcut.description, locale)} + {localizedShortcutText(shortcut.description, locale)} {shortcut.note && ( - {localizedText(shortcut.note, locale)} + {localizedShortcutText(shortcut.note, locale)} )}

diff --git a/web/app/[locale]/keyboard-shortcuts.tsx b/web/app/[locale]/keyboard-shortcuts.tsx index 49279d372e8d..61f33ba99638 100644 --- a/web/app/[locale]/keyboard-shortcuts.tsx +++ b/web/app/[locale]/keyboard-shortcuts.tsx @@ -2,18 +2,7 @@ import { useMemo, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; -import { shortcutCategories, type LocalizedText, type Shortcut } from "../../data/cmux-shortcuts"; - -const localeAliases: Record = { - nb: "no", - "zh-Hans": "zh-CN", - "zh-Hant": "zh-TW", -}; - -function localizedText(text: LocalizedText, locale: string) { - const normalizedLocale = localeAliases[locale] ?? locale; - return text[normalizedLocale] ?? text[locale.split("-")[0]] ?? text.en; -} +import { localizedShortcutText, shortcutCategories, type Shortcut } from "../../data/cmux-shortcuts"; function normalize(s: string) { return s.toLowerCase().replace(/\s+/g, " ").trim(); @@ -41,8 +30,8 @@ function KeyCombo({ combo }: { combo: string[] }) { } function ShortcutRow({ shortcut, locale }: { shortcut: Shortcut; locale: string }) { - const description = localizedText(shortcut.description, locale); - const note = shortcut.note ? localizedText(shortcut.note, locale) : undefined; + const description = localizedShortcutText(shortcut.description, locale); + const note = shortcut.note ? localizedShortcutText(shortcut.note, locale) : undefined; return (
@@ -80,8 +69,8 @@ export function KeyboardShortcuts() { ...cat, shortcuts: cat.shortcuts.filter((shortcut) => { const catTitle = t(`cat.${cat.titleKey}`); - const description = localizedText(shortcut.description, locale); - const note = shortcut.note ? localizedText(shortcut.note, locale) : ""; + const description = localizedShortcutText(shortcut.description, locale); + const note = shortcut.note ? localizedShortcutText(shortcut.note, locale) : ""; const combos = shortcut.combos.map(comboToText).join(" "); return normalize(`${catTitle} ${combos} ${description} ${note}`).includes(q); }), diff --git a/web/data/cmux-shortcuts.ts b/web/data/cmux-shortcuts.ts index 6f35434a558f..42fc47b5f4f7 100644 --- a/web/data/cmux-shortcuts.ts +++ b/web/data/cmux-shortcuts.ts @@ -4,6 +4,17 @@ export type LocalizedText = { [locale: string]: string | undefined; }; +const localeAliases: Record = { + nb: "no", + "zh-Hans": "zh-CN", + "zh-Hant": "zh-TW", +}; + +export function localizedShortcutText(text: LocalizedText, locale: string) { + const normalizedLocale = localeAliases[locale] ?? locale; + return text[normalizedLocale] ?? text[locale.split("-")[0]] ?? text.en; +} + export type Shortcut = { id: string; combos: string[][]; From 86d0182b59047ec67095940cebcbcc39ed51d6fb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:44:02 -0700 Subject: [PATCH 11/17] fix: defer hotkey panel config load --- Sources/App/GlobalHotkeyPanelContentState.swift | 17 ++++++++++++++--- Sources/App/GlobalHotkeyPanelController.swift | 1 + Sources/CmuxConfig.swift | 13 +++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Sources/App/GlobalHotkeyPanelContentState.swift b/Sources/App/GlobalHotkeyPanelContentState.swift index 0279b1a0187a..45578cf27465 100644 --- a/Sources/App/GlobalHotkeyPanelContentState.swift +++ b/Sources/App/GlobalHotkeyPanelContentState.swift @@ -1,7 +1,7 @@ import Foundation @MainActor -struct GlobalHotkeyPanelContentState { +final class GlobalHotkeyPanelContentState { let windowId: UUID let tabManager: TabManager let notificationStore: TerminalNotificationStore @@ -9,6 +9,7 @@ struct GlobalHotkeyPanelContentState { let sidebarSelectionState: SidebarSelectionState let fileExplorerState: FileExplorerState let cmuxConfigStore: CmuxConfigStore + private var didScheduleConfigLoad = false init( windowId: UUID = UUID(), @@ -29,8 +30,18 @@ struct GlobalHotkeyPanelContentState { self.sidebarSelectionState = sidebarSelectionState ?? SidebarSelectionState() self.fileExplorerState = fileExplorerState ?? FileExplorerState() self.cmuxConfigStore = resolvedConfigStore + } - resolvedConfigStore.wireDirectoryTracking(tabManager: resolvedTabManager) - resolvedConfigStore.loadAll() + func scheduleConfigLoadAfterFirstDisplay() { + guard !didScheduleConfigLoad else { return } + didScheduleConfigLoad = true + cmuxConfigStore.wireDirectoryTracking( + tabManager: tabManager, + loadsInitialConfiguration: false + ) + let cmuxConfigStore = cmuxConfigStore + DispatchQueue.main.async { [weak cmuxConfigStore] in + cmuxConfigStore?.loadAll() + } } } diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index 5802b0ce94ea..cf5db54cbb41 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -51,6 +51,7 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { panel.orderFrontRegardless() panel.makeKey() appDelegate?.setActiveMainWindow(panel) + contentState.scheduleConfigLoadAfterFirstDisplay() #if DEBUG cmuxDebugLog("globalHotkey.panel.show window=\(panel.identifier?.rawValue ?? "")") diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift index 4d47710ae764..06a819a235d9 100644 --- a/Sources/CmuxConfig.swift +++ b/Sources/CmuxConfig.swift @@ -1972,7 +1972,7 @@ final class CmuxConfigStore: ObservableObject { // MARK: - Public API - func wireDirectoryTracking(tabManager: TabManager) { + func wireDirectoryTracking(tabManager: TabManager, loadsInitialConfiguration: Bool = true) { trackingCancellables.removeAll() self.tabManager = tabManager @@ -1999,7 +1999,10 @@ final class CmuxConfigStore: ObservableObject { } .store(in: &trackingCancellables) - updateLocalConfigPath(tabManager.selectedWorkspace?.surfaceTabBarDirectory) + updateLocalConfigPath( + tabManager.selectedWorkspace?.surfaceTabBarDirectory, + loadsConfiguration: loadsInitialConfiguration + ) } func notificationHooks(startingFrom directory: String?) -> [CmuxResolvedNotificationHook] { @@ -2018,7 +2021,7 @@ final class CmuxConfigStore: ObservableObject { ) } - private func updateLocalConfigPath(_ directory: String?) { + private func updateLocalConfigPath(_ directory: String?, loadsConfiguration: Bool = true) { let newPath: String? if let directory, !directory.isEmpty { localConfigSearchDirectory = directory @@ -2034,7 +2037,9 @@ final class CmuxConfigStore: ObservableObject { if fileWatchingEnabled, newPath != nil { startLocalFileWatcher() } - loadAll() + if loadsConfiguration { + loadAll() + } } private func resolvedLocalConfigPath(startingFrom directory: String) -> String { From 0a55ddce036826282c5e1fb4457c42f0ee529542 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Fri, 22 May 2026 21:56:01 -0700 Subject: [PATCH 12/17] fix: keep hotkey overlay isolated while hidden --- Resources/Localizable.xcstrings | 98 +++++++++---------- Sources/App/GlobalHotkeyPanelController.swift | 6 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 7cfc3f274d98..6714c62a6667 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -74335,49 +74335,49 @@ "ar": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward لوحة مفاتيح اختصار اختصارات مفتاح سريع أوامر تسجيل مظهر سمة لون ألوان مخطط فاتح داكن نظام وضع النظام عام تقدم" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window لوحة مفاتيح اختصار اختصارات مفتاح سريع أوامر تسجيل مظهر سمة لون ألوان مخطط فاتح داكن نظام وضع النظام عام تقدم" } }, "bs": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward tastatura prečica prečice hotkey komande tipke snimač izgled tema boja boje shema svijetlo tamno sistem način Sistemski Globalno Naprijed" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window tastatura prečica prečice hotkey komande tipke snimač izgled tema boja boje shema svijetlo tamno sistem način Sistemski Globalno Naprijed" } }, "da": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward tastatur genvej genveje hotkey kommandoer taster optager udseende tema farve farver skema lys mørk tilstand Globalt Frem" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window tastatur genvej genveje hotkey kommandoer taster optager udseende tema farve farver skema lys mørk tilstand Globalt Frem" } }, "de": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward tastatur kurzbefehl tastenkürzel hotkey befehle tasten rekorder darstellung design farbe farben schema hell dunkel modus Vor" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window tastatur kurzbefehl tastenkürzel hotkey befehle tasten rekorder darstellung design farbe farben schema hell dunkel modus Vor" } }, "es": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward teclado atajo atajos tecla rápida comandos teclas grabador apariencia tema color colores esquema claro oscuro sistema modo Adelante" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window teclado atajo atajos tecla rápida comandos teclas grabador apariencia tema color colores esquema claro oscuro sistema modo Adelante" } }, "fr": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward clavier raccourci raccourcis touche globale commandes touches enregistreur apparence thème couleur couleurs schéma clair sombre système mode Général Suivant" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window clavier raccourci raccourcis touche globale commandes touches enregistreur apparence thème couleur couleurs schéma clair sombre système mode Général Suivant" } }, "it": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward tastiera scorciatoia scorciatoie tasto rapido comandi tasti registratore aspetto tema colore colori schema chiaro scuro sistema modalità Globale Avanti" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window tastiera scorciatoia scorciatoie tasto rapido comandi tasti registratore aspetto tema colore colori schema chiaro scuro sistema modalità Globale Avanti" } }, "ko": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward 키보드 단축키 핫키 명령 키 기록기 외관 테마 색상 색 구성표 라이트 다크 시스템 모드 전역 앞으로" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window 키보드 단축키 핫키 명령 키 기록기 외관 테마 색상 색 구성표 라이트 다크 시스템 모드 전역 앞으로" } }, "km": { @@ -74389,55 +74389,55 @@ "nb": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward tastatur snarvei snarveier hurtigtast kommandoer taster opptaker utseende tema farge farger skjema lys mørk modus Globalt Fremover" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window tastatur snarvei snarveier hurtigtast kommandoer taster opptaker utseende tema farge farger skjema lys mørk modus Globalt Fremover" } }, "pl": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward klawiatura skrót skróty klawisz polecenia klawisze rejestrator wygląd motyw kolor kolory schemat jasny ciemny tryb Systemowy Globalne Do przodu" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window klawiatura skrót skróty klawisz polecenia klawisze rejestrator wygląd motyw kolor kolory schemat jasny ciemny tryb Systemowy Globalne Do przodu" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward teclado atalho atalhos tecla rápida comandos teclas gravador aparência tema cor cores esquema claro escuro sistema modo Avançar" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window teclado atalho atalhos tecla rápida comandos teclas gravador aparência tema cor cores esquema claro escuro sistema modo Avançar" } }, "ru": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward клавиатура сочетание клавиш горячая клавиша команды клавиши запись внешний вид тема цвет цвета схема светлый темный система режим Системное Глобальные Вперед Системный Системная" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window клавиатура сочетание клавиш горячая клавиша команды клавиши запись внешний вид тема цвет цвета схема светлый темный система режим Системное Глобальные Вперед Системный Системная" } }, "th": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward แป้นพิมพ์ ทางลัด ปุ่มลัด คำสั่ง คีย์ ตัวบันทึก ลักษณะ ธีม สี โครงร่าง สว่าง มืด ระบบ โหมด ทั่วไป ไปข้างหน้า" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window แป้นพิมพ์ ทางลัด ปุ่มลัด คำสั่ง คีย์ ตัวบันทึก ลักษณะ ธีม สี โครงร่าง สว่าง มืด ระบบ โหมด ทั่วไป ไปข้างหน้า" } }, "tr": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward klavye kısayol kısayollar tuşu komutlar tuşlar kaydedici görünüm tema renk renkler şema açık koyu sistem mod Genel İleri" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window klavye kısayol kısayollar tuşu komutlar tuşlar kaydedici görünüm tema renk renkler şema açık koyu sistem mod Genel İleri" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward 键盘 快捷键 热键 命令 按键 录制器 外观 主题 颜色 配色 浅色 深色 系统 模式 全局 前进" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window 键盘 快捷键 热键 命令 按键 录制器 外观 主题 颜色 配色 浅色 深色 系统 模式 全局 前进" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward 鍵盤 快捷鍵 熱鍵 命令 按鍵 錄製器 外觀 主題 顏色 配色 淺色 深色 系統 模式 全域 下一頁" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window 鍵盤 快捷鍵 熱鍵 命令 按鍵 錄製器 外觀 主題 顏色 配色 淺色 深色 系統 模式 全域 下一頁" } }, "uk": { "stringUnit": { "state": "translated", - "value": "system shortcut global keyboard show hide bring forward клавіатура скорочення гарячі клавіші команди записувач вигляд тема колір кольори схема світлий темний система режим Системна Глобальна Вперед" + "value": "system shortcut global keyboard show hide overlay full screen hotkey window клавіатура скорочення гарячі клавіші команди записувач вигляд тема колір кольори схема світлий темний система режим Системна Глобальна Вперед" } } } @@ -82806,43 +82806,43 @@ "ar": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows لوحة مفاتيح اختصار اختصارات مفتاح سريع أوامر تسجيل مظهر سمة لون ألوان مخطط فاتح داكن نظام وضع كلمة مرور سر رمز اعتماد وصول سماح تعطيل تمكين إذن غير آمن النظام عام" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen لوحة مفاتيح اختصار اختصارات مفتاح سريع أوامر تسجيل مظهر سمة لون ألوان مخطط فاتح داكن نظام وضع كلمة مرور سر رمز اعتماد وصول سماح تعطيل تمكين إذن غير آمن النظام عام" } }, "bs": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows tastatura prečica prečice komande tipke snimač izgled tema boja boje shema svijetlo tamno sistem način lozinka tajna token vjerodajnica pristup dozvoli onemogući omogući dozvola nesigurno Sistemski Globalno" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen tastatura prečica prečice komande tipke snimač izgled tema boja boje shema svijetlo tamno sistem način lozinka tajna token vjerodajnica pristup dozvoli onemogući omogući dozvola nesigurno Sistemski Globalno" } }, "da": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows tastatur genvej genveje kommandoer taster optager udseende tema farve farver skema lys mørk tilstand adgangskode hemmelighed token legitimationsoplysninger adgang tillad deaktiver aktiver tilladelse usikker Globalt" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen tastatur genvej genveje kommandoer taster optager udseende tema farve farver skema lys mørk tilstand adgangskode hemmelighed token legitimationsoplysninger adgang tillad deaktiver aktiver tilladelse usikker Globalt" } }, "de": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows tastatur kurzbefehl tastenkürzel befehle tasten rekorder darstellung design farbe farben schema hell dunkel modus passwort geheimnis token zugangsdaten zugriff erlauben deaktivieren aktivieren berechtigung unsicher" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen tastatur kurzbefehl tastenkürzel befehle tasten rekorder darstellung design farbe farben schema hell dunkel modus passwort geheimnis token zugangsdaten zugriff erlauben deaktivieren aktivieren berechtigung unsicher" } }, "es": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows teclado atajo atajos tecla rápida comandos teclas grabador apariencia tema color colores esquema claro oscuro sistema modo contraseña secreto token credencial acceso permitir desactivar activar permiso inseguro" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen teclado atajo atajos tecla rápida comandos teclas grabador apariencia tema color colores esquema claro oscuro sistema modo contraseña secreto token credencial acceso permitir desactivar activar permiso inseguro" } }, "fr": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows clavier raccourci raccourcis touche globale commandes touches enregistreur apparence thème couleur couleurs schéma clair sombre système mode mot de passe secret jeton identifiant accès autoriser désactiver activer autorisation non sécurisé Général" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen clavier raccourci raccourcis touche globale commandes touches enregistreur apparence thème couleur couleurs schéma clair sombre système mode mot de passe secret jeton identifiant accès autoriser désactiver activer autorisation non sécurisé Général" } }, "it": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows tastiera scorciatoia scorciatoie tasto rapido comandi tasti registratore aspetto tema colore colori schema chiaro scuro sistema modalità password segreto token credenziale accesso consenti disabilita abilita autorizzazione non sicuro Globale" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen tastiera scorciatoia scorciatoie tasto rapido comandi tasti registratore aspetto tema colore colori schema chiaro scuro sistema modalità password segreto token credenziale accesso consenti disabilita abilita autorizzazione non sicuro Globale" } }, "ko": { @@ -82860,55 +82860,55 @@ "nb": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows tastatur snarvei snarveier hurtigtast kommandoer taster opptaker utseende tema farge farger skjema lys mørk modus passord hemmelighet token legitimasjon tilgang tillat deaktiver aktiver tillatelse usikker Globalt" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen tastatur snarvei snarveier hurtigtast kommandoer taster opptaker utseende tema farge farger skjema lys mørk modus passord hemmelighet token legitimasjon tilgang tillat deaktiver aktiver tillatelse usikker Globalt" } }, "pl": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows klawiatura skrót skróty klawisz polecenia klawisze rejestrator wygląd motyw kolor kolory schemat jasny ciemny tryb hasło sekret token poświadczenie dostęp zezwól wyłącz włącz uprawnienie niezabezpieczone Systemowy Globalne" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen klawiatura skrót skróty klawisz polecenia klawisze rejestrator wygląd motyw kolor kolory schemat jasny ciemny tryb hasło sekret token poświadczenie dostęp zezwól wyłącz włącz uprawnienie niezabezpieczone Systemowy Globalne" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows teclado atalho atalhos tecla rápida comandos teclas gravador aparência tema cor cores esquema claro escuro sistema modo senha segredo token credencial acesso permitir desativar ativar permissão inseguro" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen teclado atalho atalhos tecla rápida comandos teclas gravador aparência tema cor cores esquema claro escuro sistema modo senha segredo token credencial acesso permitir desativar ativar permissão inseguro" } }, "ru": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows клавиатура сочетание клавиш горячая клавиша команды клавиши запись внешний вид тема цвет цвета схема светлый темный система режим пароль секрет токен учетные данные доступ разрешить отключить включить разрешение небезопасно Системное Глобальные Системный Системная" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen клавиатура сочетание клавиш горячая клавиша команды клавиши запись внешний вид тема цвет цвета схема светлый темный система режим пароль секрет токен учетные данные доступ разрешить отключить включить разрешение небезопасно Системное Глобальные Системный Системная" } }, "th": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows แป้นพิมพ์ ทางลัด ปุ่มลัด คำสั่ง คีย์ ตัวบันทึก ลักษณะ ธีม สี โครงร่าง สว่าง มืด ระบบ โหมด รหัสผ่าน ความลับ โทเค็น ข้อมูลรับรอง เข้าถึง อนุญาต ปิด เปิด สิทธิ์ ไม่ปลอดภัย ทั่วไป" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen แป้นพิมพ์ ทางลัด ปุ่มลัด คำสั่ง คีย์ ตัวบันทึก ลักษณะ ธีม สี โครงร่าง สว่าง มืด ระบบ โหมด รหัสผ่าน ความลับ โทเค็น ข้อมูลรับรอง เข้าถึง อนุญาต ปิด เปิด สิทธิ์ ไม่ปลอดภัย ทั่วไป" } }, "tr": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows klavye kısayol kısayollar tuşu komutlar tuşlar kaydedici görünüm tema renk renkler şema açık koyu sistem mod parola gizli token kimlik bilgisi erişim izin ver devre dışı etkinleştir güvensiz Genel" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen klavye kısayol kısayollar tuşu komutlar tuşlar kaydedici görünüm tema renk renkler şema açık koyu sistem mod parola gizli token kimlik bilgisi erişim izin ver devre dışı etkinleştir güvensiz Genel" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows 键盘 快捷键 热键 命令 按键 录制器 外观 主题 颜色 配色 浅色 深色 系统 模式 密码 密钥 令牌 凭据 访问 允许 禁用 启用 权限 不安全 全局" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen 键盘 快捷键 热键 命令 按键 录制器 外观 主题 颜色 配色 浅色 深色 系统 模式 密码 密钥 令牌 凭据 访问 允许 禁用 启用 权限 不安全 全局" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows 鍵盤 快捷鍵 熱鍵 命令 按鍵 錄製器 外觀 主題 顏色 配色 淺色 深色 系統 模式 密碼 密鑰 權杖 憑證 存取 允許 停用 啟用 權限 不安全 全域" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen 鍵盤 快捷鍵 熱鍵 命令 按鍵 錄製器 外觀 主題 顏色 配色 淺色 深色 系統 模式 密碼 密鑰 權杖 憑證 存取 允許 停用 啟用 權限 不安全 全域" } }, "uk": { "stringUnit": { "state": "translated", - "value": "global hotkey enable system wide show hide all windows клавіатура скорочення гарячі клавіші команди записувач вигляд тема колір кольори схема світлий темний система режим пароль секрет токен облікові дані доступ дозволити вимкнути увімкнути дозвіл небезпечно Системна Глобальна" + "value": "global hotkey enable system wide show hide overlay hotkey window full screen клавіатура скорочення гарячі клавіші команди записувач вигляд тема колір кольори схема світлий темний система режим пароль секрет токен облікові дані доступ дозволити вимкнути увімкнути дозвіл небезпечно Системна Глобальна" } } } @@ -82931,43 +82931,43 @@ "ar": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control أتمتة واجهة برمجة سطر أوامر تحكم مأخذ وكيل خطافات منافذ بيئة لوحة مفاتيح اختصار اختصارات مفتاح سريع تسجيل عام" + "value": "global hotkey overlay shortcut recorder key command option control أتمتة واجهة برمجة سطر أوامر تحكم مأخذ وكيل خطافات منافذ بيئة لوحة مفاتيح اختصار اختصارات مفتاح سريع تسجيل عام" } }, "bs": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatizacija api cli kontrola socket agent kuke portovi okruženje tastatura prečica prečice komande tipke snimač Globalno" + "value": "global hotkey overlay shortcut recorder key command option control automatizacija api cli kontrola socket agent kuke portovi okruženje tastatura prečica prečice komande tipke snimač Globalno" } }, "da": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatisering api cli kontrol socket agent hooks porte miljø tastatur genvej genveje kommandoer taster optager Globalt" + "value": "global hotkey overlay shortcut recorder key command option control automatisering api cli kontrol socket agent hooks porte miljø tastatur genvej genveje kommandoer taster optager Globalt" } }, "de": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatisierung api cli steuerung socket agent hooks ports umgebung tastatur kurzbefehl tastenkürzel befehle tasten rekorder" + "value": "global hotkey overlay shortcut recorder key command option control automatisierung api cli steuerung socket agent hooks ports umgebung tastatur kurzbefehl tastenkürzel befehle tasten rekorder" } }, "es": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatización api cli socket agente hooks puertos entorno teclado atajo atajos tecla rápida comandos teclas grabador" + "value": "global hotkey overlay shortcut recorder key command option control automatización api cli socket agente hooks puertos entorno teclado atajo atajos tecla rápida comandos teclas grabador" } }, "fr": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatisation api cli contrôle socket agent hooks ports environnement clavier raccourci raccourcis touche globale commandes touches enregistreur Général" + "value": "global hotkey overlay shortcut recorder key command option control automatisation api cli contrôle socket agent hooks ports environnement clavier raccourci raccourcis touche globale commandes touches enregistreur Général" } }, "it": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automazione api cli controllo socket agente hook porte ambiente tastiera scorciatoia scorciatoie tasto rapido comandi tasti registratore Globale" + "value": "global hotkey overlay shortcut recorder key command option control automazione api cli controllo socket agente hook porte ambiente tastiera scorciatoia scorciatoie tasto rapido comandi tasti registratore Globale" } }, "ko": { @@ -82985,55 +82985,55 @@ "nb": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatisering api cli kontroll socket agent hooks porter miljø tastatur snarvei snarveier hurtigtast kommandoer taster opptaker Globalt" + "value": "global hotkey overlay shortcut recorder key command option control automatisering api cli kontroll socket agent hooks porter miljø tastatur snarvei snarveier hurtigtast kommandoer taster opptaker Globalt" } }, "pl": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automatyzacja api cli sterowanie gniazdo agent haki porty środowisko klawiatura skrót skróty klawisz polecenia klawisze rejestrator Globalne" + "value": "global hotkey overlay shortcut recorder key command option control automatyzacja api cli sterowanie gniazdo agent haki porty środowisko klawiatura skrót skróty klawisz polecenia klawisze rejestrator Globalne" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control automação api cli controle socket agente hooks portas ambiente teclado atalho atalhos tecla rápida comandos teclas gravador" + "value": "global hotkey overlay shortcut recorder key command option control automação api cli controle socket agente hooks portas ambiente teclado atalho atalhos tecla rápida comandos teclas gravador" } }, "ru": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control автоматизация api cli управление сокет агент хуки порты окружение клавиатура сочетание клавиш горячая клавиша команды клавиши запись Глобальные" + "value": "global hotkey overlay shortcut recorder key command option control автоматизация api cli управление сокет агент хуки порты окружение клавиатура сочетание клавиш горячая клавиша команды клавиши запись Глобальные" } }, "th": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control อัตโนมัติ api cli ควบคุม ซ็อกเก็ต เอเจนต์ ฮุก พอร์ต สภาพแวดล้อม แป้นพิมพ์ ทางลัด ปุ่มลัด คำสั่ง คีย์ ตัวบันทึก ทั่วไป" + "value": "global hotkey overlay shortcut recorder key command option control อัตโนมัติ api cli ควบคุม ซ็อกเก็ต เอเจนต์ ฮุก พอร์ต สภาพแวดล้อม แป้นพิมพ์ ทางลัด ปุ่มลัด คำสั่ง คีย์ ตัวบันทึก ทั่วไป" } }, "tr": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control otomasyon api cli kontrol soket ajan kancalar portlar ortam klavye kısayol kısayollar tuşu komutlar tuşlar kaydedici Genel" + "value": "global hotkey overlay shortcut recorder key command option control otomasyon api cli kontrol soket ajan kancalar portlar ortam klavye kısayol kısayollar tuşu komutlar tuşlar kaydedici Genel" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control 自动化 api cli 控制 套接字 代理 钩子 端口 环境 键盘 快捷键 热键 命令 按键 录制器 全局" + "value": "global hotkey overlay shortcut recorder key command option control 自动化 api cli 控制 套接字 代理 钩子 端口 环境 键盘 快捷键 热键 命令 按键 录制器 全局" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control 自動化 api cli 控制 socket 代理程式 hook 連接埠 環境 鍵盤 快捷鍵 熱鍵 命令 按鍵 錄製器 全域" + "value": "global hotkey overlay shortcut recorder key command option control 自動化 api cli 控制 socket 代理程式 hook 連接埠 環境 鍵盤 快捷鍵 熱鍵 命令 按鍵 錄製器 全域" } }, "uk": { "stringUnit": { "state": "translated", - "value": "global hotkey shortcut recorder key command option control автоматизація api cli керування сокет агент хуки порти середовище клавіатура скорочення гарячі клавіші команди записувач Глобальна" + "value": "global hotkey overlay shortcut recorder key command option control автоматизація api cli керування сокет агент хуки порти середовище клавіатура скорочення гарячі клавіші команди записувач Глобальна" } } } diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index cf5db54cbb41..bf7659f60ae4 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -44,9 +44,8 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { return } - if NSApp.isHidden { - NSApp.unhide(nil) - } + // Keep the overlay showable while the app remains hidden; calling unhide restores normal windows. + panel.canHide = !NSApp.isHidden position(panel) panel.orderFrontRegardless() panel.makeKey() @@ -61,6 +60,7 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { func hide() { guard let panel else { return } panel.orderOut(nil) + panel.canHide = true #if DEBUG cmuxDebugLog("globalHotkey.panel.hide window=\(panel.identifier?.rawValue ?? "")") #endif From a9bea733b9c633e7066e074bff27d43e50fbf71d Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 23 May 2026 20:38:37 -0700 Subject: [PATCH 13/17] fix: initialize hotkey panel config store upfront --- .../App/GlobalHotkeyPanelContentState.swift | 16 +------- Sources/App/GlobalHotkeyPanelController.swift | 1 - .../GlobalHotkeyPanelControllerTests.swift | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/Sources/App/GlobalHotkeyPanelContentState.swift b/Sources/App/GlobalHotkeyPanelContentState.swift index 45578cf27465..fbd6925a5c08 100644 --- a/Sources/App/GlobalHotkeyPanelContentState.swift +++ b/Sources/App/GlobalHotkeyPanelContentState.swift @@ -9,7 +9,6 @@ final class GlobalHotkeyPanelContentState { let sidebarSelectionState: SidebarSelectionState let fileExplorerState: FileExplorerState let cmuxConfigStore: CmuxConfigStore - private var didScheduleConfigLoad = false init( windowId: UUID = UUID(), @@ -30,18 +29,7 @@ final class GlobalHotkeyPanelContentState { self.sidebarSelectionState = sidebarSelectionState ?? SidebarSelectionState() self.fileExplorerState = fileExplorerState ?? FileExplorerState() self.cmuxConfigStore = resolvedConfigStore - } - - func scheduleConfigLoadAfterFirstDisplay() { - guard !didScheduleConfigLoad else { return } - didScheduleConfigLoad = true - cmuxConfigStore.wireDirectoryTracking( - tabManager: tabManager, - loadsInitialConfiguration: false - ) - let cmuxConfigStore = cmuxConfigStore - DispatchQueue.main.async { [weak cmuxConfigStore] in - cmuxConfigStore?.loadAll() - } + resolvedConfigStore.wireDirectoryTracking(tabManager: resolvedTabManager) + resolvedConfigStore.loadAll() } } diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index bf7659f60ae4..cc4d61ae8f07 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -50,7 +50,6 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { panel.orderFrontRegardless() panel.makeKey() appDelegate?.setActiveMainWindow(panel) - contentState.scheduleConfigLoadAfterFirstDisplay() #if DEBUG cmuxDebugLog("globalHotkey.panel.show window=\(panel.identifier?.rawValue ?? "")") diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift index 61dbc197c3e7..5ec8350f61d2 100644 --- a/cmuxTests/GlobalHotkeyPanelControllerTests.swift +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -57,4 +57,42 @@ final class GlobalHotkeyPanelControllerTests: XCTestCase { XCTAssertTrue(AppDelegate.MainWindowContextRole.standard.isSessionRestorable) XCTAssertFalse(AppDelegate.MainWindowContextRole.globalHotkeyPanel.isSessionRestorable) } + + func testContentStateLoadsConfigurationDuringInitialization() throws { + let tempDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-hotkey-config-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: tempDirectory) + } + + let configURL = tempDirectory.appendingPathComponent("cmux.json") + let configJSON = """ + { + "ui": { + "surfaceTabBar": { + "buttons": [ + { "command": "echo hotkey" } + ] + } + } + } + """ + try configJSON.write(to: configURL, atomically: true, encoding: .utf8) + + let store = CmuxConfigStore( + globalConfigPath: configURL.path, + startFileWatchers: false + ) + XCTAssertEqual(store.configRevision, 0) + + _ = GlobalHotkeyPanelContentState( + tabManager: TabManager(autoWelcomeIfNeeded: false), + cmuxConfigStore: store + ) + + XCTAssertGreaterThan(store.configRevision, 0) + XCTAssertEqual(store.surfaceTabBarButtonSourcePath, configURL.path) + XCTAssertEqual(store.surfaceTabBarButtons.first?.terminalCommand, "echo hotkey") + } } From 556fe173d2f2635256f07f98039cce7bdfe91658 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 00:52:03 -0700 Subject: [PATCH 14/17] fix: restore active window after hotkey panel hides --- Sources/App/GlobalHotkeyPanelController.swift | 1 + Sources/AppDelegate.swift | 42 +++++++++++++++ Sources/CmuxConfig.swift | 13 ++--- .../GlobalHotkeyPanelControllerTests.swift | 53 +++++++++++++++++++ 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/Sources/App/GlobalHotkeyPanelController.swift b/Sources/App/GlobalHotkeyPanelController.swift index cc4d61ae8f07..c2b16ae4876f 100644 --- a/Sources/App/GlobalHotkeyPanelController.swift +++ b/Sources/App/GlobalHotkeyPanelController.swift @@ -60,6 +60,7 @@ final class GlobalHotkeyPanelController: NSObject, NSWindowDelegate { guard let panel else { return } panel.orderOut(nil) panel.canHide = true + appDelegate?.restoreActiveMainWindowAfterHiding(panel) #if DEBUG cmuxDebugLog("globalHotkey.panel.hide window=\(panel.identifier?.rawValue ?? "")") #endif diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index c5710b33cd6b..32c4eb8ad685 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -14784,6 +14784,48 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #endif } + func restoreActiveMainWindowAfterHiding(_ hiddenWindow: NSWindow) { + guard let hiddenContext = contextForMainTerminalWindow(hiddenWindow, reindex: false), + tabManager === hiddenContext.tabManager else { return } + + let nextContext = visibleSessionRestorableMainWindowContext(excluding: hiddenContext, hiddenWindow: hiddenWindow) + activateMainWindowContext(nextContext) +#if DEBUG + cmuxDebugLog( + "mainWindow.active.restoreAfterHide hidden={\(debugWindowToken(hiddenWindow))} next={\(debugContextToken(nextContext))} \(debugShortcutRouteSnapshot())" + ) +#endif + } + + private func visibleSessionRestorableMainWindowContext( + excluding excludedContext: MainWindowContext, + hiddenWindow: NSWindow + ) -> MainWindowContext? { + func candidate(from window: NSWindow?) -> MainWindowContext? { + guard let window, window !== hiddenWindow, window.isVisible else { return nil } + guard let context = contextForMainTerminalWindow(window), + context !== excludedContext, + context.role.isSessionRestorable else { return nil } + return context + } + + if let context = candidate(from: NSApp.keyWindow) { + return context + } + if let context = candidate(from: NSApp.mainWindow) { + return context + } + for window in NSApp.orderedWindows { + if let context = candidate(from: window) { + return context + } + } + return mainWindowContexts.values.first { context in + guard context !== excludedContext, context.role.isSessionRestorable else { return false } + return resolvedWindow(for: context)?.isVisible == true + } + } + private func handleMainTerminalWindowShouldClose() -> Bool { // XCTest has no UI for the warn-before-quit dialog and would either block // on runModal or have NSApp.terminate kill the test process. diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift index a1c6cc14e0e8..0a12392f246a 100644 --- a/Sources/CmuxConfig.swift +++ b/Sources/CmuxConfig.swift @@ -1972,7 +1972,7 @@ final class CmuxConfigStore: ObservableObject { // MARK: - Public API - func wireDirectoryTracking(tabManager: TabManager, loadsInitialConfiguration: Bool = true) { + func wireDirectoryTracking(tabManager: TabManager) { trackingCancellables.removeAll() self.tabManager = tabManager @@ -1999,10 +1999,7 @@ final class CmuxConfigStore: ObservableObject { } .store(in: &trackingCancellables) - updateLocalConfigPath( - tabManager.selectedWorkspace?.surfaceTabBarDirectory, - loadsConfiguration: loadsInitialConfiguration - ) + updateLocalConfigPath(tabManager.selectedWorkspace?.surfaceTabBarDirectory) } func notificationHooks(startingFrom directory: String?) -> [CmuxResolvedNotificationHook] { @@ -2021,7 +2018,7 @@ final class CmuxConfigStore: ObservableObject { ) } - private func updateLocalConfigPath(_ directory: String?, loadsConfiguration: Bool = true) { + private func updateLocalConfigPath(_ directory: String?) { let newPath: String? if let directory, !directory.isEmpty { localConfigSearchDirectory = directory @@ -2037,9 +2034,7 @@ final class CmuxConfigStore: ObservableObject { if fileWatchingEnabled, newPath != nil { startLocalFileWatcher() } - if loadsConfiguration { - loadAll() - } + loadAll() } private func resolvedLocalConfigPath(startingFrom directory: String) -> String { diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift index 5ec8350f61d2..c3b694dd1431 100644 --- a/cmuxTests/GlobalHotkeyPanelControllerTests.swift +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -95,4 +95,57 @@ final class GlobalHotkeyPanelControllerTests: XCTestCase { XCTAssertEqual(store.surfaceTabBarButtonSourcePath, configURL.path) XCTAssertEqual(store.surfaceTabBarButtons.first?.terminalCommand, "echo hotkey") } + + func testHidingHotkeyPanelRestoresVisibleStandardWindowContext() { + _ = NSApplication.shared + let appDelegate = AppDelegate() + let standardWindow = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + let panel = GlobalHotkeyPanel( + contentRect: NSRect(x: 20, y: 20, width: 800, height: 600), + styleMask: GlobalHotkeyPanelConfiguration.styleMask, + backing: .buffered, + defer: false + ) + defer { + standardWindow.orderOut(nil) + panel.orderOut(nil) + standardWindow.close() + panel.close() + } + + let standardManager = TabManager(autoWelcomeIfNeeded: false) + let hotkeyManager = TabManager(autoWelcomeIfNeeded: false) + appDelegate.registerMainWindow( + standardWindow, + windowId: UUID(), + tabManager: standardManager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .standard + ) + appDelegate.registerMainWindow( + panel, + windowId: UUID(), + tabManager: hotkeyManager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .globalHotkeyPanel + ) + + standardWindow.orderFront(nil) + appDelegate.setActiveMainWindow(panel) + XCTAssertTrue(appDelegate.tabManager === hotkeyManager) + + panel.orderOut(nil) + appDelegate.restoreActiveMainWindowAfterHiding(panel) + + XCTAssertTrue(appDelegate.tabManager === standardManager) + } } From b0e30df029ef95adfd3e1efb878d0f20e31168cd Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:02:03 -0700 Subject: [PATCH 15/17] fix: keep active context on restorable windows --- Sources/AppDelegate.swift | 13 +++-- .../GlobalHotkeyPanelControllerTests.swift | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 32c4eb8ad685..42c3afbff856 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5594,7 +5594,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent commandPaletteSnapshotByWindowId.removeValue(forKey: context.windowId) if tabManager === context.tabManager { - activateMainWindowContext(Array(mainWindowContexts.values).first { resolvedWindow(for: $0) != nil } ?? (allowWindowlessFallback ? mainWindowContexts.values.first : nil)) + activateMainWindowContext(firstSessionRestorableMainWindowContext(allowWindowlessFallback: allowWindowlessFallback)) } if let store = notificationStore { @@ -5610,6 +5610,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } } + private func firstSessionRestorableMainWindowContext(allowWindowlessFallback: Bool = false) -> MainWindowContext? { + mainWindowContexts.values.first { context in + context.role.isSessionRestorable && resolvedWindow(for: context) != nil + } ?? (allowWindowlessFallback ? mainWindowContexts.values.first { $0.role.isSessionRestorable } : nil) + } + #if DEBUG func unregisterMainWindowContextForTesting(windowId: UUID) { mainWindowContexts.values.filter { $0.windowId == windowId }.forEach { discardOrphanedMainWindowContext($0, allowWindowlessFallback: true) } @@ -14872,10 +14878,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // Repoint "active" pointers to any remaining main terminal window. let nextContext: MainWindowContext? = { if let keyWindow = NSApp.keyWindow, - let ctx = contextForMainTerminalWindow(keyWindow, reindex: false) { + let ctx = contextForMainTerminalWindow(keyWindow, reindex: false), + ctx.role.isSessionRestorable { return ctx } - return mainWindowContexts.values.first + return firstSessionRestorableMainWindowContext(allowWindowlessFallback: true) }() activateMainWindowContext(nextContext) diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift index c3b694dd1431..6b0eb02abb33 100644 --- a/cmuxTests/GlobalHotkeyPanelControllerTests.swift +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -148,4 +148,57 @@ final class GlobalHotkeyPanelControllerTests: XCTestCase { XCTAssertTrue(appDelegate.tabManager === standardManager) } + + func testDiscardingActiveStandardContextDoesNotFallBackToHotkeyPanel() { + _ = NSApplication.shared + let appDelegate = AppDelegate() + let standardWindow = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + let panel = GlobalHotkeyPanel( + contentRect: NSRect(x: 20, y: 20, width: 800, height: 600), + styleMask: GlobalHotkeyPanelConfiguration.styleMask, + backing: .buffered, + defer: false + ) + defer { + standardWindow.orderOut(nil) + panel.orderOut(nil) + standardWindow.close() + panel.close() + } + + let standardWindowId = UUID() + let standardManager = TabManager(autoWelcomeIfNeeded: false) + let hotkeyManager = TabManager(autoWelcomeIfNeeded: false) + appDelegate.registerMainWindow( + standardWindow, + windowId: standardWindowId, + tabManager: standardManager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .standard + ) + appDelegate.registerMainWindow( + panel, + windowId: UUID(), + tabManager: hotkeyManager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .globalHotkeyPanel + ) + + appDelegate.setActiveMainWindow(standardWindow) + XCTAssertTrue(appDelegate.tabManager === standardManager) + + appDelegate.unregisterMainWindowContextForTesting(windowId: standardWindowId) + + XCTAssertNil(appDelegate.tabManager) + XCTAssertFalse(appDelegate.tabManager === hotkeyManager) + } } From f815f8ddedde3a568e6b68051c13d579a5d229ed Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:24:07 -0700 Subject: [PATCH 16/17] fix: ignore hotkey panel for close warning --- Sources/AppDelegate.swift | 30 +++++++-- .../GlobalHotkeyPanelControllerTests.swift | 65 +++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 42c3afbff856..a7b652644699 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5611,11 +5611,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func firstSessionRestorableMainWindowContext(allowWindowlessFallback: Bool = false) -> MainWindowContext? { - mainWindowContexts.values.first { context in + Array(mainWindowContexts.values).first { context in context.role.isSessionRestorable && resolvedWindow(for: context) != nil } ?? (allowWindowlessFallback ? mainWindowContexts.values.first { $0.role.isSessionRestorable } : nil) } + private var liveSessionRestorableMainWindowContextCount: Int { + Array(mainWindowContexts.values).reduce(0) { count, context in + guard context.role.isSessionRestorable, resolvedWindow(for: context) != nil else { return count } + return count + 1 + } + } + #if DEBUG func unregisterMainWindowContextForTesting(windowId: UUID) { mainWindowContexts.values.filter { $0.windowId == windowId }.forEach { discardOrphanedMainWindowContext($0, allowWindowlessFallback: true) } @@ -14833,14 +14840,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func handleMainTerminalWindowShouldClose() -> Bool { - // XCTest has no UI for the warn-before-quit dialog and would either block - // on runModal or have NSApp.terminate kill the test process. - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { return true } - guard !isTerminatingApp, mainWindowContexts.count <= 1 else { return true } + guard shouldPromptBeforeClosingMainTerminalWindow() else { return true } _ = handleQuitShortcutWarning() return false } + private func shouldPromptBeforeClosingMainTerminalWindow( + isRunningUnderXCTest: Bool = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + ) -> Bool { + // XCTest has no UI for the warn-before-quit dialog and would either block + // on runModal or have NSApp.terminate kill the test process. + guard !isRunningUnderXCTest else { return false } + guard !isTerminatingApp else { return false } + return liveSessionRestorableMainWindowContextCount <= 1 + } + +#if DEBUG + func shouldPromptBeforeClosingMainTerminalWindowForTesting(isRunningUnderXCTest: Bool = false) -> Bool { + shouldPromptBeforeClosingMainTerminalWindow(isRunningUnderXCTest: isRunningUnderXCTest) + } +#endif + private func unregisterMainWindow(_ window: NSWindow) { // Reset cascade point so the next new window appears near the closing // window's position, matching upstream Ghostty behavior. diff --git a/cmuxTests/GlobalHotkeyPanelControllerTests.swift b/cmuxTests/GlobalHotkeyPanelControllerTests.swift index 6b0eb02abb33..50632a76c461 100644 --- a/cmuxTests/GlobalHotkeyPanelControllerTests.swift +++ b/cmuxTests/GlobalHotkeyPanelControllerTests.swift @@ -201,4 +201,69 @@ final class GlobalHotkeyPanelControllerTests: XCTestCase { XCTAssertNil(appDelegate.tabManager) XCTAssertFalse(appDelegate.tabManager === hotkeyManager) } + + func testCloseWarningCountsOnlySessionRestorableMainWindowContexts() { + _ = NSApplication.shared + let appDelegate = AppDelegate() + let firstStandardWindow = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + let secondStandardWindow = NSWindow( + contentRect: NSRect(x: 40, y: 40, width: 800, height: 600), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + let panel = GlobalHotkeyPanel( + contentRect: NSRect(x: 20, y: 20, width: 800, height: 600), + styleMask: GlobalHotkeyPanelConfiguration.styleMask, + backing: .buffered, + defer: false + ) + defer { + firstStandardWindow.orderOut(nil) + secondStandardWindow.orderOut(nil) + panel.orderOut(nil) + firstStandardWindow.close() + secondStandardWindow.close() + panel.close() + } + + appDelegate.registerMainWindow( + firstStandardWindow, + windowId: UUID(), + tabManager: TabManager(autoWelcomeIfNeeded: false), + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .standard + ) + appDelegate.registerMainWindow( + panel, + windowId: UUID(), + tabManager: TabManager(autoWelcomeIfNeeded: false), + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .globalHotkeyPanel + ) + + XCTAssertTrue(appDelegate.shouldPromptBeforeClosingMainTerminalWindowForTesting()) + XCTAssertFalse(appDelegate.shouldPromptBeforeClosingMainTerminalWindowForTesting(isRunningUnderXCTest: true)) + + appDelegate.registerMainWindow( + secondStandardWindow, + windowId: UUID(), + tabManager: TabManager(autoWelcomeIfNeeded: false), + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState(), + role: .standard + ) + + XCTAssertFalse(appDelegate.shouldPromptBeforeClosingMainTerminalWindowForTesting()) + } } From 0738d270a3f7783525841ca0cea8c694aae4a809 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 02:32:30 -0700 Subject: [PATCH 17/17] fix: normalize project file --- cmux.xcodeproj/project.pbxproj | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 2941c772a8d1..8b1e40923ba0 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -315,15 +315,15 @@ D7AB34400000000000000003 /* GhosttyTerminalViewVisibilityPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */; }; D3284101A1B2C3D4E5F60718 /* GhosttyTextInputSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3284102A1B2C3D4E5F60718 /* GhosttyTextInputSupport.swift */; }; 617C70C1617C70C1617C70C1 /* GitPollClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617C70C2617C70C2617C70C2 /* GitPollClock.swift */; }; + 2758B0012758B0012758B001 /* GlobalHotkeyPanelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */; }; + 2758B0052758B0052758B005 /* GlobalHotkeyPanelContentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */; }; + 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */; }; + 2758A0032758A0032758A003 /* GlobalHotkeyPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */; }; + 2758B0032758B0032758B003 /* GlobalHotkeyPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */; }; 3865A0023865A0023865A002 /* GlobalSearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865B0023865B0023865B002 /* GlobalSearchCoordinator.swift */; }; 3865A0073865A0073865A007 /* GlobalSearchDocuments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865B0073865B0073865B007 /* GlobalSearchDocuments.swift */; }; 3865A0083865A0083865A008 /* GlobalSearchPanelCaptureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865B0083865B0083865B008 /* GlobalSearchPanelCaptureManager.swift */; }; 3865A0053865A0053865A005 /* GlobalSearchShortcutSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865B0053865B0053865B005 /* GlobalSearchShortcutSettingsTests.swift */; }; - 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */; }; - 2758A0032758A0032758A003 /* GlobalHotkeyPanelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */; }; - 2758B0012758B0012758B001 /* GlobalHotkeyPanelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */; }; - 2758B0032758B0032758B003 /* GlobalHotkeyPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */; }; - 2758B0052758B0052758B005 /* GlobalHotkeyPanelContentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */; }; C1ADE10002A1B2C3D4E5F719 /* grok in Copy CLI */ = {isa = PBXBuildFile; fileRef = C1ADE10001A1B2C3D4E5F719 /* grok */; }; C0DE34020000000000000005 /* HelpMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000006 /* HelpMenuUITests.swift */; }; F5320000A1B2C3D4E5F60718 /* HermesAgentIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5320001A1B2C3D4E5F60718 /* HermesAgentIndex.swift */; }; @@ -999,15 +999,15 @@ D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalViewVisibilityPolicyTests.swift; sourceTree = ""; }; D3284102A1B2C3D4E5F60718 /* GhosttyTextInputSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTextInputSupport.swift; sourceTree = ""; }; 617C70C2617C70C2617C70C2 /* GitPollClock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitPollClock.swift; sourceTree = ""; }; + 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelConfiguration.swift; sourceTree = ""; }; + 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelContentState.swift; sourceTree = ""; }; + 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelController.swift; sourceTree = ""; }; + 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalHotkeyPanelControllerTests.swift; sourceTree = ""; }; + 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelLayout.swift; sourceTree = ""; }; 3865B0023865B0023865B002 /* GlobalSearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search/GlobalSearchCoordinator.swift; sourceTree = ""; }; 3865B0073865B0073865B007 /* GlobalSearchDocuments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search/GlobalSearchDocuments.swift; sourceTree = ""; }; 3865B0083865B0083865B008 /* GlobalSearchPanelCaptureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search/GlobalSearchPanelCaptureManager.swift; sourceTree = ""; }; 3865B0053865B0053865B005 /* GlobalSearchShortcutSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchShortcutSettingsTests.swift; sourceTree = ""; }; - 2758A0022758A0022758A002 /* GlobalHotkeyPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelController.swift; sourceTree = ""; }; - 2758A0042758A0042758A004 /* GlobalHotkeyPanelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalHotkeyPanelControllerTests.swift; sourceTree = ""; }; - 2758B0022758B0022758B002 /* GlobalHotkeyPanelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelConfiguration.swift; sourceTree = ""; }; - 2758B0042758B0042758B004 /* GlobalHotkeyPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelLayout.swift; sourceTree = ""; }; - 2758B0062758B0062758B006 /* GlobalHotkeyPanelContentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/GlobalHotkeyPanelContentState.swift; sourceTree = ""; }; C1ADE10001A1B2C3D4E5F719 /* grok */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Resources/bin/grok; sourceTree = SOURCE_ROOT; }; C0DE34020000000000000006 /* HelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpMenuUITests.swift; sourceTree = ""; }; F5320001A1B2C3D4E5F60718 /* HermesAgentIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HermesAgentIndex.swift; sourceTree = ""; }; @@ -2619,13 +2619,13 @@ D0B1000AA1B2C3D4E5F60001 /* GhosttyTerminalViewSupport.swift in Sources */, D3284101A1B2C3D4E5F60718 /* GhosttyTextInputSupport.swift in Sources */, 617C70C1617C70C1617C70C1 /* GitPollClock.swift in Sources */, + 2758B0012758B0012758B001 /* GlobalHotkeyPanelConfiguration.swift in Sources */, + 2758B0052758B0052758B005 /* GlobalHotkeyPanelContentState.swift in Sources */, + 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */, + 2758B0032758B0032758B003 /* GlobalHotkeyPanelLayout.swift in Sources */, 3865A0023865A0023865A002 /* GlobalSearchCoordinator.swift in Sources */, 3865A0073865A0073865A007 /* GlobalSearchDocuments.swift in Sources */, 3865A0083865A0083865A008 /* GlobalSearchPanelCaptureManager.swift in Sources */, - 2758A0012758A0012758A001 /* GlobalHotkeyPanelController.swift in Sources */, - 2758B0052758B0052758B005 /* GlobalHotkeyPanelContentState.swift in Sources */, - 2758B0012758B0012758B001 /* GlobalHotkeyPanelConfiguration.swift in Sources */, - 2758B0032758B0032758B003 /* GlobalHotkeyPanelLayout.swift in Sources */, F5320000A1B2C3D4E5F60718 /* HermesAgentIndex.swift in Sources */, CD0CFE6000000000CD0CFE60 /* HostAccountFlow.swift in Sources */, CD0CFE6200000000CD0CFE62 /* HostSettingsActions.swift in Sources */, @@ -2999,8 +2999,8 @@ 3069F1D10000000000000003 /* GhosttyPasteboardFidelityTests.swift in Sources */, C13519000000000000000003 /* GhosttyTerminalStartupEnvironmentTests.swift in Sources */, D7AB34400000000000000003 /* GhosttyTerminalViewVisibilityPolicyTests.swift in Sources */, - 3865A0053865A0053865A005 /* GlobalSearchShortcutSettingsTests.swift in Sources */, 2758A0032758A0032758A003 /* GlobalHotkeyPanelControllerTests.swift in Sources */, + 3865A0053865A0053865A005 /* GlobalSearchShortcutSettingsTests.swift in Sources */, F1C1AA21B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift in Sources */, C34670020000000000000001 /* KeyboardShortcutContextTests.swift in Sources */, E3309A0B /* KeyboardShortcutSettingsEqualizeSplitsTests.swift in Sources */,