mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-15 16:13:56 +00:00
core: send selection_changed notification
The core had no signal to the apprt when the active selection changed, so a consumer (e.g. a screen reader) kept reading a stale selection until some unrelated query refreshed it. This change adds a payload-less selection_changed action that's fired on a selection state transition. The apprt reads the current selection through the normal read path. This consolidates selection state changes so the notification fires consistently: all sites route through setSelection rather than calling screen.select directly, including the mouse paths that previously bypassed it for clipboard timing. The new setSelectionAndCopy extends setSelection with the additional 'copy_on_select' behavior. On macOS, this posts .ghosttySelectionDidChange, which is debounced before posting a NSAccessibility .selectedTextChanged notification. GTK has no consumer yet and no-ops the action.
This commit is contained in:
@@ -620,6 +620,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_RING_BELL:
|
||||
ringBell(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_SELECTION_CHANGED:
|
||||
selectionChanged(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_READONLY:
|
||||
setReadonly(app, target: target, v: action.action.readonly)
|
||||
|
||||
@@ -1070,6 +1073,27 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func selectionChanged(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) {
|
||||
switch target.tag {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("selection changed 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: .ghosttySelectionDidChange,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func setReadonly(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
||||
@@ -360,6 +360,9 @@ extension Notification.Name {
|
||||
/// Ring the bell
|
||||
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
|
||||
|
||||
/// The active selection changed
|
||||
static let ghosttySelectionDidChange = Notification.Name("com.mitchellh.ghostty.ghosttySelectionDidChange")
|
||||
|
||||
/// Readonly mode changed
|
||||
static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly")
|
||||
static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly"
|
||||
|
||||
@@ -76,6 +76,9 @@ extension Ghostty {
|
||||
// Cancellable for search state needle changes
|
||||
private var searchNeedleCancellable: AnyCancellable?
|
||||
|
||||
// Cancellable for the debounced accessibility selection-change post.
|
||||
private var accessibilitySelectionCancellable: AnyCancellable?
|
||||
|
||||
// Whether the pointer should be visible or not
|
||||
@Published private(set) var pointerStyle: CursorStyle = .horizontalText
|
||||
|
||||
@@ -286,6 +289,16 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// A drag can emit multiple selection changes. Debounce so screen
|
||||
// readers hear one announcement once the selection settles.
|
||||
accessibilitySelectionCancellable = NotificationCenter.default
|
||||
.publisher(for: .ghosttySelectionDidChange, object: self)
|
||||
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
NSAccessibility.post(element: self, notification: .selectedTextChanged)
|
||||
}
|
||||
|
||||
// Before we initialize the surface we want to register our notifications
|
||||
// so there is no window where we can't receive them.
|
||||
let center = NotificationCenter.default
|
||||
|
||||
Reference in New Issue
Block a user