mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-25 10:58:39 +03:00
macOS: Implement basic bell features (no sound) (#7101)
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. Note that neither of these respect the `system` `bell-features` config because they're both unobtrusive (the dock icon bounces only once, the title change is silent and similar to GTK tab attention) and unrelated to system settings. 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,
|
name: .ghosttyConfigDidChange,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyBellDidRing(_:)),
|
||||||
|
name: .ghosttyBellDidRing,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
// Configure user notifications
|
// Configure user notifications
|
||||||
let actions = [
|
let actions = [
|
||||||
@ -502,6 +508,11 @@ class AppDelegate: NSObject,
|
|||||||
ghosttyConfigDidChange(config: config)
|
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) {
|
private func ghosttyConfigDidChange(config: Ghostty.Config) {
|
||||||
// Update the config we need to store
|
// Update the config we need to store
|
||||||
self.derivedConfig = DerivedConfig(config)
|
self.derivedConfig = DerivedConfig(config)
|
||||||
|
@ -538,6 +538,9 @@ extension Ghostty {
|
|||||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||||
colorChange(app, target: target, change: action.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:
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||||
fallthrough
|
fallthrough
|
||||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||||
@ -747,6 +750,30 @@ extension Ghostty {
|
|||||||
appDelegate.toggleVisibility(self)
|
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(
|
private static func moveTab(
|
||||||
_ app: ghostty_app_t,
|
_ app: ghostty_app_t,
|
||||||
target: ghostty_target_s,
|
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
|
/// details on what each means. We only add documentation if there is a strange conversion
|
||||||
/// due to the embedded library and Swift.
|
/// 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 {
|
var initialWindow: Bool {
|
||||||
guard let config = self.config else { return true }
|
guard let config = self.config else { return true }
|
||||||
var v = true;
|
var v = true;
|
||||||
@ -543,6 +551,12 @@ extension Ghostty.Config {
|
|||||||
case download
|
case download
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BellFeatures: OptionSet {
|
||||||
|
let rawValue: CUnsignedInt
|
||||||
|
|
||||||
|
static let system = BellFeatures(rawValue: 1 << 0)
|
||||||
|
}
|
||||||
|
|
||||||
enum MacHidden : String {
|
enum MacHidden : String {
|
||||||
case never
|
case never
|
||||||
case always
|
case always
|
||||||
|
@ -253,6 +253,9 @@ extension Notification.Name {
|
|||||||
|
|
||||||
/// Resize the window to a default size.
|
/// Resize the window to a default size.
|
||||||
static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize")
|
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
|
// 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
|
@EnvironmentObject private var ghostty: Ghostty.App
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
var result = surfaceView.title
|
||||||
|
if (surfaceView.bell) {
|
||||||
|
result = "🔔 \(result)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
|
|
||||||
@ -74,7 +83,7 @@ extension Ghostty {
|
|||||||
|
|
||||||
Surface(view: surfaceView, size: geo.size)
|
Surface(view: surfaceView, size: geo.size)
|
||||||
.focused($surfaceFocus)
|
.focused($surfaceFocus)
|
||||||
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
.focusedValue(\.ghosttySurfaceTitle, title)
|
||||||
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
|
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
|
||||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||||
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
|
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
|
||||||
|
@ -63,6 +63,9 @@ extension Ghostty {
|
|||||||
/// dynamically updated. Otherwise, the background color is the default background color.
|
/// dynamically updated. Otherwise, the background color is the default background color.
|
||||||
@Published private(set) var backgroundColor: Color? = nil
|
@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
|
// An initial size to request for a window. This will only affect
|
||||||
// then the view is moved to a new window.
|
// then the view is moved to a new window.
|
||||||
var initialSize: NSSize? = nil
|
var initialSize: NSSize? = nil
|
||||||
@ -190,6 +193,11 @@ extension Ghostty {
|
|||||||
selector: #selector(ghosttyColorDidChange(_:)),
|
selector: #selector(ghosttyColorDidChange(_:)),
|
||||||
name: .ghosttyColorDidChange,
|
name: .ghosttyColorDidChange,
|
||||||
object: self)
|
object: self)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(ghosttyBellDidRing(_:)),
|
||||||
|
name: .ghosttyBellDidRing,
|
||||||
|
object: self)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(windowDidChangeScreen),
|
selector: #selector(windowDidChangeScreen),
|
||||||
@ -300,9 +308,12 @@ extension Ghostty {
|
|||||||
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
|
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On macOS 13+ we can store our continuous clock...
|
|
||||||
if (focused) {
|
if (focused) {
|
||||||
|
// On macOS 13+ we can store our continuous clock...
|
||||||
focusInstant = ContinuousClock.now
|
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) {
|
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||||
@ -855,6 +871,9 @@ extension Ghostty {
|
|||||||
return
|
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
|
// We need to translate the mods (maybe) to handle configs such as option-as-alt
|
||||||
let translationModsGhostty = Ghostty.eventModifierFlags(
|
let translationModsGhostty = Ghostty.eventModifierFlags(
|
||||||
mods: ghostty_surface_key_translation_mods(
|
mods: ghostty_surface_key_translation_mods(
|
||||||
|
@ -35,6 +35,9 @@ extension Ghostty {
|
|||||||
// on supported platforms.
|
// on supported platforms.
|
||||||
@Published var focusInstant: ContinuousClock.Instant? = nil
|
@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
|
// Returns sizing information for the surface. This is the raw C
|
||||||
// structure because I'm lazy.
|
// structure because I'm lazy.
|
||||||
var surfaceSize: ghostty_surface_size_s? {
|
var surfaceSize: ghostty_surface_size_s? {
|
||||||
|
@ -1874,7 +1874,13 @@ keybind: Keybinds = .{},
|
|||||||
/// for instance under the "Sound > Alert Sound" setting in GNOME,
|
/// for instance under the "Sound > Alert Sound" setting in GNOME,
|
||||||
/// or the "Accessibility > System Bell" settings in KDE Plasma.
|
/// 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 = .{},
|
@"bell-features": BellFeatures = .{},
|
||||||
|
|
||||||
/// Control the in-app notifications that Ghostty shows.
|
/// Control the in-app notifications that Ghostty shows.
|
||||||
|
Reference in New Issue
Block a user