macOS: add `toggle_background_opacity` keybind action (#9117)
Related Issue: #5047 Discussion: #4664 ### Investigation The behavior of iTerm mentioned on the Discussion thread was as follows: - `cmd + u` can be used to toggle "Use Transparency" - The "Use Transparency" toggle operates on a per-surface basis - The "Use Transparency" state persists even after reloading the config ### Summary Based on the investigation and discussions in the preceding pull requests, this implements the `toggle_background_opacity` keybind action for macOS with the following specifications: - Switches background opacity on a per-surface basis - The background opacity state persists even after reloading the config - Background opacity switching functionality is also available in Quick Terminal - Does nothing if `background-opacity` is set to 1 or higher - Does nothing if in fullscreen mode ### Verification This functionality has been tested across following scenarios, confirming correct action behavior for: - Split Window - Each split window maintains synchronized background opacity states - Tab Group - Background opacity states do not synchronize between tabs - Quick Terminal - Background opacity states remain synchronized even when windows are closed - Command Palette ### AI Disclosure This pull request was made with assistance from Claude Code. I reviewed all AI-generated code and wrote the final output manually. https://github.com/user-attachments/assets/e46ff8f0-42f2-442f-97dd-d5f5c33b10f1pull/9933/head
commit
4827c7f53a
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<AnyCancellable> = []
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue