Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 95 additions & 9 deletions macos/Sources/Features/Terminal/BaseTerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1155,6 +1164,8 @@ class BaseTerminalController: NSWindowController,

// Set our update overlay state
updateOverlayIsVisible = defaultUpdateOverlayVisibility()

refreshProxyIcon()
}

func defaultUpdateOverlayVisibility() -> Bool {
Expand Down Expand Up @@ -1243,6 +1254,8 @@ class BaseTerminalController: NSWindowController,
DispatchQueue.main.async {
self.syncFocusToSurfaceTree()
}

refreshProxyIcon()
}

func windowDidResignKey(_ notification: Notification) {
Expand All @@ -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? {
Expand Down Expand Up @@ -1427,14 +1456,71 @@ 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
let focusFollowsMouse: Bool
let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom

init() {
self.macosTitlebarProxyIcon = .visible
self.macosTitlebarProxyIcon = .hidden
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not change the default to avoid surprising people that are used to (and perhaps prefer) the current behavior.

self.windowStepResize = false
self.focusFollowsMouse = false
self.splitPreserveZoom = .init()
Expand Down
2 changes: 1 addition & 1 deletion macos/Sources/Ghostty/Ghostty.Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ extension Ghostty {
}

var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon {
let defaultValue = MacOSTitlebarProxyIcon.visible
let defaultValue = MacOSTitlebarProxyIcon.hidden
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not change the default to avoid surprising people that are used to (and perhaps prefer) the current behavior.

guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>?
let key = "macos-titlebar-proxy-icon"
Expand Down
5 changes: 3 additions & 2 deletions macos/Sources/Ghostty/GhosttyPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions macos/Tests/Ghostty/ConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 7 additions & 9 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not change the default to avoid surprising people that are used to (and perhaps prefer) the current behavior.


/// Controls the windowing behavior when dropping a file or folder
/// onto the Ghostty icon in the macOS dock.
Expand Down Expand Up @@ -8955,6 +8952,7 @@ pub const MacTitlebarStyle = enum {
pub const MacTitlebarProxyIcon = enum {
visible,
hidden,
disabled,
};

/// See macos-hidden
Expand Down