Skip to content

Commit cc690ed

Browse files
committed
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...
1 parent 392aab2 commit cc690ed

File tree

8 files changed

+95
-3
lines changed

8 files changed

+95
-3
lines changed

Diff for: macos/Sources/App/macOS/AppDelegate.swift

+11
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ class AppDelegate: NSObject,
186186
name: .ghosttyConfigDidChange,
187187
object: nil
188188
)
189+
NotificationCenter.default.addObserver(
190+
self,
191+
selector: #selector(ghosttyBellDidRing(_:)),
192+
name: .ghosttyBellDidRing,
193+
object: nil
194+
)
189195

190196
// Configure user notifications
191197
let actions = [
@@ -502,6 +508,11 @@ class AppDelegate: NSObject,
502508
ghosttyConfigDidChange(config: config)
503509
}
504510

511+
@objc private func ghosttyBellDidRing(_ notification: Notification) {
512+
// Bounce the dock icon if we're not focused.
513+
NSApp.requestUserAttention(.informationalRequest)
514+
}
515+
505516
private func ghosttyConfigDidChange(config: Ghostty.Config) {
506517
// Update the config we need to store
507518
self.derivedConfig = DerivedConfig(config)

Diff for: macos/Sources/Ghostty/Ghostty.App.swift

+27
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,9 @@ extension Ghostty {
538538
case GHOSTTY_ACTION_COLOR_CHANGE:
539539
colorChange(app, target: target, change: action.action.color_change)
540540

541+
case GHOSTTY_ACTION_RING_BELL:
542+
ringBell(app, target: target)
543+
541544
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
542545
fallthrough
543546
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
@@ -747,6 +750,30 @@ extension Ghostty {
747750
appDelegate.toggleVisibility(self)
748751
}
749752

753+
private static func ringBell(
754+
_ app: ghostty_app_t,
755+
target: ghostty_target_s) {
756+
switch (target.tag) {
757+
case GHOSTTY_TARGET_APP:
758+
// Technically we could still request app attention here but there
759+
// are no known cases where the bell is rang with an app target so
760+
// I think its better to warn.
761+
Ghostty.logger.warning("ring bell does nothing with an app target")
762+
return
763+
764+
case GHOSTTY_TARGET_SURFACE:
765+
guard let surface = target.target.surface else { return }
766+
guard let surfaceView = self.surfaceView(from: surface) else { return }
767+
NotificationCenter.default.post(
768+
name: .ghosttyBellDidRing,
769+
object: surfaceView
770+
)
771+
772+
default:
773+
assertionFailure()
774+
}
775+
}
776+
750777
private static func moveTab(
751778
_ app: ghostty_app_t,
752779
target: ghostty_target_s,

Diff for: macos/Sources/Ghostty/Ghostty.Config.swift

+14
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ extension Ghostty {
116116
/// details on what each means. We only add documentation if there is a strange conversion
117117
/// due to the embedded library and Swift.
118118

119+
var bellFeatures: BellFeatures {
120+
guard let config = self.config else { return .init() }
121+
var v: CUnsignedInt = 0
122+
let key = "bell-features"
123+
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() }
124+
return .init(rawValue: v)
125+
}
126+
119127
var initialWindow: Bool {
120128
guard let config = self.config else { return true }
121129
var v = true;
@@ -543,6 +551,12 @@ extension Ghostty.Config {
543551
case download
544552
}
545553

554+
struct BellFeatures: OptionSet {
555+
let rawValue: CUnsignedInt
556+
557+
static let system = BellFeatures(rawValue: 1 << 0)
558+
}
559+
546560
enum MacHidden : String {
547561
case never
548562
case always

Diff for: macos/Sources/Ghostty/Package.swift

+3
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ extension Notification.Name {
253253

254254
/// Resize the window to a default size.
255255
static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize")
256+
257+
/// Ring the bell
258+
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
256259
}
257260

258261
// NOTE: I am moving all of these to Notification.Name extensions over time. This

Diff for: macos/Sources/Ghostty/SurfaceView.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ extension Ghostty {
5959

6060
@EnvironmentObject private var ghostty: Ghostty.App
6161

62+
var title: String {
63+
var result = surfaceView.title
64+
if (surfaceView.bell) {
65+
result = "🔔 \(result)"
66+
}
67+
68+
return result
69+
}
70+
6271
var body: some View {
6372
let center = NotificationCenter.default
6473

@@ -74,7 +83,7 @@ extension Ghostty {
7483

7584
Surface(view: surfaceView, size: geo.size)
7685
.focused($surfaceFocus)
77-
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
86+
.focusedValue(\.ghosttySurfaceTitle, title)
7887
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
7988
.focusedValue(\.ghosttySurfaceView, surfaceView)
8089
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)

Diff for: macos/Sources/Ghostty/SurfaceView_AppKit.swift

+20-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ extension Ghostty {
6363
/// dynamically updated. Otherwise, the background color is the default background color.
6464
@Published private(set) var backgroundColor: Color? = nil
6565

66+
/// True when the bell is active. This is set inactive on focus or event.
67+
@Published private(set) var bell: Bool = false
68+
6669
// An initial size to request for a window. This will only affect
6770
// then the view is moved to a new window.
6871
var initialSize: NSSize? = nil
@@ -190,6 +193,11 @@ extension Ghostty {
190193
selector: #selector(ghosttyColorDidChange(_:)),
191194
name: .ghosttyColorDidChange,
192195
object: self)
196+
center.addObserver(
197+
self,
198+
selector: #selector(ghosttyBellDidRing(_:)),
199+
name: .ghosttyBellDidRing,
200+
object: self)
193201
center.addObserver(
194202
self,
195203
selector: #selector(windowDidChangeScreen),
@@ -300,9 +308,12 @@ extension Ghostty {
300308
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
301309
}
302310

303-
// On macOS 13+ we can store our continuous clock...
304311
if (focused) {
312+
// On macOS 13+ we can store our continuous clock...
305313
focusInstant = ContinuousClock.now
314+
315+
// We unset our bell state if we gained focus
316+
bell = false
306317
}
307318
}
308319

@@ -556,6 +567,11 @@ extension Ghostty {
556567
}
557568
}
558569

570+
@objc private func ghosttyBellDidRing(_ notification: SwiftUI.Notification) {
571+
// Bell state goes to true
572+
bell = true
573+
}
574+
559575
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
560576
guard let window = self.window else { return }
561577
guard let object = notification.object as? NSWindow, window == object else { return }
@@ -855,6 +871,9 @@ extension Ghostty {
855871
return
856872
}
857873

874+
// On any keyDown event we unset our bell state
875+
bell = false
876+
858877
// We need to translate the mods (maybe) to handle configs such as option-as-alt
859878
let translationModsGhostty = Ghostty.eventModifierFlags(
860879
mods: ghostty_surface_key_translation_mods(

Diff for: macos/Sources/Ghostty/SurfaceView_UIKit.swift

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ extension Ghostty {
3535
// on supported platforms.
3636
@Published var focusInstant: ContinuousClock.Instant? = nil
3737

38+
/// True when the bell is active. This is set inactive on focus or event.
39+
@Published var bell: Bool = false
40+
3841
// Returns sizing information for the surface. This is the raw C
3942
// structure because I'm lazy.
4043
var surfaceSize: ghostty_surface_size_s? {

Diff for: src/config/Config.zig

+7-1
Original file line numberDiff line numberDiff line change
@@ -1874,7 +1874,13 @@ keybind: Keybinds = .{},
18741874
/// for instance under the "Sound > Alert Sound" setting in GNOME,
18751875
/// or the "Accessibility > System Bell" settings in KDE Plasma.
18761876
///
1877-
/// Currently only implemented on Linux.
1877+
/// On macOS this has no affect.
1878+
///
1879+
/// On macOS, if the app is unfocused, it will bounce the app icon in the dock
1880+
/// once. Additionally, the title of the window with the alerted terminal
1881+
/// surface will contain a bell emoji (🔔) until the terminal is focused
1882+
/// or a key is pressed. These are not currently configurable since they're
1883+
/// considered unobtrusive.
18781884
@"bell-features": BellFeatures = .{},
18791885

18801886
/// Control the in-app notifications that Ghostty shows.

0 commit comments

Comments
 (0)