From 4c6d3f8ed2d53f5881ca525750650066c55c5c9f Mon Sep 17 00:00:00 2001 From: himura467 Date: Fri, 10 Oct 2025 11:01:41 +0900 Subject: [PATCH 1/6] macos: add `toggle_background_opacity` keybind action --- include/ghostty.h | 1 + .../QuickTerminalController.swift | 10 +++++++- .../Terminal/BaseTerminalController.swift | 19 +++++++++++++++ .../Terminal/TerminalController.swift | 8 +++++++ .../Window Styles/TerminalWindow.swift | 3 +++ macos/Sources/Ghostty/Ghostty.App.swift | 24 +++++++++++++++++++ src/Surface.zig | 6 +++++ src/apprt/action.zig | 4 ++++ src/input/Binding.zig | 11 +++++++++ src/input/command.zig | 6 +++++ 10 files changed, 91 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index b0395b89e..47db34e71 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -803,6 +803,7 @@ typedef enum { GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE, GHOSTTY_ACTION_TOGGLE_VISIBILITY, + GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY, GHOSTTY_ACTION_MOVE_TAB, GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_SPLIT, diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 4377b6510..d2db44d2d 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -313,6 +313,13 @@ class QuickTerminalController: BaseTerminalController { animateOut() } + override func toggleBackgroundOpacity() { + super.toggleBackgroundOpacity() + + // Sync the window appearance with the new opacity state + syncAppearance() + } + // MARK: Methods func toggle() { @@ -608,7 +615,8 @@ class QuickTerminalController: BaseTerminalController { guard window.isVisible else { return } // If we have window transparency then set it transparent. Otherwise set it opaque. - if (self.derivedConfig.backgroundOpacity < 1) { + // Also check if the user has overridden transparency to be fully opaque. + if !isBackgroundOpaque && self.derivedConfig.backgroundOpacity < 1 { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 98f1bcbf8..892bef555 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -78,6 +78,9 @@ class BaseTerminalController: NSWindowController, /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + /// Track whether background is forced opaque (true) or using config transparency (false) + var isBackgroundOpaque: Bool = false + /// The cancellables related to our focused surface. private var focusedSurfaceCancellables: Set = [] @@ -812,6 +815,22 @@ class BaseTerminalController: NSWindowController, } } + // MARK: Background Opacity + + /// Toggle the background opacity between transparent and opaque states. + /// If the configured background-opacity is already opaque (>= 1), this resets + /// the override flag to false so that future config changes take effect. + /// Subclasses should override this to sync their appearance after toggling. + func toggleBackgroundOpacity() { + // If config is already opaque, just ensure override is disabled + if ghostty.config.backgroundOpacity >= 1 { + isBackgroundOpaque = false + } else { + // Otherwise toggle between transparent and opaque + isBackgroundOpaque.toggle() + } + } + // MARK: Fullscreen /// Toggle fullscreen for the given mode. diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a980723ba..29b856cdb 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -176,6 +176,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr syncAppearance(focusedSurface.derivedConfig) } + override func toggleBackgroundOpacity() { + super.toggleBackgroundOpacity() + + // Sync the window appearance with the new opacity state + guard let focusedSurface else { return } + syncAppearance(focusedSurface.derivedConfig) + } + // MARK: Terminal Creation /// Returns all the available terminal controllers present in the app currently. diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 0c0ac0646..730cdea65 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -469,7 +469,10 @@ class TerminalWindow: NSWindow { // Window transparency only takes effect if our window is not native fullscreen. // In native fullscreen we disable transparency/opacity because the background // becomes gray and widgets show through. + // Also check if the user has overridden transparency to be fully opaque. + let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && + !forceOpaque && surfaceConfig.backgroundOpacity < 1 { isOpaque = false diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2cd0a362a..4e9d039d4 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -573,6 +573,9 @@ extension Ghostty { case GHOSTTY_ACTION_TOGGLE_VISIBILITY: toggleVisibility(app, target: target) + case GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY: + toggleBackgroundOpacity(app, target: target) + case GHOSTTY_ACTION_KEY_SEQUENCE: keySequence(app, target: target, v: action.action.key_sequence) @@ -1375,6 +1378,27 @@ extension Ghostty { } } + private static func toggleBackgroundOpacity( + _ app: ghostty_app_t, + target: ghostty_target_s + ) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle background opacity does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface, + let surfaceView = self.surfaceView(from: surface), + let controller = surfaceView.window?.windowController as? BaseTerminalController else { return } + + controller.toggleBackgroundOpacity() + + default: + assertionFailure() + } + } + private static func toggleSecureInput( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/src/Surface.zig b/src/Surface.zig index 4786e0b86..d84e786f3 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5518,6 +5518,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .toggle_background_opacity => return try self.rt_app.performAction( + .{ .surface = self }, + .toggle_background_opacity, + {}, + ), + .show_on_screen_keyboard => return try self.rt_app.performAction( .{ .surface = self }, .show_on_screen_keyboard, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index af1c22552..7b9e9d222 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -115,6 +115,9 @@ pub const Action = union(Key) { /// Toggle the visibility of all Ghostty terminal windows. toggle_visibility, + /// Toggle the window background opacity. This currently only works on macOS. + toggle_background_opacity, + /// Moves a tab by a relative offset. /// /// Adjusts the tab position based on `offset` (e.g., -1 for left, +1 @@ -335,6 +338,7 @@ pub const Action = union(Key) { toggle_quick_terminal, toggle_command_palette, toggle_visibility, + toggle_background_opacity, move_tab, goto_tab, goto_split, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 31672bc1a..9f3ad8a2a 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -755,6 +755,16 @@ pub const Action = union(enum) { /// Only implemented on macOS. toggle_visibility, + /// Toggle the window background opacity between transparent and opaque. + /// + /// This does nothing when `background-opacity` is set to 1 or above. + /// + /// When `background-opacity` is less than 1, this action will either make + /// the window transparent or not depending on its current transparency state. + /// + /// Only implemented on macOS. + toggle_background_opacity, + /// Check for updates. /// /// Only implemented on macOS. @@ -1240,6 +1250,7 @@ pub const Action = union(enum) { .toggle_secure_input, .toggle_mouse_reporting, .toggle_command_palette, + .toggle_background_opacity, .show_on_screen_keyboard, .reset_window_size, .crash, diff --git a/src/input/command.zig b/src/input/command.zig index a377effa2..d5daafd7d 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -618,6 +618,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle whether mouse events are reported to terminal applications.", }}, + .toggle_background_opacity => comptime &.{.{ + .action = .toggle_background_opacity, + .title = "Toggle Background Opacity", + .description = "Toggle the window background between transparent and opaque.", + }}, + .check_for_updates => comptime &.{.{ .action = .check_for_updates, .title = "Check for Updates", From ded3dd4cbcf84e0156c3cdd4eb43ae0a2d6c2e89 Mon Sep 17 00:00:00 2001 From: himura467 Date: Fri, 10 Oct 2025 12:56:20 +0900 Subject: [PATCH 2/6] refactor(macos): do nothing if `background-opacity >= 1` --- .../Terminal/BaseTerminalController.swift | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 892bef555..f8e0cc8e9 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -818,17 +818,14 @@ class BaseTerminalController: NSWindowController, // MARK: Background Opacity /// Toggle the background opacity between transparent and opaque states. - /// If the configured background-opacity is already opaque (>= 1), this resets - /// the override flag to false so that future config changes take effect. - /// Subclasses should override this to sync their appearance after toggling. + /// Do nothing if the configured background-opacity is >= 1 (already opaque). + /// Subclasses should override this to add platform-specific checks and sync appearance. func toggleBackgroundOpacity() { - // If config is already opaque, just ensure override is disabled - if ghostty.config.backgroundOpacity >= 1 { - isBackgroundOpaque = false - } else { - // Otherwise toggle between transparent and opaque - isBackgroundOpaque.toggle() - } + // Do nothing if config is already fully opaque + guard ghostty.config.backgroundOpacity < 1 else { return } + + // Toggle between transparent and opaque + isBackgroundOpaque.toggle() } // MARK: Fullscreen From 8d49c698e47519a889269cbdcdef33f705135767 Mon Sep 17 00:00:00 2001 From: himura467 Date: Fri, 10 Oct 2025 12:56:52 +0900 Subject: [PATCH 3/6] refactor(macos): do nothing if in fullscreen --- macos/Sources/Features/Terminal/TerminalController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 29b856cdb..cc5b48700 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -177,6 +177,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } override func toggleBackgroundOpacity() { + // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) + guard let window = self.window, !window.styleMask.contains(.fullScreen) else { return } + super.toggleBackgroundOpacity() // Sync the window appearance with the new opacity state From ba2cbef1f1d3effcfbc672dbea52ac9b0b01bdcf Mon Sep 17 00:00:00 2001 From: himura467 Date: Fri, 10 Oct 2025 14:56:15 +0900 Subject: [PATCH 4/6] apprt/gtk: list `toggle_background_opacity` as unimplemented --- src/apprt/gtk/class/application.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index c951cc6ac..be0f3f2c8 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -741,6 +741,7 @@ pub const Application = extern struct { .close_all_windows, .float_window, .toggle_visibility, + .toggle_background_opacity, .cell_size, .key_sequence, .render_inspector, From f9a1f526c897a3f8c94c697f3624de0e1c250fcd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Dec 2025 11:38:25 -0800 Subject: [PATCH 5/6] update some copy for the background opacity toggle --- src/apprt/action.zig | 4 +++- src/input/command.zig | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 7b9e9d222..8e0a9d018 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -115,7 +115,9 @@ pub const Action = union(Key) { /// Toggle the visibility of all Ghostty terminal windows. toggle_visibility, - /// Toggle the window background opacity. This currently only works on macOS. + /// Toggle the window background opacity. This only has an effect + /// if the window started as transparent (non-opaque), and toggles + /// it between fully opaque and the configured background opacity. toggle_background_opacity, /// Moves a tab by a relative offset. diff --git a/src/input/command.zig b/src/input/command.zig index d5daafd7d..6ac4312a9 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -621,7 +621,7 @@ fn actionCommands(action: Action.Key) []const Command { .toggle_background_opacity => comptime &.{.{ .action = .toggle_background_opacity, .title = "Toggle Background Opacity", - .description = "Toggle the window background between transparent and opaque.", + .description = "Toggle the background opacity of a window that started transparent.", }}, .check_for_updates => comptime &.{.{ From 95f4093e96f98dc963575a043ece13778c339cd1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Dec 2025 12:59:51 -0800 Subject: [PATCH 6/6] macos: make syncAppearance a virtual method on BaseTerminalController --- .../QuickTerminalController.swift | 9 +----- .../Terminal/BaseTerminalController.swift | 21 +++++++++++++- .../Terminal/TerminalController.swift | 29 +++++-------------- .../Window Styles/TerminalWindow.swift | 1 + 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index d2db44d2d..8a642034f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -313,13 +313,6 @@ class QuickTerminalController: BaseTerminalController { animateOut() } - override func toggleBackgroundOpacity() { - super.toggleBackgroundOpacity() - - // Sync the window appearance with the new opacity state - syncAppearance() - } - // MARK: Methods func toggle() { @@ -603,7 +596,7 @@ class QuickTerminalController: BaseTerminalController { }) } - private func syncAppearance() { + override func syncAppearance() { guard let window else { return } defer { updateColorSchemeForSurfaceTree() } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index f8e0cc8e9..5f067c128 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -815,7 +815,7 @@ class BaseTerminalController: NSWindowController, } } - // MARK: Background Opacity + // MARK: Appearance /// Toggle the background opacity between transparent and opaque states. /// Do nothing if the configured background-opacity is >= 1 (already opaque). @@ -823,9 +823,25 @@ class BaseTerminalController: NSWindowController, func toggleBackgroundOpacity() { // Do nothing if config is already fully opaque guard ghostty.config.backgroundOpacity < 1 else { return } + + // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) + guard let window, !window.styleMask.contains(.fullScreen) else { return } // Toggle between transparent and opaque isBackgroundOpaque.toggle() + + // Update our appearance + syncAppearance() + } + + /// Override this to resync any appearance related properties. This will be called automatically + /// when certain window properties change that affect appearance. The list below should be updated + /// as we add new things: + /// + /// - ``toggleBackgroundOpacity`` + func syncAppearance() { + // Purposely a no-op. This lets subclasses override this and we can call + // it virtually from here. } // MARK: Fullscreen @@ -888,6 +904,9 @@ class BaseTerminalController: NSWindowController, } else { updateOverlayIsVisible = defaultUpdateOverlayVisibility() } + + // Always resync our appearance + syncAppearance() } // MARK: Clipboard Confirmation diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cc5b48700..8a0c5f46d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -165,28 +165,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } - - override func fullscreenDidChange() { - super.fullscreenDidChange() - - // When our fullscreen state changes, we resync our appearance because some - // properties change when fullscreen or not. - guard let focusedSurface else { return } - - syncAppearance(focusedSurface.derivedConfig) - } - - override func toggleBackgroundOpacity() { - // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) - guard let window = self.window, !window.styleMask.contains(.fullScreen) else { return } - - super.toggleBackgroundOpacity() - - // Sync the window appearance with the new opacity state - guard let focusedSurface else { return } - syncAppearance(focusedSurface.derivedConfig) - } - // MARK: Terminal Creation /// Returns all the available terminal controllers present in the app currently. @@ -500,6 +478,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabWindowsHash = v self.relabelTabs() } + + override func syncAppearance() { + // When our focus changes, we update our window appearance based on the + // currently focused surface. + guard let focusedSurface else { return } + syncAppearance(focusedSurface.derivedConfig) + } private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // Let our window handle its own appearance diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 730cdea65..4196df97f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -469,6 +469,7 @@ class TerminalWindow: NSWindow { // Window transparency only takes effect if our window is not native fullscreen. // In native fullscreen we disable transparency/opacity because the background // becomes gray and widgets show through. + // // Also check if the user has overridden transparency to be fully opaque. let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) &&