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:
@@ -1217,7 +1217,7 @@ fn selectionScrollTick(self: *Surface) !void {
|
||||
|
||||
// We modified our viewport and selection so we need to queue
|
||||
// a render.
|
||||
try self.io.terminal.screens.active.select(selection);
|
||||
try self.setSelection(selection);
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -2326,19 +2326,46 @@ fn copySelectionToClipboards(
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the selection contents.
|
||||
/// Set the active selection and notify the apprt on a genuine state
|
||||
/// transition. All selection mutations route through here rather than
|
||||
/// `screen.select` directly so the notification fires consistently. To
|
||||
/// also copy per `copy_on_select`, use `setSelectionAndCopy`.
|
||||
///
|
||||
/// This must be called with the renderer mutex held.
|
||||
fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
||||
// Compute the transition before `select` below, which untracks (frees)
|
||||
// the previous selection's tracked pins; reading them after would be a
|
||||
// use-after-free.
|
||||
const prev_ = self.io.terminal.screens.active.selection;
|
||||
const changed = changed: {
|
||||
const prev = prev_ orelse break :changed sel_ != null;
|
||||
const sel = sel_ orelse break :changed true;
|
||||
break :changed !sel.eql(prev);
|
||||
};
|
||||
|
||||
try self.io.terminal.screens.active.select(sel_);
|
||||
|
||||
if (changed) {
|
||||
_ = self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.selection_changed,
|
||||
{},
|
||||
) catch |err| {
|
||||
log.warn("apprt failed selection_changed notification err={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a selection and, per `copy_on_select`, copy it to the clipboard.
|
||||
/// For committing selection gestures (mouse release, select-all binding).
|
||||
///
|
||||
/// This must be called with the renderer mutex held.
|
||||
fn setSelectionAndCopy(self: *Surface, sel: terminal.Selection) !void {
|
||||
try self.setSelection(sel);
|
||||
|
||||
// If copy on select is false then exit early.
|
||||
if (self.config.copy_on_select == .false) return;
|
||||
|
||||
// Set our selection clipboard. If the selection is cleared we do not
|
||||
// clear the clipboard.
|
||||
const sel = sel_ orelse return;
|
||||
|
||||
switch (self.config.copy_on_select) {
|
||||
.false => unreachable, // handled above with an early exit
|
||||
|
||||
@@ -3836,7 +3863,7 @@ pub fn mouseButtonCallback(
|
||||
if (self.config.copy_on_select != .false) {
|
||||
const prev_ = self.io.terminal.screens.active.selection;
|
||||
if (prev_) |prev| {
|
||||
try self.setSelection(terminal.Selection.init(
|
||||
try self.setSelectionAndCopy(terminal.Selection.init(
|
||||
prev.start(),
|
||||
prev.end(),
|
||||
prev.rectangle,
|
||||
@@ -3981,16 +4008,16 @@ pub fn mouseButtonCallback(
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
// We set the selection directly rather than use `setSelection` because
|
||||
// we want to avoid copying the selection to the selection clipboard.
|
||||
// For left mouse clicks we only set the clipboard on release.
|
||||
// Use `setSelection` (not `setSelectionAndCopy`) here to avoid
|
||||
// touching the selection clipboard: for left mouse clicks we only
|
||||
// copy on release.
|
||||
if (press_selection) |selection| {
|
||||
try self.io.terminal.screens.active.select(selection);
|
||||
try self.setSelection(selection);
|
||||
try self.queueRender();
|
||||
} else if (self.mouse.selection_gesture.left_click_count == 1 and
|
||||
self.io.terminal.screens.active.selection != null)
|
||||
{
|
||||
try self.io.terminal.screens.active.select(null);
|
||||
try self.setSelection(null);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
@@ -4056,13 +4083,13 @@ pub fn mouseButtonCallback(
|
||||
// If there is a link at this position, we want to
|
||||
// select the link. Otherwise, select the word.
|
||||
if (try self.linkAtPos(pos)) |link| {
|
||||
try self.setSelection(link.selection);
|
||||
try self.setSelectionAndCopy(link.selection);
|
||||
} else {
|
||||
const sel = screen.selectWord(
|
||||
pin,
|
||||
self.config.selection_word_chars,
|
||||
) orelse break :sel;
|
||||
try self.setSelection(sel);
|
||||
try self.setSelectionAndCopy(sel);
|
||||
}
|
||||
try self.queueRender();
|
||||
|
||||
@@ -4460,7 +4487,7 @@ pub fn mousePressureCallback(
|
||||
);
|
||||
}
|
||||
|
||||
try self.io.terminal.screens.active.select(sel orelse break :select);
|
||||
try self.setSelection(sel orelse break :select);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
@@ -4665,7 +4692,7 @@ pub fn cursorPosCallback(
|
||||
}
|
||||
|
||||
// Update our selection based on the gesture state
|
||||
try self.io.terminal.screens.active.select(drag_selection);
|
||||
try self.setSelection(drag_selection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5416,7 +5443,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
|
||||
const sel = self.io.terminal.screens.active.selectAll();
|
||||
if (sel) |s| {
|
||||
try self.setSelection(s);
|
||||
try self.setSelectionAndCopy(s);
|
||||
try self.queueRender();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -295,6 +295,10 @@ pub const Action = union(Key) {
|
||||
/// it needs to ring the bell. This is usually a sound or visual effect.
|
||||
ring_bell,
|
||||
|
||||
/// Called when the active selection changes. The apprt should read the
|
||||
/// current selection itself; this carries no payload.
|
||||
selection_changed,
|
||||
|
||||
/// Undo the last action. See the "undo" keybinding for more
|
||||
/// details on what can and cannot be undone.
|
||||
undo,
|
||||
@@ -396,6 +400,7 @@ pub const Action = union(Key) {
|
||||
config_change,
|
||||
close_window,
|
||||
ring_bell,
|
||||
selection_changed,
|
||||
undo,
|
||||
redo,
|
||||
check_for_updates,
|
||||
|
||||
@@ -743,6 +743,9 @@ pub const Application = extern struct {
|
||||
|
||||
.ring_bell => Action.ringBell(target),
|
||||
|
||||
// GTK has no accessibility consumer for this yet.
|
||||
.selection_changed => {},
|
||||
|
||||
.scrollbar => Action.scrollbar(target, value),
|
||||
|
||||
.set_title => Action.setTitle(target, value),
|
||||
|
||||
Reference in New Issue
Block a user