diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 1d6e6ba7..303e8960 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ A5001207 /* UpdatePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001217 /* UpdatePopoverView.swift */; }; A5001208 /* UpdateTitlebarAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001218 /* UpdateTitlebarAccessory.swift */; }; A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; }; + A5001240 /* WindowDecorationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001241 /* WindowDecorationsController.swift */; }; A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; }; A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; }; B9000002A1B2C3D4E5F60719 /* cmuxterm.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000001A1B2C3D4E5F60719 /* cmuxterm.swift */; }; @@ -119,6 +120,7 @@ A5001219 /* WindowToolbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowToolbarController.swift; sourceTree = ""; }; A5001222 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = ""; }; + A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = ""; }; 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = ""; }; C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = ""; }; A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -238,6 +240,7 @@ A5001217 /* UpdatePopoverView.swift */, A5001218 /* UpdateTitlebarAccessory.swift */, A5001219 /* WindowToolbarController.swift */, + A5001241 /* WindowDecorationsController.swift */, A5001222 /* WindowAccessor.swift */, ); path = Sources; @@ -404,6 +407,7 @@ A5001207 /* UpdatePopoverView.swift in Sources */, A5001208 /* UpdateTitlebarAccessory.swift in Sources */, A5001209 /* WindowToolbarController.swift in Sources */, + A5001240 /* WindowDecorationsController.swift in Sources */, A500120C /* WindowAccessor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -510,7 +514,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -526,7 +530,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.9.0; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -555,7 +559,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -571,7 +575,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.9.0; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -624,10 +628,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.9.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -641,10 +645,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.8.0; + MARKETING_VERSION = 1.9.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index f407c814..71615ee1 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -11,6 +11,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private var workspaceObserver: NSObjectProtocol? private let updateController = UpdateController() private lazy var titlebarAccessoryController = UpdateTitlebarAccessoryController(viewModel: updateViewModel) + private let windowDecorationsController = WindowDecorationsController() var updateViewModel: UpdateViewModel { updateController.viewModel @@ -31,6 +32,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent configureUserNotifications() updateController.startUpdater() titlebarAccessoryController.start() + windowDecorationsController.start() #if DEBUG UpdateTestSupport.applyIfNeeded(to: updateController.viewModel) if ProcessInfo.processInfo.environment["CMUX_UI_TEST_TRIGGER_UPDATE_CHECK"] == "1" { @@ -133,6 +135,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent titlebarAccessoryController.attach(to: window) } + func applyWindowDecorations(to window: NSWindow) { + windowDecorationsController.apply(to: window) + } + func toggleNotificationsPopover(animated: Bool = true) { titlebarAccessoryController.toggleNotificationsPopover(animated: animated) } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 337ef032..4cc93b23 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -146,6 +146,7 @@ struct ContentView: View { .background(WindowAccessor { window in window.identifier = NSUserInterfaceItemIdentifier("cmux.main") AppDelegate.shared?.attachUpdateAccessory(to: window) + AppDelegate.shared?.applyWindowDecorations(to: window) }) } diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 771af9f5..c9b30137 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -176,7 +176,7 @@ class GhosttyApp { GhosttyPasteboardHelper.writeString(fallback, to: location) } } - runtimeConfig.close_surface_cb = { userdata, processAlive in + runtimeConfig.close_surface_cb = { userdata, _ in guard let userdata else { return } let surfaceView = Unmanaged.fromOpaque(userdata).takeUnretainedValue() guard let tabId = surfaceView.tabId, @@ -185,10 +185,15 @@ class GhosttyApp { } DispatchQueue.main.async { - _ = AppDelegate.shared?.tabManager?.closeSurface( - tabId: tabId, - surfaceId: surfaceId - ) + if let surface = surfaceView.terminalSurface, + surface.needsConfirmClose() { + AppDelegate.shared?.tabManager?.closePanelWithConfirmation( + tabId: tabId, + surfaceId: surfaceId + ) + return + } + _ = AppDelegate.shared?.tabManager?.closeSurface(tabId: tabId, surfaceId: surfaceId) } } diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 4f2ff84f..f31afccf 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -373,20 +373,7 @@ class TabManager: ObservableObject { guard let selectedId = selectedTabId, let tab = tabs.first(where: { $0.id == selectedId }), let focusedSurfaceId = tab.focusedSurfaceId else { return } - guard tab.splitTree.isSplit else { - closeTabIfRunningProcess(tab) - return - } - - let focusedSurface = tab.surface(for: focusedSurfaceId) - if focusedSurface?.needsConfirmClose() == true { - guard confirmClose( - title: "Close panel?", - message: "This will close the current split panel in this tab." - ) else { return } - } - - _ = closeSurface(tabId: selectedId, surfaceId: focusedSurfaceId) + closePanelWithConfirmation(tab: tab, surfaceId: focusedSurfaceId) } func closeCurrentTabWithConfirmation() { @@ -421,6 +408,28 @@ class TabManager: ObservableObject { closeTab(tab) } + private func closePanelWithConfirmation(tab: Tab, surfaceId: UUID) { + guard tab.splitTree.isSplit else { + closeTabIfRunningProcess(tab) + return + } + + let surface = tab.surface(for: surfaceId) + if surface?.needsConfirmClose() == true { + guard confirmClose( + title: "Close panel?", + message: "This will close the current split panel in this tab." + ) else { return } + } + + _ = closeSurface(tabId: tab.id, surfaceId: surfaceId) + } + + func closePanelWithConfirmation(tabId: UUID, surfaceId: UUID) { + guard let tab = tabs.first(where: { $0.id == tabId }) else { return } + closePanelWithConfirmation(tab: tab, surfaceId: surfaceId) + } + private func tabNeedsConfirmClose(_ tab: Tab) -> Bool { guard let root = tab.splitTree.root else { return false } return root.leaves().contains { $0.needsConfirmClose() } diff --git a/Sources/WindowDecorationsController.swift b/Sources/WindowDecorationsController.swift new file mode 100644 index 00000000..a359678c --- /dev/null +++ b/Sources/WindowDecorationsController.swift @@ -0,0 +1,53 @@ +import AppKit + +final class WindowDecorationsController { + private var observers: [NSObjectProtocol] = [] + private var didStart = false + + func start() { + guard !didStart else { return } + didStart = true + attachToExistingWindows() + installObservers() + } + + func apply(to window: NSWindow) { + let shouldHideButtons = shouldHideTrafficLights(for: window) + hideStandardButtons(on: window, hidden: shouldHideButtons) + } + + private func installObservers() { + let center = NotificationCenter.default + let handler: (Notification) -> Void = { [weak self] notification in + guard let self, let window = notification.object as? NSWindow else { return } + self.apply(to: window) + } + observers.append(center.addObserver(forName: NSWindow.didBecomeKeyNotification, object: nil, queue: .main, using: handler)) + observers.append(center.addObserver(forName: NSWindow.didBecomeMainNotification, object: nil, queue: .main, using: handler)) + } + + private func attachToExistingWindows() { + for window in NSApp.windows { + apply(to: window) + } + } + + private func hideStandardButtons(on window: NSWindow, hidden: Bool) { + window.standardWindowButton(.closeButton)?.isHidden = hidden + window.standardWindowButton(.miniaturizeButton)?.isHidden = hidden + window.standardWindowButton(.zoomButton)?.isHidden = hidden + } + + private func shouldHideTrafficLights(for window: NSWindow) -> Bool { + if window.isSheet { + return true + } + if window is NSPanel { + return true + } + if window.styleMask.contains(.utilityWindow) || window.styleMask.contains(.docModalWindow) { + return true + } + return false + } +} diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index ccf4d611..ff77502b 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -310,9 +310,9 @@ private final class AboutWindowController: NSWindowController, NSWindowDelegate static let shared = AboutWindowController() private init() { - let window = NSWindow( + let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 360, height: 520), - styleMask: [.titled, .closable], + styleMask: [.titled, .closable, .utilityWindow], backing: .buffered, defer: false ) @@ -321,11 +321,10 @@ private final class AboutWindowController: NSWindowController, NSWindowDelegate window.titlebarAppearsTransparent = true window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false - window.standardWindowButton(.miniaturizeButton)?.isHidden = true - window.standardWindowButton(.zoomButton)?.isHidden = true window.identifier = NSUserInterfaceItemIdentifier("cmux.about") window.center() window.contentView = NSHostingView(rootView: AboutPanelView()) + AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @@ -604,5 +603,6 @@ private struct SettingsRootView: View { guard identifier.hasPrefix("cmux.") else { continue } window.removeTitlebarAccessoryViewController(at: index) } + AppDelegate.shared?.applyWindowDecorations(to: window) } }