From 2085815ab508b6adc419e9e97dc1b48b63097a2e Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 8 Mar 2026 17:48:43 +0530 Subject: [PATCH] Feature: Terminal stay open and shortcuts --- DynamicIsland.xcodeproj/project.pbxproj | 8 ++--- DynamicIsland/ContentView.swift | 31 +++++++++++++++- DynamicIsland/DynamicIslandApp.swift | 17 +++++++++ DynamicIsland/Localizable.xcstrings | 16 +++++++++ .../Shortcuts/ShortcutConstants.swift | 1 + .../components/Settings/SettingsView.swift | 35 +++++++++++++++++++ DynamicIsland/models/Constants.swift | 1 + 7 files changed, 104 insertions(+), 5 deletions(-) diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index 173f25e..e6db8a4 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -433,7 +433,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 9Y64TRM77N; @@ -475,7 +475,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll.dev; PRODUCT_NAME = Atoll; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -505,7 +505,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 9Y64TRM77N; @@ -547,7 +547,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll; PRODUCT_NAME = Atoll; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 8797339..5c57d00 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -150,6 +150,7 @@ struct ContentView: View { @State private var lastHapticTime: Date = Date() @State private var hoverClickMonitor: Any? @State private var hoverClickLocalMonitor: Any? + @State private var stickyTerminalClickMonitor: Any? @State private var gestureProgress: CGFloat = .zero @State private var skipGestureActiveDirection: MusicManager.SkipDirection? @@ -424,6 +425,9 @@ struct ContentView: View { isHovering = false } } + if newState == .closed { + removeStickyTerminalClickMonitor() + } } .onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in runAfter(0.1) { @@ -606,6 +610,7 @@ struct ContentView: View { .onDisappear { hoverTask?.cancel() stopHoverClickMonitor() + removeStickyTerminalClickMonitor() cancelMusicControlWindowSync() hideMusicControlWindow() cancelMusicControlVisibilityTimer() @@ -1586,6 +1591,23 @@ struct ContentView: View { } } + private func installStickyTerminalClickMonitor() { + guard stickyTerminalClickMonitor == nil else { return } + stickyTerminalClickMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak vm] _ in + Task { @MainActor in + guard let vm, vm.notchState == .open else { return } + vm.close() + } + } + } + + private func removeStickyTerminalClickMonitor() { + if let stickyTerminalClickMonitor { + NSEvent.removeMonitor(stickyTerminalClickMonitor) + self.stickyTerminalClickMonitor = nil + } + } + // MARK: - Hover Management /// Handle hover state changes with debouncing @@ -1594,6 +1616,7 @@ struct ContentView: View { if hovering { startHoverClickMonitor() + removeStickyTerminalClickMonitor() } else { stopHoverClickMonitor() } @@ -1642,6 +1665,12 @@ struct ContentView: View { if self.vm.notchState == .open && !self.shouldPreventAutoClose() { self.vm.close() + } else if self.vm.notchState == .open + && Defaults[.terminalStickyMode] + && self.coordinator.currentView == .terminal { + // Terminal sticky mode kept the notch open — install a + // global click monitor so a click outside closes it. + self.installStickyTerminalClickMonitor() } } } @@ -1660,7 +1689,7 @@ struct ContentView: View { } private func shouldPreventAutoClose() -> Bool { - coordinator.firstLaunch || hasAnyActivePopovers() || vm.isAutoCloseSuppressed || SharingStateManager.shared.preventNotchClose + coordinator.firstLaunch || hasAnyActivePopovers() || vm.isAutoCloseSuppressed || SharingStateManager.shared.preventNotchClose || (Defaults[.terminalStickyMode] && coordinator.currentView == .terminal) } // Helper to prevent rapid haptic feedback diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index dbac59e..d84ee76 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -894,6 +894,22 @@ class AppDelegate: NSObject, NSApplicationDelegate { ColorPickerPanelManager.shared.toggleColorPickerPanel() } + KeyboardShortcuts.onKeyDown(for: .toggleTerminalTab) { [weak self] in + guard let self else { return } + guard Defaults[.enableShortcuts], Defaults[.enableTerminalFeature] else { return } + + if vm.notchState == .closed { + vm.open() + coordinator.currentView = .terminal + } else { + if coordinator.currentView == .terminal { + vm.close() + } else { + coordinator.currentView = .terminal + } + } + } + KeyboardShortcuts.onKeyDown(for: .screenAssistantPanel) { [weak self] in guard let self else { return } guard Defaults[.enableShortcuts], Defaults[.enableScreenAssistant] else { return } @@ -920,6 +936,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { updateShortcut(.clipboardHistoryPanel, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableClipboardManager]) updateShortcut(.colorPickerPanel, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableColorPickerFeature]) updateShortcut(.screenAssistantPanel, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableScreenAssistant]) + updateShortcut(.toggleTerminalTab, isEnabled: Defaults[.enableShortcuts] && Defaults[.enableTerminalFeature]) } @MainActor diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index f1e2a2d..a39ffa0 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -13239,6 +13239,9 @@ "Just now" : { "comment" : "A string indicating that an action was performed just recently.", "isCommentAutoGenerated" : true + }, + "Keep terminal open until clicked outside" : { + }, "Keyboard Backlight HUD" : { "localizations" : { @@ -18201,6 +18204,9 @@ } } } + }, + "Opens the terminal tab in the notch. Default is Ctrl+`. Only works when terminal feature is enabled." : { + }, "Option as Meta key" : { @@ -18648,6 +18654,9 @@ } } } + }, + "Prevents the terminal from closing when the cursor accidentally leaves the notch area." : { + }, "Preview" : { "localizations" : { @@ -23266,6 +23275,9 @@ "Terminal" : { "comment" : "The title of the settings view related to the terminal.", "isCommentAutoGenerated" : true + }, + "Terminal feature is disabled" : { + }, "Terminal is disabled" : { "comment" : "A message displayed when the terminal feature is disabled.", @@ -23849,6 +23861,10 @@ } } }, + "Toggle Terminal Tab:" : { + "comment" : "Name for a keyboard shortcut that toggles the terminal tab in the notch.", + "isCommentAutoGenerated" : true + }, "Toggle the Dynamic Island open or closed from anywhere." : { "comment" : "A description of the \"Toggle Navigation\" keyboard shortcut.", "isCommentAutoGenerated" : true, diff --git a/DynamicIsland/Shortcuts/ShortcutConstants.swift b/DynamicIsland/Shortcuts/ShortcutConstants.swift index ab7735e..67f542b 100644 --- a/DynamicIsland/Shortcuts/ShortcutConstants.swift +++ b/DynamicIsland/Shortcuts/ShortcutConstants.swift @@ -31,5 +31,6 @@ extension KeyboardShortcuts.Name { static let increaseBacklight = Self("increaseBacklight", default: .init(.f2, modifiers: [.command])) static let toggleSneakPeek = Self("toggleSneakPeek", default: .init(.h, modifiers: [.command, .shift])) static let toggleNotchOpen = Self("toggleNotchOpen", default: .init(.i, modifiers: [.command, .shift])) + static let toggleTerminalTab = Self("toggleTerminalTab", default: .init(.backtick, modifiers: [.control])) static let startDemoTimer = Self("startDemoTimer", default: .init(.t, modifiers: [.command, .shift])) } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 314c602..0a1c2f6 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -5473,6 +5473,29 @@ struct Shortcuts: View { .font(.caption) } + Section { + HStack { + VStack(alignment: .leading) { + KeyboardShortcuts.Recorder("Toggle Terminal Tab:", name: .toggleTerminalTab) + .disabled(!enableShortcuts || !Defaults[.enableTerminalFeature]) + if !Defaults[.enableTerminalFeature] { + Text("Terminal feature is disabled") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.top, 2) + } + } + Spacer() + } + } header: { + Text("Terminal") + } footer: { + Text("Opens the terminal tab in the notch. Default is Ctrl+`. Only works when terminal feature is enabled.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + Section { HStack { VStack(alignment: .leading) { @@ -7218,6 +7241,18 @@ struct TerminalSettings: View { Section { Defaults.Toggle("Enable terminal", key: .enableTerminalFeature) .settingsHighlight(id: highlightID("Enable terminal")) + + if enableTerminalFeature { + Defaults.Toggle(key: .terminalStickyMode) { + VStack(alignment: .leading, spacing: 2) { + Text("Keep terminal open until clicked outside") + Text("Prevents the terminal from closing when the cursor accidentally leaves the notch area.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .settingsHighlight(id: highlightID("Keep terminal open")) + } } header: { Text("General") } footer: { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index d2592ed..347335c 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -926,6 +926,7 @@ extension Defaults.Keys { static let terminalBackgroundColor = Key("terminalBackgroundColor", default: .black) static let terminalForegroundColor = Key("terminalForegroundColor", default: .white) static let terminalCursorColor = Key("terminalCursorColor", default: Color(.selectedControlColor)) + static let terminalStickyMode = Key("terminalStickyMode", default: false) // MARK: Timer Feature static let enableTimerFeature = Key("enableTimerFeature", default: true)