macos: add proxy icon hover mode and disabled state
Extend macos-titlebar-proxy-icon from two values (visible/hidden) to three (visible/hidden/disabled). hidden now means show-on-hover using NSTrackingArea on the titlebar, matching standard macOS behavior. disabled completely removes the proxy icon. Default changes from visible to hidden. Config changes apply immediately to all open windows via GhosttyConfigDidChange notification. Closes #5919pull/12166/head
parent
0043e665f5
commit
6f2e3be66f
|
|
@ -80,6 +80,15 @@ class BaseTerminalController: NSWindowController,
|
|||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
/// The current pwd for the focused surface. We cache this separately from representedURL
|
||||
/// so config reload can restore the proxy icon after disabled mode clears the window state.
|
||||
private var currentPwdURL: URL?
|
||||
|
||||
/// Hover state and tracking used for the "hidden" proxy icon mode.
|
||||
private var isHoveringProxyIconTitlebar: Bool = false
|
||||
private weak var proxyIconTrackingView: NSView?
|
||||
private var proxyIconTrackingArea: NSTrackingArea?
|
||||
|
||||
/// Track whether background is forced opaque (true) or using config transparency (false)
|
||||
var isBackgroundOpaque: Bool = false
|
||||
|
||||
|
|
@ -224,6 +233,9 @@ class BaseTerminalController: NSWindowController,
|
|||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
undoManager?.removeAllActions(withTarget: self)
|
||||
if let trackingArea = proxyIconTrackingArea, let trackingView = proxyIconTrackingView {
|
||||
trackingView.removeTrackingArea(trackingArea)
|
||||
}
|
||||
if let eventMonitor {
|
||||
NSEvent.removeMonitor(eventMonitor)
|
||||
}
|
||||
|
|
@ -567,6 +579,9 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
// Update our derived config
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
||||
// Immediately refresh proxy icon behavior on all open windows.
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
@objc private func ghosttyCommandPaletteDidToggle(_ notification: Notification) {
|
||||
|
|
@ -853,14 +868,8 @@ class BaseTerminalController: NSWindowController,
|
|||
}
|
||||
|
||||
func pwdDidChange(to: URL?) {
|
||||
guard let window else { return }
|
||||
|
||||
if derivedConfig.macosTitlebarProxyIcon == .visible {
|
||||
// Use the 'to' URL directly
|
||||
window.representedURL = to
|
||||
} else {
|
||||
window.representedURL = nil
|
||||
}
|
||||
currentPwdURL = to
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
|
|
@ -1155,6 +1164,8 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
// Set our update overlay state
|
||||
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
|
||||
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
func defaultUpdateOverlayVisibility() -> Bool {
|
||||
|
|
@ -1243,6 +1254,8 @@ class BaseTerminalController: NSWindowController,
|
|||
DispatchQueue.main.async {
|
||||
self.syncFocusToSurfaceTree()
|
||||
}
|
||||
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
|
|
@ -1262,10 +1275,26 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
func windowDidResize(_ notification: Notification) {
|
||||
windowFrameDidChange()
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
func windowDidMove(_ notification: Notification) {
|
||||
windowFrameDidChange()
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
super.mouseEntered(with: event)
|
||||
guard derivedConfig.macosTitlebarProxyIcon == .hidden else { return }
|
||||
isHoveringProxyIconTitlebar = true
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
super.mouseExited(with: event)
|
||||
guard derivedConfig.macosTitlebarProxyIcon == .hidden else { return }
|
||||
isHoveringProxyIconTitlebar = false
|
||||
refreshProxyIcon()
|
||||
}
|
||||
|
||||
func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? {
|
||||
|
|
@ -1427,6 +1456,63 @@ class BaseTerminalController: NSWindowController,
|
|||
ghostty.resetTerminal(surface: surface)
|
||||
}
|
||||
|
||||
private func refreshProxyIcon() {
|
||||
guard let window else { return }
|
||||
|
||||
refreshProxyIconTrackingArea()
|
||||
|
||||
let pwdURL = currentPwdURL ?? window.representedURL
|
||||
|
||||
switch derivedConfig.macosTitlebarProxyIcon {
|
||||
case .visible:
|
||||
window.representedURL = pwdURL
|
||||
window.standardWindowButton(.documentIconButton)?.isHidden = (pwdURL == nil)
|
||||
|
||||
case .hidden:
|
||||
window.representedURL = pwdURL
|
||||
window.standardWindowButton(.documentIconButton)?.isHidden =
|
||||
!isHoveringProxyIconTitlebar || pwdURL == nil
|
||||
|
||||
case .disabled:
|
||||
window.standardWindowButton(.documentIconButton)?.isHidden = true
|
||||
window.representedURL = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshProxyIconTrackingArea() {
|
||||
if let trackingArea = proxyIconTrackingArea, let trackingView = proxyIconTrackingView {
|
||||
trackingView.removeTrackingArea(trackingArea)
|
||||
proxyIconTrackingArea = nil
|
||||
proxyIconTrackingView = nil
|
||||
}
|
||||
|
||||
guard derivedConfig.macosTitlebarProxyIcon == .hidden else {
|
||||
isHoveringProxyIconTitlebar = false
|
||||
return
|
||||
}
|
||||
|
||||
guard let terminalWindow = window as? TerminalWindow,
|
||||
let titlebarContainer = terminalWindow.titlebarContainer else {
|
||||
isHoveringProxyIconTitlebar = false
|
||||
return
|
||||
}
|
||||
|
||||
let trackingArea = NSTrackingArea(
|
||||
rect: .zero,
|
||||
options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
|
||||
owner: self,
|
||||
userInfo: nil)
|
||||
titlebarContainer.addTrackingArea(trackingArea)
|
||||
|
||||
proxyIconTrackingArea = trackingArea
|
||||
proxyIconTrackingView = titlebarContainer
|
||||
|
||||
let mouseLocation = titlebarContainer.convert(
|
||||
window?.mouseLocationOutsideOfEventStream ?? .zero,
|
||||
from: nil)
|
||||
isHoveringProxyIconTitlebar = titlebarContainer.bounds.contains(mouseLocation)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
|
|
@ -1434,7 +1520,7 @@ class BaseTerminalController: NSWindowController,
|
|||
let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom
|
||||
|
||||
init() {
|
||||
self.macosTitlebarProxyIcon = .visible
|
||||
self.macosTitlebarProxyIcon = .hidden
|
||||
self.windowStepResize = false
|
||||
self.focusFollowsMouse = false
|
||||
self.splitPreserveZoom = .init()
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ extension Ghostty {
|
|||
}
|
||||
|
||||
var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon {
|
||||
let defaultValue = MacOSTitlebarProxyIcon.visible
|
||||
let defaultValue = MacOSTitlebarProxyIcon.hidden
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>?
|
||||
let key = "macos-titlebar-proxy-icon"
|
||||
|
|
|
|||
|
|
@ -316,8 +316,9 @@ extension Ghostty {
|
|||
|
||||
/// Enum for the macos-titlebar-proxy-icon config option
|
||||
enum MacOSTitlebarProxyIcon: String {
|
||||
case visible
|
||||
case hidden
|
||||
case visible = "visible"
|
||||
case hidden = "hidden"
|
||||
case disabled = "disabled"
|
||||
}
|
||||
|
||||
/// Enum for auto-update-channel config option
|
||||
|
|
|
|||
|
|
@ -106,6 +106,21 @@ struct ConfigTests {
|
|||
#expect(config.macosTitlebarStyle == expected)
|
||||
}
|
||||
|
||||
@Test func macosTitlebarProxyIconDefaultsToHidden() throws {
|
||||
let config = try TemporaryConfig("")
|
||||
#expect(config.macosTitlebarProxyIcon == .hidden)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
("visible", Ghostty.Config.MacOSTitlebarProxyIcon.visible),
|
||||
("hidden", Ghostty.Config.MacOSTitlebarProxyIcon.hidden),
|
||||
("disabled", Ghostty.Config.MacOSTitlebarProxyIcon.disabled),
|
||||
])
|
||||
func macosTitlebarProxyIconValues(raw: String, expected: Ghostty.Config.MacOSTitlebarProxyIcon) throws {
|
||||
let config = try TemporaryConfig("macos-titlebar-proxy-icon = \(raw)")
|
||||
#expect(config.macosTitlebarProxyIcon == expected)
|
||||
}
|
||||
|
||||
@Test func resizeOverlayDefaultsToAfterFirst() throws {
|
||||
let config = try TemporaryConfig("")
|
||||
#expect(config.resizeOverlay == .after_first)
|
||||
|
|
|
|||
|
|
@ -3247,17 +3247,14 @@ keybind: Keybinds = .{},
|
|||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `visible` - Show the proxy icon.
|
||||
/// * `hidden` - Hide the proxy icon.
|
||||
///
|
||||
/// The default value is `visible`.
|
||||
/// Proxy icon visibility mode for the titlebar. Valid values:
|
||||
/// - `visible` - always show the proxy icon
|
||||
/// - `hidden` - show proxy icon on hover (default, modern macOS behavior)
|
||||
/// - `disabled` - never show the proxy icon (drag-drop and context menu also disabled)
|
||||
///
|
||||
/// This setting can be changed at runtime and will affect all currently
|
||||
/// open windows but only after their working directory changes again.
|
||||
/// Therefore, to make this work after changing the setting, you must
|
||||
/// usually `cd` to a different directory, open a different file in an
|
||||
/// editor, etc.
|
||||
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
|
||||
/// open windows immediately.
|
||||
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .hidden,
|
||||
|
||||
/// Controls the windowing behavior when dropping a file or folder
|
||||
/// onto the Ghostty icon in the macOS dock.
|
||||
|
|
@ -8955,6 +8952,7 @@ pub const MacTitlebarStyle = enum {
|
|||
pub const MacTitlebarProxyIcon = enum {
|
||||
visible,
|
||||
hidden,
|
||||
disabled,
|
||||
};
|
||||
|
||||
/// See macos-hidden
|
||||
|
|
|
|||
Loading…
Reference in New Issue