Yep. AI rewrote the whole thing.

This commit is contained in:
2026-03-13 03:24:24 +11:00
parent e4719cb9f4
commit fe6c7d8c12
47 changed files with 5348 additions and 1182 deletions

View File

@@ -1,11 +1,13 @@
import AppKit
import Carbon.HIToolbox
import Combine
/// Manages global and local hotkeys.
///
/// The toggle hotkey uses Carbon's `RegisterEventHotKey` which works
/// system-wide without Accessibility permission. Tab-level hotkeys
/// use a local `NSEvent` monitor (only fires when our app is active).
@MainActor
class HotkeyManager {
static let shared = HotkeyManager()
@@ -27,37 +29,35 @@ class HotkeyManager {
private var hotKeyRef: EventHotKeyRef?
private var eventHandlerRef: EventHandlerRef?
private var localMonitor: Any?
private var defaultsObserver: NSObjectProtocol?
private let settingsProvider: TerminalSessionConfigurationProviding
private var settingsCancellable: AnyCancellable?
private init() {}
init(settingsProvider: TerminalSessionConfigurationProviding? = nil) {
self.settingsProvider = settingsProvider ?? AppSettingsController.shared
}
// MARK: - Resolved bindings (live from UserDefaults)
// MARK: - Resolved bindings from typed runtime settings
private var toggleBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyToggle) ?? .cmdReturn
settingsProvider.hotkeySettings.toggle
}
private var newTabBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyNewTab) ?? .cmdT
settingsProvider.hotkeySettings.newTab
}
private var closeTabBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyCloseTab) ?? .cmdW
settingsProvider.hotkeySettings.closeTab
}
private var nextTabBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyNextTab) ?? .cmdShiftRB
settingsProvider.hotkeySettings.nextTab
}
private var prevTabBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyPreviousTab) ?? .cmdShiftLB
settingsProvider.hotkeySettings.previousTab
}
private var detachBinding: HotkeyBinding {
binding(for: NotchSettings.Keys.hotkeyDetachTab) ?? .cmdD
settingsProvider.hotkeySettings.detachTab
}
private var sizePresets: [TerminalSizePreset] {
TerminalSizePresetStore.load()
}
private func binding(for key: String) -> HotkeyBinding? {
guard let json = UserDefaults.standard.string(forKey: key) else { return nil }
return HotkeyBinding.fromJSON(json)
settingsProvider.terminalSizePresets
}
// MARK: - Start / Stop
@@ -73,10 +73,7 @@ class HotkeyManager {
unregisterToggleHotkey()
removeCarbonHandler()
removeLocalMonitor()
if let obs = defaultsObserver {
NotificationCenter.default.removeObserver(obs)
defaultsObserver = nil
}
settingsCancellable = nil
}
// MARK: - Carbon global hotkey (toggle)
@@ -130,7 +127,7 @@ class HotkeyManager {
let binding = toggleBinding
let carbonMods = carbonModifiers(from: binding.modifiers)
var hotKeyID = EventHotKeyID(
let hotKeyID = EventHotKeyID(
signature: OSType(0x444E5452), // "DNTR"
id: 1
)
@@ -163,15 +160,17 @@ class HotkeyManager {
}
}
/// Re-register the toggle hotkey whenever the user changes it in settings.
/// Re-register the toggle hotkey whenever the typed settings change.
private func observeToggleHotkeyChanges() {
defaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.registerToggleHotkey()
}
guard let settingsProvider = settingsProvider as? AppSettingsController else { return }
settingsCancellable = settingsProvider.$settings
.map(\.hotkeys.toggle)
.removeDuplicates()
.dropFirst()
.sink { [weak self] _ in
self?.registerToggleHotkey()
}
}
// MARK: - Local monitor (tab-level hotkeys, only when our app is active)

View File

@@ -1,32 +1,29 @@
import AppKit
import SwiftUI
import Combine
import SwiftUI
/// Manages one NotchWindow per connected display.
/// Routes all open/close through centralized methods that handle
/// window activation, key status, and first responder assignment
/// so the terminal can receive keyboard input.
/// Coordinates screen/workspace state with notch lifecycle and
/// delegates raw window work to `WindowCoordinator`.
@MainActor
class ScreenManager: ObservableObject {
final class ScreenManager: ObservableObject {
static let shared = ScreenManager()
private let focusRetryDelay: TimeInterval = 0.01
private let presetResizeFrameInterval: TimeInterval = 1.0 / 60.0
private(set) var windows: [String: NotchWindow] = [:]
private(set) var viewModels: [String: NotchViewModel] = [:]
private var presetResizeTimers: [String: Timer] = [:]
@AppStorage(NotchSettings.Keys.showOnAllDisplays)
private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays
private let screenRegistry = ScreenRegistry.shared
private let windowCoordinator = WindowCoordinator()
private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self)
private var cancellables = Set<AnyCancellable>()
private init() {}
private var showOnAllDisplays: Bool {
AppSettingsController.shared.settings.display.showOnAllDisplays
}
// MARK: - Lifecycle
func start() {
screenRegistry.refreshConnectedScreens()
observeScreenChanges()
rebuildWindows()
setupHotkeys()
@@ -41,94 +38,54 @@ class ScreenManager: ObservableObject {
// MARK: - Hotkey wiring
private func setupHotkeys() {
let hk = HotkeyManager.shared
let tm = TerminalManager.shared
let hotkeyManager = HotkeyManager.shared
// Callbacks are invoked on the main thread by HotkeyManager.
// MainActor.assumeIsolated lets us safely call @MainActor methods.
hk.onToggle = { [weak self] in
MainActor.assumeIsolated { self?.toggleNotchOnActiveScreen() }
hotkeyManager.onToggle = { [weak self] in
MainActor.assumeIsolated { self?.orchestrator.toggleOnActiveScreen() }
}
hk.onNewTab = { MainActor.assumeIsolated { tm.newTab() } }
hk.onCloseTab = { MainActor.assumeIsolated { tm.closeActiveTab() } }
hk.onNextTab = { MainActor.assumeIsolated { tm.nextTab() } }
hk.onPreviousTab = { MainActor.assumeIsolated { tm.previousTab() } }
hk.onDetachTab = { [weak self] in
hotkeyManager.onNewTab = { [weak self] in
MainActor.assumeIsolated { self?.activeWorkspace().newTab() }
}
hotkeyManager.onCloseTab = { [weak self] in
MainActor.assumeIsolated { self?.activeWorkspace().closeActiveTab() }
}
hotkeyManager.onNextTab = { [weak self] in
MainActor.assumeIsolated { self?.activeWorkspace().nextTab() }
}
hotkeyManager.onPreviousTab = { [weak self] in
MainActor.assumeIsolated { self?.activeWorkspace().previousTab() }
}
hotkeyManager.onDetachTab = { [weak self] in
MainActor.assumeIsolated { self?.detachActiveTab() }
}
hk.onApplySizePreset = { [weak self] preset in
hotkeyManager.onApplySizePreset = { [weak self] preset in
MainActor.assumeIsolated { self?.applySizePreset(preset) }
}
hk.onSwitchToTab = { index in
MainActor.assumeIsolated { tm.switchToTab(at: index) }
hotkeyManager.onSwitchToTab = { [weak self] index in
MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) }
}
hk.start()
hotkeyManager.start()
}
// MARK: - Toggle
func toggleNotchOnActiveScreen() {
let mouseLocation = NSEvent.mouseLocation
let targetScreen = NSScreen.screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }
?? NSScreen.main
guard let screen = targetScreen else { return }
let uuid = screen.displayUUID
// Close any other open notch first
for (otherUUID, otherVM) in viewModels where otherUUID != uuid {
if otherVM.notchState == .open {
closeNotch(screenUUID: otherUUID)
}
}
if let vm = viewModels[uuid] {
if vm.notchState == .open {
closeNotch(screenUUID: uuid)
} else {
openNotch(screenUUID: uuid)
}
}
orchestrator.toggleOnActiveScreen()
}
// MARK: - Open / Close
func openNotch(screenUUID: String) {
guard let vm = viewModels[screenUUID],
let window = windows[screenUUID] else { return }
vm.cancelCloseTransition()
withAnimation(vm.openAnimation) {
vm.open()
}
window.isNotchOpen = true
HotkeyManager.shared.isNotchOpen = true
// Activate the app so the window can become key.
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
focusActiveTerminal(in: screenUUID)
func openNotch(screenID: ScreenID) {
orchestrator.open(screenID: screenID)
}
func closeNotch(screenUUID: String) {
guard let vm = viewModels[screenUUID],
let window = windows[screenUUID] else { return }
vm.beginCloseTransition()
withAnimation(vm.closeAnimation) {
vm.close()
}
window.isNotchOpen = false
HotkeyManager.shared.isNotchOpen = false
func closeNotch(screenID: ScreenID) {
orchestrator.close(screenID: screenID)
}
private func detachActiveTab() {
if let session = TerminalManager.shared.detachActiveTab() {
if let session = activeWorkspace().detachActiveTab() {
DispatchQueue.main.async {
PopoutWindowController.shared.popout(session: session)
}
@@ -136,235 +93,105 @@ class ScreenManager: ObservableObject {
}
func applySizePreset(_ preset: TerminalSizePreset) {
guard let (screenUUID, vm) = viewModels.first(where: { $0.value.notchState == .open }) else {
UserDefaults.standard.set(preset.width, forKey: NotchSettings.Keys.openWidth)
UserDefaults.standard.set(preset.height, forKey: NotchSettings.Keys.openHeight)
guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else {
AppSettingsController.shared.update {
$0.display.openWidth = preset.width
$0.display.openHeight = preset.height
}
return
}
let startSize = vm.notchSize
let targetSize = vm.setStoredOpenSize(preset.size)
animatePresetResize(for: screenUUID, from: startSize, to: targetSize, duration: vm.openAnimationDuration)
let startSize = context.notchSize
let targetSize = context.setStoredOpenSize(preset.size)
windowCoordinator.animatePresetResize(
for: context.id,
context: context,
from: startSize,
to: targetSize,
duration: context.openAnimationDuration
)
}
// MARK: - Window creation
func rebuildWindows() {
cleanupAllWindows()
screenRegistry.refreshConnectedScreens()
let screens: [NSScreen]
if showOnAllDisplays {
screens = NSScreen.screens
} else {
screens = [NSScreen.main].compactMap { $0 }
}
for screen in screens {
for screen in visibleScreens() {
createWindow(for: screen)
}
}
private func createWindow(for screen: NSScreen) {
let uuid = screen.displayUUID
let vm = NotchViewModel(screenUUID: uuid)
let initialContentSize = vm.openNotchSize
let screenID = screen.displayUUID
guard let context = screenRegistry.screenContext(for: screenID) else { return }
let window = NotchWindow(
contentRect: NSRect(origin: .zero, size: CGSize(width: initialContentSize.width + 40, height: initialContentSize.height + 20)),
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow],
backing: .buffered,
defer: false
)
context.requestOpen = { [weak self] in
self?.orchestrator.open(screenID: screenID)
}
context.requestClose = { [weak self] in
self?.orchestrator.close(screenID: screenID)
}
context.requestWindowResize = { [weak self] in
guard let self,
let context = self.screenRegistry.screenContext(for: screenID) else {
return
}
// Close the notch when the window loses focus
window.onResignKey = { [weak self] in
self?.closeNotch(screenUUID: uuid)
self.windowCoordinator.updateWindowFrame(
for: screenID,
context: context,
centerHorizontally: true
)
}
context.requestTerminalFocus = { [weak self] in
guard let self else { return }
// Wire the ViewModel callbacks so ContentView routes through us
vm.requestOpen = { [weak self] in
self?.openNotch(screenUUID: uuid)
}
vm.requestClose = { [weak self] in
self?.closeNotch(screenUUID: uuid)
}
vm.requestWindowResize = { [weak self] in
self?.updateWindowFrame(for: uuid, centerHorizontally: true)
self.windowCoordinator.focusActiveTerminal(for: screenID) { [weak self] in
self?.screenRegistry.workspaceController(for: screenID).activeTab?.terminalView
}
}
let hostingView = NSHostingView(
rootView: ContentView(vm: vm, terminalManager: TerminalManager.shared)
.preferredColorScheme(.dark)
rootView: ContentView(
screen: context,
orchestrator: orchestrator
)
.preferredColorScheme(.dark)
)
let containerView = NSView(frame: NSRect(origin: .zero, size: window.frame.size))
containerView.autoresizesSubviews = true
containerView.wantsLayer = true
containerView.layer?.backgroundColor = NSColor.clear.cgColor
hostingView.frame = containerView.bounds
hostingView.autoresizingMask = [.width, .height]
containerView.addSubview(hostingView)
window.contentView = containerView
windows[uuid] = window
viewModels[uuid] = vm
updateWindowFrame(for: uuid, centerHorizontally: true)
window.orderFrontRegardless()
windowCoordinator.createWindow(
on: screen,
context: context,
contentView: hostingView,
onResignKey: { [weak self] in
guard !context.suppressCloseOnFocusLoss else { return }
self?.orchestrator.close(screenID: screenID)
}
)
}
// MARK: - Repositioning
func repositionWindows() {
for (uuid, window) in windows {
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == uuid }) else { continue }
guard let vm = viewModels[uuid] else { continue }
screenRegistry.refreshConnectedScreens()
vm.refreshClosedSize()
updateWindowFrame(for: uuid, on: screen, window: window, centerHorizontally: true)
for context in screenRegistry.allScreens() {
context.refreshClosedSize()
windowCoordinator.repositionWindow(
for: context.id,
context: context,
centerHorizontally: true
)
}
}
private func updateWindowFrame(for screenUUID: String, centerHorizontally: Bool = false) {
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }),
let window = windows[screenUUID] else { return }
updateWindowFrame(for: screenUUID, on: screen, window: window, centerHorizontally: centerHorizontally)
}
private func updateWindowFrame(
for screenUUID: String,
on screen: NSScreen,
window: NotchWindow,
centerHorizontally: Bool = false
) {
let frame = targetWindowFrame(
for: screenUUID,
on: screen,
window: window,
centerHorizontally: centerHorizontally,
contentSize: nil
)
guard !window.frame.equalTo(frame) else { return }
window.setFrame(frame, display: false)
}
private func targetWindowFrame(
for screenUUID: String,
on screen: NSScreen,
window: NotchWindow,
centerHorizontally: Bool,
contentSize: CGSize?
) -> NSRect {
guard let vm = viewModels[screenUUID] else { return window.frame }
let shadowPadding: CGFloat = 20
let openSize = contentSize ?? vm.openNotchSize
let windowWidth = openSize.width + 40
let windowHeight = openSize.height + shadowPadding
let centeredX = screen.frame.origin.x + (screen.frame.width - windowWidth) / 2
let x: CGFloat = centerHorizontally || vm.notchState == .closed
? centeredX
: min(max(window.frame.minX, screen.frame.minX), screen.frame.maxX - windowWidth)
return NSRect(
x: x,
y: screen.frame.origin.y + screen.frame.height - windowHeight,
width: windowWidth,
height: windowHeight
)
}
private func animatePresetResize(
for screenUUID: String,
from startSize: CGSize,
to targetSize: CGSize,
duration: TimeInterval
) {
cancelPresetResize(for: screenUUID)
guard let vm = viewModels[screenUUID] else { return }
guard startSize != targetSize else {
vm.notchSize = targetSize
updateWindowFrame(for: screenUUID, centerHorizontally: true)
return
}
vm.isPresetResizing = true
let startTime = CACurrentMediaTime()
let duration = max(duration, presetResizeFrameInterval)
let timer = Timer(timeInterval: presetResizeFrameInterval, repeats: true) { [weak self] timer in
MainActor.assumeIsolated {
guard let self, let vm = self.viewModels[screenUUID] else {
timer.invalidate()
return
}
let elapsed = CACurrentMediaTime() - startTime
let progress = min(1, elapsed / duration)
let easedProgress = 0.5 - (cos(.pi * progress) / 2)
let size = CGSize(
width: startSize.width + ((targetSize.width - startSize.width) * easedProgress),
height: startSize.height + ((targetSize.height - startSize.height) * easedProgress)
)
vm.notchSize = size
self.updateWindowFrame(for: screenUUID, contentSize: size, centerHorizontally: true)
if progress >= 1 {
vm.notchSize = targetSize
vm.isPresetResizing = false
self.updateWindowFrame(for: screenUUID, contentSize: targetSize, centerHorizontally: true)
self.presetResizeTimers[screenUUID] = nil
timer.invalidate()
}
}
}
presetResizeTimers[screenUUID] = timer
RunLoop.main.add(timer, forMode: .common)
timer.fire()
}
private func cancelPresetResize(for screenUUID: String) {
presetResizeTimers[screenUUID]?.invalidate()
presetResizeTimers[screenUUID] = nil
viewModels[screenUUID]?.isPresetResizing = false
}
private func updateWindowFrame(
for screenUUID: String,
contentSize: CGSize,
centerHorizontally: Bool = false
) {
guard let screen = NSScreen.screens.first(where: { $0.displayUUID == screenUUID }),
let window = windows[screenUUID] else { return }
let frame = targetWindowFrame(
for: screenUUID,
on: screen,
window: window,
centerHorizontally: centerHorizontally,
contentSize: contentSize
)
guard !window.frame.equalTo(frame) else { return }
window.setFrame(frame, display: false)
}
// MARK: - Cleanup
private func cleanupAllWindows() {
for (_, timer) in presetResizeTimers {
timer.invalidate()
}
presetResizeTimers.removeAll()
for (_, window) in windows {
window.orderOut(nil)
window.close()
}
windows.removeAll()
viewModels.removeAll()
orchestrator.cancelAllPendingWork()
windowCoordinator.cleanupAllWindows()
}
// MARK: - Screen observation
@@ -372,33 +199,62 @@ class ScreenManager: ObservableObject {
private func observeScreenChanges() {
NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification)
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.sink { [weak self] _ in self?.handleScreenConfigurationChange() }
.sink { [weak self] _ in
self?.handleScreenConfigurationChange()
}
.store(in: &cancellables)
}
private func handleScreenConfigurationChange() {
let currentUUIDs = Set(NSScreen.screens.map { $0.displayUUID })
let knownUUIDs = Set(windows.keys)
if currentUUIDs != knownUUIDs {
screenRegistry.refreshConnectedScreens()
let currentScreenIDs = Set(visibleScreens().map(\.displayUUID))
let knownScreenIDs = windowCoordinator.windowScreenIDs()
if currentScreenIDs != knownScreenIDs {
rebuildWindows()
} else {
repositionWindows()
}
}
private func focusActiveTerminal(in screenUUID: String, attemptsRemaining: Int = 12) {
guard let window = windows[screenUUID],
let terminalView = TerminalManager.shared.activeTab?.terminalView else { return }
private func activeWorkspace() -> WorkspaceController {
guard let screenID = screenRegistry.activeScreenID() else {
return WorkspaceRegistry.shared.defaultWorkspaceController
}
if terminalView.window === window {
window.makeFirstResponder(terminalView)
return screenRegistry.workspaceController(for: screenID)
}
private func visibleScreens() -> [NSScreen] {
if showOnAllDisplays {
return NSScreen.screens
}
return [NSScreen.main].compactMap { $0 }
}
}
extension ScreenManager: NotchPresentationHost {
func canPresentNotch(for screenID: ScreenID) -> Bool {
windowCoordinator.hasWindow(for: screenID)
}
func performOpenPresentation(for screenID: ScreenID) {
guard screenRegistry.screenContext(for: screenID) != nil else {
return
}
guard attemptsRemaining > 0 else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + focusRetryDelay) { [weak self] in
self?.focusActiveTerminal(in: screenUUID, attemptsRemaining: attemptsRemaining - 1)
windowCoordinator.presentOpen(for: screenID) { [weak self] in
self?.screenRegistry.workspaceController(for: screenID).activeTab?.terminalView
}
}
func performClosePresentation(for screenID: ScreenID) {
guard screenRegistry.screenContext(for: screenID) != nil else {
return
}
windowCoordinator.presentClose(for: screenID)
}
}

View File

@@ -34,6 +34,7 @@ class SettingsWindowController: NSObject, NSWindowDelegate {
defer: false
)
win.title = "CommandNotch Settings"
win.identifier = NSUserInterfaceItemIdentifier("settings.window")
win.contentView = hostingView
win.center()
win.delegate = self

View File

@@ -0,0 +1,291 @@
import AppKit
import QuartzCore
import SwiftUI
struct WindowFrameCalculator {
static let horizontalPadding: CGFloat = 40
static let verticalPadding: CGFloat = 20
static func targetFrame(
screenFrame: CGRect,
currentWindowFrame: CGRect,
notchState: NotchState,
contentSize: CGSize,
centerHorizontally: Bool
) -> CGRect {
let windowWidth = contentSize.width + horizontalPadding
let windowHeight = contentSize.height + verticalPadding
let centeredX = screenFrame.origin.x + ((screenFrame.width - windowWidth) / 2)
let x: CGFloat
if centerHorizontally || notchState == .closed {
x = centeredX
} else {
x = min(
max(currentWindowFrame.minX, screenFrame.minX),
screenFrame.maxX - windowWidth
)
}
return CGRect(
x: x,
y: screenFrame.origin.y + screenFrame.height - windowHeight,
width: windowWidth,
height: windowHeight
)
}
}
@MainActor
final class WindowCoordinator {
private let focusRetryDelay: TimeInterval
private let presetResizeFrameInterval: TimeInterval
private let screenLookup: @MainActor (ScreenID) -> NSScreen?
private let applicationActivator: @MainActor () -> Void
private let hotkeyOpenStateHandler: @MainActor (Bool) -> Void
private(set) var windows: [ScreenID: NotchWindow] = [:]
private var presetResizeTimers: [ScreenID: Timer] = [:]
init(
focusRetryDelay: TimeInterval = 0.01,
presetResizeFrameInterval: TimeInterval = 1.0 / 60.0,
screenLookup: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
NSScreen.screens.first { $0.displayUUID == screenID }
},
applicationActivator: @escaping @MainActor () -> Void = {
NSApp.activate(ignoringOtherApps: true)
},
hotkeyOpenStateHandler: @escaping @MainActor (Bool) -> Void = { isOpen in
HotkeyManager.shared.isNotchOpen = isOpen
}
) {
self.focusRetryDelay = focusRetryDelay
self.presetResizeFrameInterval = presetResizeFrameInterval
self.screenLookup = screenLookup
self.applicationActivator = applicationActivator
self.hotkeyOpenStateHandler = hotkeyOpenStateHandler
}
func hasWindow(for screenID: ScreenID) -> Bool {
windows[screenID] != nil
}
func windowScreenIDs() -> Set<ScreenID> {
Set(windows.keys)
}
func createWindow(
on screen: NSScreen,
context: ScreenContext,
contentView: NSView,
onResignKey: @escaping () -> Void
) {
let initialFrame = WindowFrameCalculator.targetFrame(
screenFrame: screen.frame,
currentWindowFrame: .zero,
notchState: context.notchState,
contentSize: context.openNotchSize,
centerHorizontally: true
)
let window = NotchWindow(
contentRect: initialFrame,
styleMask: [.borderless, .nonactivatingPanel, .utilityWindow],
backing: .buffered,
defer: false
)
window.onResignKey = onResignKey
let containerView = NSView(frame: NSRect(origin: .zero, size: initialFrame.size))
containerView.autoresizesSubviews = true
containerView.wantsLayer = true
containerView.layer?.backgroundColor = NSColor.clear.cgColor
contentView.frame = containerView.bounds
contentView.autoresizingMask = [.width, .height]
containerView.addSubview(contentView)
window.contentView = containerView
windows[context.id] = window
updateWindowFrame(for: context.id, context: context, centerHorizontally: true)
window.orderFrontRegardless()
}
func repositionWindow(for screenID: ScreenID, context: ScreenContext, centerHorizontally: Bool = false) {
updateWindowFrame(for: screenID, context: context, centerHorizontally: centerHorizontally)
}
func updateWindowFrame(
for screenID: ScreenID,
context: ScreenContext,
contentSize: CGSize? = nil,
centerHorizontally: Bool = false
) {
guard let screen = screenLookup(screenID),
let window = windows[screenID] else {
return
}
let frame = WindowFrameCalculator.targetFrame(
screenFrame: screen.frame,
currentWindowFrame: window.frame,
notchState: context.notchState,
contentSize: resolvedContentSize(for: context, override: contentSize),
centerHorizontally: centerHorizontally
)
guard !window.frame.equalTo(frame) else { return }
window.setFrame(frame, display: false)
}
func animatePresetResize(
for screenID: ScreenID,
context: ScreenContext,
from startSize: CGSize,
to targetSize: CGSize,
duration: TimeInterval
) {
cancelPresetResize(for: screenID)
guard startSize != targetSize else {
context.notchSize = targetSize
updateWindowFrame(for: screenID, context: context, contentSize: targetSize, centerHorizontally: true)
return
}
context.isPresetResizing = true
let startTime = CACurrentMediaTime()
let frameInterval = max(duration, presetResizeFrameInterval)
let timer = Timer(timeInterval: presetResizeFrameInterval, repeats: true) { [weak self] timer in
MainActor.assumeIsolated {
guard let self else {
timer.invalidate()
return
}
let elapsed = CACurrentMediaTime() - startTime
let progress = min(1, elapsed / frameInterval)
let easedProgress = 0.5 - (cos(.pi * progress) / 2)
let size = CGSize(
width: startSize.width + ((targetSize.width - startSize.width) * easedProgress),
height: startSize.height + ((targetSize.height - startSize.height) * easedProgress)
)
context.notchSize = size
self.updateWindowFrame(for: screenID, context: context, contentSize: size, centerHorizontally: true)
if progress >= 1 {
context.notchSize = targetSize
context.isPresetResizing = false
self.updateWindowFrame(for: screenID, context: context, contentSize: targetSize, centerHorizontally: true)
self.presetResizeTimers[screenID] = nil
timer.invalidate()
}
}
}
presetResizeTimers[screenID] = timer
RunLoop.main.add(timer, forMode: .common)
timer.fire()
}
func presentOpen(
for screenID: ScreenID,
terminalViewProvider: @escaping @MainActor () -> NSView?
) {
guard let window = windows[screenID] else { return }
window.isNotchOpen = true
updateHotkeyOpenState()
applicationActivator()
window.makeKeyAndOrderFront(nil)
focusActiveTerminal(
in: screenID,
attemptsRemaining: 12,
terminalViewProvider: terminalViewProvider
)
}
func focusActiveTerminal(
for screenID: ScreenID,
terminalViewProvider: @escaping @MainActor () -> NSView?
) {
focusActiveTerminal(
in: screenID,
attemptsRemaining: 12,
terminalViewProvider: terminalViewProvider
)
}
func presentClose(for screenID: ScreenID) {
guard let window = windows[screenID] else { return }
window.isNotchOpen = false
updateHotkeyOpenState()
}
func cleanupAllWindows() {
for timer in presetResizeTimers.values {
timer.invalidate()
}
presetResizeTimers.removeAll()
for window in windows.values {
window.orderOut(nil)
window.close()
}
windows.removeAll()
updateHotkeyOpenState()
}
private func focusActiveTerminal(
in screenID: ScreenID,
attemptsRemaining: Int,
terminalViewProvider: @escaping @MainActor () -> NSView?
) {
guard let window = windows[screenID],
let terminalView = terminalViewProvider() else {
return
}
if terminalView.window === window {
window.makeFirstResponder(terminalView)
return
}
guard attemptsRemaining > 0 else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + focusRetryDelay) { [weak self] in
Task { @MainActor in
self?.focusActiveTerminal(
in: screenID,
attemptsRemaining: attemptsRemaining - 1,
terminalViewProvider: terminalViewProvider
)
}
}
}
private func cancelPresetResize(for screenID: ScreenID) {
presetResizeTimers[screenID]?.invalidate()
presetResizeTimers[screenID] = nil
}
private func resolvedContentSize(for context: ScreenContext, override: CGSize?) -> CGSize {
if let override {
return override
}
return context.notchState == .open ? context.notchSize : context.openNotchSize
}
private func updateHotkeyOpenState() {
hotkeyOpenStateHandler(windows.values.contains(where: \.isNotchOpen))
}
}