Merge c4e1ab8883 into bfe633a948
commit
9a33a11976
|
|
@ -934,6 +934,7 @@ typedef enum {
|
|||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||
GHOSTTY_ACTION_CLOSE_WINDOW,
|
||||
GHOSTTY_ACTION_RING_BELL,
|
||||
GHOSTTY_ACTION_SELECTION_CHANGED,
|
||||
GHOSTTY_ACTION_UNDO,
|
||||
GHOSTTY_ACTION_REDO,
|
||||
GHOSTTY_ACTION_CHECK_FOR_UPDATES,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in New Issue