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..8a642034f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -596,7 +596,7 @@ class QuickTerminalController: BaseTerminalController { }) } - private func syncAppearance() { + override func syncAppearance() { guard let window else { return } defer { updateColorSchemeForSurfaceTree() } @@ -608,7 +608,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..5f067c128 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,35 @@ class BaseTerminalController: NSWindowController, } } + // MARK: Appearance + + /// Toggle the background opacity between transparent and opaque states. + /// 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() { + // 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 /// Toggle fullscreen for the given mode. @@ -872,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 a980723ba..8a0c5f46d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -165,17 +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) - } - // MARK: Terminal Creation /// Returns all the available terminal controllers present in the app currently. @@ -489,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 0c0ac0646..4196df97f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -469,7 +469,11 @@ 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..8e0a9d018 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -115,6 +115,11 @@ pub const Action = union(Key) { /// Toggle the visibility of all Ghostty terminal windows. toggle_visibility, + /// 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. /// /// Adjusts the tab position based on `offset` (e.g., -1 for left, +1 @@ -335,6 +340,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/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, 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..6ac4312a9 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 background opacity of a window that started transparent.", + }}, + .check_for_updates => comptime &.{.{ .action = .check_for_updates, .title = "Check for Updates",