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...pull/7101/head
parent
392aab2e4a
commit
cc690eddb5
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue