Configurable right click behavior (#8254)
This MR addresses #4404 following the approach suggested [here](https://github.com/ghostty-org/ghostty/issues/4404#issuecomment-2708410143). Implementing the behavior known e.g. from Putty or Windows Terminal. The following configuration values for `right-click-action` are provided: * `context-menu` - Show the context menu. * `paste` - Paste the contents of the clipboard. * `copy` - Copy the selected text to the clipboard. * `copy-and-paste` - Copy the selected text to the clipboard, paste if nothing is selected. * `ignore` - Do nothing, ignore the right-click. I followed #5935 for getting an idea on where to start. I hope this to be a temporary solution until "bindable mouse bindings" are introduced. This is my first time writing Zig code, so I am happy to incorporate any feedback. Thank you all very much for your work!pull/8288/head
commit
5745f5048c
110
src/Surface.zig
110
src/Surface.zig
|
|
@ -247,6 +247,7 @@ const DerivedConfig = struct {
|
||||||
clipboard_paste_protection: bool,
|
clipboard_paste_protection: bool,
|
||||||
clipboard_paste_bracketed_safe: bool,
|
clipboard_paste_bracketed_safe: bool,
|
||||||
copy_on_select: configpkg.CopyOnSelect,
|
copy_on_select: configpkg.CopyOnSelect,
|
||||||
|
right_click_action: configpkg.RightClickAction,
|
||||||
confirm_close_surface: configpkg.ConfirmCloseSurface,
|
confirm_close_surface: configpkg.ConfirmCloseSurface,
|
||||||
cursor_click_to_move: bool,
|
cursor_click_to_move: bool,
|
||||||
desktop_notifications: bool,
|
desktop_notifications: bool,
|
||||||
|
|
@ -314,6 +315,7 @@ const DerivedConfig = struct {
|
||||||
.clipboard_paste_protection = config.@"clipboard-paste-protection",
|
.clipboard_paste_protection = config.@"clipboard-paste-protection",
|
||||||
.clipboard_paste_bracketed_safe = config.@"clipboard-paste-bracketed-safe",
|
.clipboard_paste_bracketed_safe = config.@"clipboard-paste-bracketed-safe",
|
||||||
.copy_on_select = config.@"copy-on-select",
|
.copy_on_select = config.@"copy-on-select",
|
||||||
|
.right_click_action = config.@"right-click-action",
|
||||||
.confirm_close_surface = config.@"confirm-close-surface",
|
.confirm_close_surface = config.@"confirm-close-surface",
|
||||||
.cursor_click_to_move = config.@"cursor-click-to-move",
|
.cursor_click_to_move = config.@"cursor-click-to-move",
|
||||||
.desktop_notifications = config.@"desktop-notifications",
|
.desktop_notifications = config.@"desktop-notifications",
|
||||||
|
|
@ -1833,6 +1835,32 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copySelectionToClipboards(
|
||||||
|
self: *Surface,
|
||||||
|
sel: terminal.Selection,
|
||||||
|
clipboards: []const apprt.Clipboard,
|
||||||
|
) void {
|
||||||
|
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
|
||||||
|
.sel = sel,
|
||||||
|
.trim = self.config.clipboard_trim_trailing_spaces,
|
||||||
|
}) catch |err| {
|
||||||
|
log.err("error reading selection string err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer self.alloc.free(buf);
|
||||||
|
|
||||||
|
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
|
||||||
|
buf,
|
||||||
|
clipboard,
|
||||||
|
false,
|
||||||
|
) catch |err| {
|
||||||
|
log.err(
|
||||||
|
"error setting clipboard string clipboard={} err={}",
|
||||||
|
.{ clipboard, err },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the selection contents.
|
/// Set the selection contents.
|
||||||
///
|
///
|
||||||
/// This must be called with the renderer mutex held.
|
/// This must be called with the renderer mutex held.
|
||||||
|
|
@ -1850,33 +1878,12 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
||||||
const sel = sel_ orelse return;
|
const sel = sel_ orelse return;
|
||||||
if (prev_) |prev| if (sel.eql(prev)) return;
|
if (prev_) |prev| if (sel.eql(prev)) return;
|
||||||
|
|
||||||
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
|
|
||||||
.sel = sel,
|
|
||||||
.trim = self.config.clipboard_trim_trailing_spaces,
|
|
||||||
}) catch |err| {
|
|
||||||
log.err("error reading selection string err={}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
defer self.alloc.free(buf);
|
|
||||||
|
|
||||||
// Set the clipboard. This is not super DRY but it is clear what
|
|
||||||
// we're doing for each setting without being clever.
|
|
||||||
switch (self.config.copy_on_select) {
|
switch (self.config.copy_on_select) {
|
||||||
.false => unreachable, // handled above with an early exit
|
.false => unreachable, // handled above with an early exit
|
||||||
|
|
||||||
// Both standard and selection clipboards are set.
|
// Both standard and selection clipboards are set.
|
||||||
.clipboard => {
|
.clipboard => {
|
||||||
const clipboards: []const apprt.Clipboard = &.{ .standard, .selection };
|
self.copySelectionToClipboards(sel, &.{ .standard, .selection });
|
||||||
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
|
|
||||||
buf,
|
|
||||||
clipboard,
|
|
||||||
false,
|
|
||||||
) catch |err| {
|
|
||||||
log.err(
|
|
||||||
"error setting clipboard string clipboard={} err={}",
|
|
||||||
.{ clipboard, err },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// The selection clipboard is set if supported, otherwise the standard.
|
// The selection clipboard is set if supported, otherwise the standard.
|
||||||
|
|
@ -1885,17 +1892,7 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
||||||
.selection
|
.selection
|
||||||
else
|
else
|
||||||
.standard;
|
.standard;
|
||||||
|
self.copySelectionToClipboards(sel, &.{clipboard});
|
||||||
self.rt_surface.setClipboardString(
|
|
||||||
buf,
|
|
||||||
clipboard,
|
|
||||||
false,
|
|
||||||
) catch |err| {
|
|
||||||
log.err(
|
|
||||||
"error setting clipboard string clipboard={} err={}",
|
|
||||||
.{ clipboard, err },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3582,18 +3579,49 @@ pub fn mouseButtonCallback(
|
||||||
break :pin pin;
|
break :pin pin;
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we already have a selection and the selection contains
|
switch (self.config.right_click_action) {
|
||||||
// where we clicked then we don't want to modify the selection.
|
.ignore => {
|
||||||
if (self.io.terminal.screen.selection) |prev_sel| {
|
// Return early to skip clearing the selection.
|
||||||
if (prev_sel.contains(screen, pin)) break :sel;
|
try self.queueRender();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
.copy => {
|
||||||
|
if (self.io.terminal.screen.selection) |sel| {
|
||||||
|
self.copySelectionToClipboards(sel, &.{.standard});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.@"copy-or-paste" => {
|
||||||
|
if (self.io.terminal.screen.selection) |sel| {
|
||||||
|
self.copySelectionToClipboards(sel, &.{.standard});
|
||||||
|
} else {
|
||||||
|
try self.startClipboardRequest(.standard, .paste);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.paste => {
|
||||||
|
try self.startClipboardRequest(.standard, .paste);
|
||||||
|
},
|
||||||
|
.@"context-menu" => {
|
||||||
|
// If we already have a selection and the selection contains
|
||||||
|
// where we clicked then we don't want to modify the selection.
|
||||||
|
if (self.io.terminal.screen.selection) |prev_sel| {
|
||||||
|
if (prev_sel.contains(screen, pin)) break :sel;
|
||||||
|
|
||||||
// The selection doesn't contain our pin, so we create a new
|
// The selection doesn't contain our pin, so we create a new
|
||||||
// word selection where we clicked.
|
// word selection where we clicked.
|
||||||
|
}
|
||||||
|
|
||||||
|
const sel = screen.selectWord(pin) orelse break :sel;
|
||||||
|
try self.setSelection(sel);
|
||||||
|
try self.queueRender();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const sel = screen.selectWord(pin) orelse break :sel;
|
try self.setSelection(null);
|
||||||
try self.setSelection(sel);
|
|
||||||
try self.queueRender();
|
try self.queueRender();
|
||||||
|
|
||||||
|
// Consume the event such that the context menu is not displayed.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub const ClipboardAccess = Config.ClipboardAccess;
|
||||||
pub const Command = Config.Command;
|
pub const Command = Config.Command;
|
||||||
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
|
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
|
||||||
pub const CopyOnSelect = Config.CopyOnSelect;
|
pub const CopyOnSelect = Config.CopyOnSelect;
|
||||||
|
pub const RightClickAction = Config.RightClickAction;
|
||||||
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
||||||
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
|
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
|
||||||
pub const FontShapingBreak = Config.FontShapingBreak;
|
pub const FontShapingBreak = Config.FontShapingBreak;
|
||||||
|
|
|
||||||
|
|
@ -1900,6 +1900,19 @@ keybind: Keybinds = .{},
|
||||||
else => .false,
|
else => .false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The action to take when the user right-clicks on the terminal surface.
|
||||||
|
///
|
||||||
|
/// Valid values:
|
||||||
|
/// * `context-menu` - Show the context menu.
|
||||||
|
/// * `paste` - Paste the contents of the clipboard.
|
||||||
|
/// * `copy` - Copy the selected text to the clipboard.
|
||||||
|
/// * `copy-or-paste` - If there is a selection, copy the selected text to
|
||||||
|
/// the clipboard; otherwise, paste the contents of the clipboard.
|
||||||
|
/// * `ignore` - Do nothing, ignore the right-click.
|
||||||
|
///
|
||||||
|
/// The default value is `context-menu`.
|
||||||
|
@"right-click-action": RightClickAction = .@"context-menu",
|
||||||
|
|
||||||
/// The time in milliseconds between clicks to consider a click a repeat
|
/// The time in milliseconds between clicks to consider a click a repeat
|
||||||
/// (double, triple, etc.) or an entirely new single click. A value of zero will
|
/// (double, triple, etc.) or an entirely new single click. A value of zero will
|
||||||
/// use a platform-specific default. The default on macOS is determined by the
|
/// use a platform-specific default. The default on macOS is determined by the
|
||||||
|
|
@ -6709,6 +6722,25 @@ pub const CopyOnSelect = enum {
|
||||||
clipboard,
|
clipboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Options for right-click actions.
|
||||||
|
pub const RightClickAction = enum {
|
||||||
|
/// No action is taken on right-click.
|
||||||
|
ignore,
|
||||||
|
|
||||||
|
/// Pastes from the system clipboard.
|
||||||
|
paste,
|
||||||
|
|
||||||
|
/// Copies the selected text to the system clipboard.
|
||||||
|
copy,
|
||||||
|
|
||||||
|
/// Copies the selected text to the system clipboard and
|
||||||
|
/// pastes the clipboard if no text is selected.
|
||||||
|
@"copy-or-paste",
|
||||||
|
|
||||||
|
/// Shows a context menu with options.
|
||||||
|
@"context-menu",
|
||||||
|
};
|
||||||
|
|
||||||
/// Shell integration values
|
/// Shell integration values
|
||||||
pub const ShellIntegration = enum {
|
pub const ShellIntegration = enum {
|
||||||
none,
|
none,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue