mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
macOS: Implement basic bell features (no sound)
Fixes #7099 This adds basic bell features to macOS to conceptually match the GTK implementation. When a bell is triggered, macOS will do the following: 1. Bounce the dock icon once, if the app isn't already in focus. 2. Add a bell emoji (🔔) to the title of the surface that triggered the bell. This emoji will be removed after the surface is focused or a keyboard event if the surface is already focused. This behavior matches iTerm2. This doesn't add an icon badge because macOS's dockTitle.badgeLabel API wasn't doing anything for me and I wasn't able to fully figure out why...
This commit is contained in:
@@ -186,6 +186,12 @@ class AppDelegate: NSObject,
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyBellDidRing(_:)),
|
||||
name: .ghosttyBellDidRing,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Configure user notifications
|
||||
let actions = [
|
||||
@@ -502,6 +508,11 @@ class AppDelegate: NSObject,
|
||||
ghosttyConfigDidChange(config: config)
|
||||
}
|
||||
|
||||
@objc private func ghosttyBellDidRing(_ notification: Notification) {
|
||||
// Bounce the dock icon if we're not focused.
|
||||
NSApp.requestUserAttention(.informationalRequest)
|
||||
}
|
||||
|
||||
private func ghosttyConfigDidChange(config: Ghostty.Config) {
|
||||
// Update the config we need to store
|
||||
self.derivedConfig = DerivedConfig(config)
|
||||
|
@@ -538,6 +538,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||
colorChange(app, target: target, change: action.action.color_change)
|
||||
|
||||
case GHOSTTY_ACTION_RING_BELL:
|
||||
ringBell(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||
@@ -747,6 +750,30 @@ extension Ghostty {
|
||||
appDelegate.toggleVisibility(self)
|
||||
}
|
||||
|
||||
private static func ringBell(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
// Technically we could still request app attention here but there
|
||||
// are no known cases where the bell is rang with an app target so
|
||||
// I think its better to warn.
|
||||
Ghostty.logger.warning("ring bell does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyBellDidRing,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func moveTab(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
@@ -116,6 +116,14 @@ extension Ghostty {
|
||||
/// details on what each means. We only add documentation if there is a strange conversion
|
||||
/// due to the embedded library and Swift.
|
||||
|
||||
var bellFeatures: BellFeatures {
|
||||
guard let config = self.config else { return .init() }
|
||||
var v: CUnsignedInt = 0
|
||||
let key = "bell-features"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() }
|
||||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var initialWindow: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = true;
|
||||
@@ -543,6 +551,12 @@ extension Ghostty.Config {
|
||||
case download
|
||||
}
|
||||
|
||||
struct BellFeatures: OptionSet {
|
||||
let rawValue: CUnsignedInt
|
||||
|
||||
static let system = BellFeatures(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
enum MacHidden : String {
|
||||
case never
|
||||
case always
|
||||
|
@@ -253,6 +253,9 @@ extension Notification.Name {
|
||||
|
||||
/// Resize the window to a default size.
|
||||
static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize")
|
||||
|
||||
/// Ring the bell
|
||||
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
|
||||
}
|
||||
|
||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||
|
@@ -59,6 +59,15 @@ extension Ghostty {
|
||||
|
||||
@EnvironmentObject private var ghostty: Ghostty.App
|
||||
|
||||
var title: String {
|
||||
var result = surfaceView.title
|
||||
if (surfaceView.bell) {
|
||||
result = "🔔 \(result)"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let center = NotificationCenter.default
|
||||
|
||||
@@ -74,7 +83,7 @@ extension Ghostty {
|
||||
|
||||
Surface(view: surfaceView, size: geo.size)
|
||||
.focused($surfaceFocus)
|
||||
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
||||
.focusedValue(\.ghosttySurfaceTitle, title)
|
||||
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
|
||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
|
||||
|
@@ -63,6 +63,9 @@ extension Ghostty {
|
||||
/// dynamically updated. Otherwise, the background color is the default background color.
|
||||
@Published private(set) var backgroundColor: Color? = nil
|
||||
|
||||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published private(set) var bell: Bool = false
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
@@ -190,6 +193,11 @@ extension Ghostty {
|
||||
selector: #selector(ghosttyColorDidChange(_:)),
|
||||
name: .ghosttyColorDidChange,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyBellDidRing(_:)),
|
||||
name: .ghosttyBellDidRing,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
@@ -300,9 +308,12 @@ extension Ghostty {
|
||||
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
|
||||
}
|
||||
|
||||
// On macOS 13+ we can store our continuous clock...
|
||||
if (focused) {
|
||||
// On macOS 13+ we can store our continuous clock...
|
||||
focusInstant = ContinuousClock.now
|
||||
|
||||
// We unset our bell state if we gained focus
|
||||
bell = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +567,11 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyBellDidRing(_ notification: SwiftUI.Notification) {
|
||||
// Bell state goes to true
|
||||
bell = true
|
||||
}
|
||||
|
||||
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||
guard let window = self.window else { return }
|
||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||
@@ -855,6 +871,9 @@ extension Ghostty {
|
||||
return
|
||||
}
|
||||
|
||||
// On any keyDown event we unset our bell state
|
||||
bell = false
|
||||
|
||||
// We need to translate the mods (maybe) to handle configs such as option-as-alt
|
||||
let translationModsGhostty = Ghostty.eventModifierFlags(
|
||||
mods: ghostty_surface_key_translation_mods(
|
||||
|
@@ -35,6 +35,9 @@ extension Ghostty {
|
||||
// on supported platforms.
|
||||
@Published var focusInstant: ContinuousClock.Instant? = nil
|
||||
|
||||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published var bell: Bool = false
|
||||
|
||||
// Returns sizing information for the surface. This is the raw C
|
||||
// structure because I'm lazy.
|
||||
var surfaceSize: ghostty_surface_size_s? {
|
||||
|
@@ -1874,7 +1874,13 @@ keybind: Keybinds = .{},
|
||||
/// for instance under the "Sound > Alert Sound" setting in GNOME,
|
||||
/// or the "Accessibility > System Bell" settings in KDE Plasma.
|
||||
///
|
||||
/// Currently only implemented on Linux.
|
||||
/// On macOS this has no affect.
|
||||
///
|
||||
/// On macOS, if the app is unfocused, it will bounce the app icon in the dock
|
||||
/// once. Additionally, the title of the window with the alerted terminal
|
||||
/// surface will contain a bell emoji (🔔) until the terminal is focused
|
||||
/// or a key is pressed. These are not currently configurable since they're
|
||||
/// considered unobtrusive.
|
||||
@"bell-features": BellFeatures = .{},
|
||||
|
||||
/// Control the in-app notifications that Ghostty shows.
|
||||
|
Reference in New Issue
Block a user