Normalize window controls and confirm close panel

This commit is contained in:
Lawrence Chen 2026-01-29 02:20:54 -08:00
parent ba68dc3637
commit 004a353fe5
7 changed files with 109 additions and 31 deletions

View file

@ -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 = "<group>"; };
A5001222 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = "<group>"; };
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -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)";

View file

@ -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)
}

View file

@ -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)
})
}

View file

@ -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<GhosttyNSView>.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)
}
}

View file

@ -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() }

View file

@ -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
}
}

View file

@ -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)
}
}