mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #266 from mitchellh/primary-clipboard
Primary (selection) clipboard support
This commit is contained in:
@ -29,6 +29,11 @@ typedef void *ghostty_config_t;
|
||||
typedef void *ghostty_surface_t;
|
||||
|
||||
// Enums are up top so we can reference them later.
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_STANDARD,
|
||||
GHOSTTY_CLIPBOARD_SELECTION,
|
||||
} ghostty_clipboard_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_SPLIT_RIGHT,
|
||||
GHOSTTY_SPLIT_DOWN
|
||||
@ -238,8 +243,8 @@ typedef struct {
|
||||
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
|
||||
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *);
|
||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void *, bool);
|
||||
typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e);
|
||||
@ -248,6 +253,7 @@ typedef void (*ghostty_runtime_toggle_fullscreen_cb)(void *, bool);
|
||||
|
||||
typedef struct {
|
||||
void *userdata;
|
||||
bool supports_selection_clipboard;
|
||||
ghostty_runtime_wakeup_cb wakeup_cb;
|
||||
ghostty_runtime_reload_config_cb reload_config_cb;
|
||||
ghostty_runtime_set_title_cb set_title_cb;
|
||||
|
@ -54,11 +54,12 @@ extension Ghostty {
|
||||
// uses to interface with the application runtime environment.
|
||||
var runtime_cfg = ghostty_runtime_config_s(
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
supports_selection_clipboard: false,
|
||||
wakeup_cb: { userdata in AppState.wakeup(userdata) },
|
||||
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
|
||||
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
|
||||
read_clipboard_cb: { userdata in AppState.readClipboard(userdata) },
|
||||
write_clipboard_cb: { userdata, str in AppState.writeClipboard(userdata, string: str) },
|
||||
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) },
|
||||
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
||||
new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) },
|
||||
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
|
||||
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
|
||||
@ -170,7 +171,10 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
static func readClipboard(_ userdata: UnsafeMutableRawPointer?) -> UnsafePointer<CChar>? {
|
||||
static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e) -> UnsafePointer<CChar>? {
|
||||
// We only support the standard clipboard
|
||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return nil }
|
||||
|
||||
guard let appState = self.appState(fromSurface: userdata) else { return nil }
|
||||
guard let str = NSPasteboard.general.string(forType: .string) else { return nil }
|
||||
|
||||
@ -180,7 +184,10 @@ extension Ghostty {
|
||||
return (str as NSString).utf8String
|
||||
}
|
||||
|
||||
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?) {
|
||||
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e) {
|
||||
// We only support the standard clipboard
|
||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
|
||||
|
||||
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
||||
let pb = NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
|
193
src/Surface.zig
193
src/Surface.zig
@ -144,6 +144,7 @@ const DerivedConfig = struct {
|
||||
clipboard_read: bool,
|
||||
clipboard_write: bool,
|
||||
clipboard_trim_trailing_spaces: bool,
|
||||
copy_on_select: configpkg.CopyOnSelect,
|
||||
confirm_close_surface: bool,
|
||||
mouse_interval: u64,
|
||||
macos_non_native_fullscreen: bool,
|
||||
@ -159,6 +160,7 @@ const DerivedConfig = struct {
|
||||
.clipboard_read = config.@"clipboard-read",
|
||||
.clipboard_write = config.@"clipboard-write",
|
||||
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
|
||||
.copy_on_select = config.@"copy-on-select",
|
||||
.confirm_close_surface = config.@"confirm-close-surface",
|
||||
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
||||
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
|
||||
@ -601,11 +603,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
.clipboard_read => |kind| try self.clipboardRead(kind),
|
||||
|
||||
.clipboard_write => |req| switch (req) {
|
||||
.small => |v| try self.clipboardWrite(v.data[0..v.len]),
|
||||
.stable => |v| try self.clipboardWrite(v),
|
||||
.small => |v| try self.clipboardWrite(v.data[0..v.len], .standard),
|
||||
.stable => |v| try self.clipboardWrite(v, .standard),
|
||||
.alloc => |v| {
|
||||
defer v.alloc.free(v.data);
|
||||
try self.clipboardWrite(v.data);
|
||||
try self.clipboardWrite(v.data, .standard);
|
||||
},
|
||||
},
|
||||
|
||||
@ -719,13 +721,60 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
return .{ .x = x, .y = y };
|
||||
}
|
||||
|
||||
/// Paste from the clipboard
|
||||
fn clipboardPaste(
|
||||
self: *Surface,
|
||||
loc: apprt.Clipboard,
|
||||
lock: bool,
|
||||
) !void {
|
||||
const data = self.rt_surface.getClipboardString(loc) catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
if (data.len > 0) {
|
||||
const bracketed = bracketed: {
|
||||
if (lock) self.renderer_state.mutex.lock();
|
||||
defer if (lock) self.renderer_state.mutex.unlock();
|
||||
|
||||
// With the lock held, we must scroll to the bottom.
|
||||
// We always scroll to the bottom for these inputs.
|
||||
self.scrollToBottom() catch |err| {
|
||||
log.warn("error scrolling to bottom err={}", .{err});
|
||||
};
|
||||
|
||||
break :bracketed self.io.terminal.modes.bracketed_paste;
|
||||
};
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[200~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
data,
|
||||
), .{ .forever = {} });
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[201~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
try self.io_thread.wakeup.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// This is similar to clipboardPaste but is used specifically for OSC 52
|
||||
fn clipboardRead(self: *const Surface, kind: u8) !void {
|
||||
if (!self.config.clipboard_read) {
|
||||
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = self.rt_surface.getClipboardString() catch |err| {
|
||||
const data = self.rt_surface.getClipboardString(.standard) catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -755,7 +804,7 @@ fn clipboardRead(self: *const Surface, kind: u8) !void {
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
fn clipboardWrite(self: *const Surface, data: []const u8) !void {
|
||||
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
|
||||
if (!self.config.clipboard_write) {
|
||||
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
|
||||
return;
|
||||
@ -773,7 +822,52 @@ fn clipboardWrite(self: *const Surface, data: []const u8) !void {
|
||||
try dec.decode(buf, data);
|
||||
assert(buf[buf.len] == 0);
|
||||
|
||||
self.rt_surface.setClipboardString(buf) catch |err| {
|
||||
self.rt_surface.setClipboardString(buf, loc) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the selection contents.
|
||||
///
|
||||
/// This must be called with the renderer mutex held.
|
||||
fn setSelection(self: *Surface, sel_: ?terminal.Selection) void {
|
||||
const prev_ = self.io.terminal.screen.selection;
|
||||
self.io.terminal.screen.selection = sel_;
|
||||
|
||||
// Determine the clipboard we want to copy selection to, if it is enabled.
|
||||
const clipboard: apprt.Clipboard = switch (self.config.copy_on_select) {
|
||||
.false => return,
|
||||
.true => .selection,
|
||||
.clipboard => .standard,
|
||||
};
|
||||
|
||||
// Set our selection clipboard. If the selection is cleared we do not
|
||||
// clear the clipboard. If the selection is set, we only set the clipboard
|
||||
// again if it changed, since setting the clipboard can be an expensive
|
||||
// operation.
|
||||
const sel = sel_ orelse return;
|
||||
if (prev_) |prev| if (std.meta.eql(sel, prev)) return;
|
||||
|
||||
// Check if our runtime supports the selection clipboard at all.
|
||||
// We can save a lot of work if it doesn't.
|
||||
if (@hasDecl(apprt.runtime.Surface, "supportsClipboard")) {
|
||||
if (!self.rt_surface.supportsClipboard(clipboard)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var buf = self.io.terminal.screen.selectionString(
|
||||
self.alloc,
|
||||
sel,
|
||||
self.config.clipboard_trim_trailing_spaces,
|
||||
) catch |err| {
|
||||
log.err("error reading selection string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
self.rt_surface.setClipboardString(buf, clipboard) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -905,7 +999,7 @@ pub fn charCallback(self: *Surface, codepoint: u21) !void {
|
||||
|
||||
// Clear the selection if we have one.
|
||||
if (self.io.terminal.screen.selection != null) {
|
||||
self.io.terminal.screen.selection = null;
|
||||
self.setSelection(null);
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@ -1212,7 +1306,7 @@ pub fn scrollCallback(
|
||||
// The selection can occur if the user uses the shift mod key to
|
||||
// override mouse grabbing from the window.
|
||||
if (self.io.terminal.modes.mouse_event != .none) {
|
||||
self.io.terminal.screen.selection = null;
|
||||
self.setSelection(null);
|
||||
}
|
||||
|
||||
// If we're in alternate screen with alternate scroll enabled, then
|
||||
@ -1523,7 +1617,7 @@ pub fn mouseButtonCallback(
|
||||
// In any other mouse button scenario without shift pressed we
|
||||
// clear the selection since the underlying application can handle
|
||||
// that in any way (i.e. "scrolling").
|
||||
self.io.terminal.screen.selection = null;
|
||||
self.setSelection(null);
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
|
||||
@ -1591,7 +1685,7 @@ pub fn mouseButtonCallback(
|
||||
switch (self.mouse.left_click_count) {
|
||||
// First mouse click, clear selection
|
||||
1 => if (self.io.terminal.screen.selection != null) {
|
||||
self.io.terminal.screen.selection = null;
|
||||
self.setSelection(null);
|
||||
try self.queueRender();
|
||||
},
|
||||
|
||||
@ -1599,7 +1693,7 @@ pub fn mouseButtonCallback(
|
||||
2 => {
|
||||
const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point);
|
||||
if (sel_) |sel| {
|
||||
self.io.terminal.screen.selection = sel;
|
||||
self.setSelection(sel);
|
||||
try self.queueRender();
|
||||
}
|
||||
},
|
||||
@ -1608,7 +1702,7 @@ pub fn mouseButtonCallback(
|
||||
3 => {
|
||||
const sel_ = self.io.terminal.screen.selectLine(self.mouse.left_click_point);
|
||||
if (sel_) |sel| {
|
||||
self.io.terminal.screen.selection = sel;
|
||||
self.setSelection(sel);
|
||||
try self.queueRender();
|
||||
}
|
||||
},
|
||||
@ -1617,6 +1711,18 @@ pub fn mouseButtonCallback(
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// Middle-click pastes from our selection clipboard
|
||||
if (button == .middle and action == .press) {
|
||||
if (self.config.copy_on_select != .false) {
|
||||
const clipboard: apprt.Clipboard = switch (self.config.copy_on_select) {
|
||||
.true => .selection,
|
||||
.clipboard => .standard,
|
||||
.false => unreachable,
|
||||
};
|
||||
try self.clipboardPaste(clipboard, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursorPosCallback(
|
||||
@ -1705,7 +1811,7 @@ fn dragLeftClickDouble(
|
||||
// We may not have a selection if we started our dbl-click in an area
|
||||
// that had no data, then we dragged our mouse into an area with data.
|
||||
var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse {
|
||||
self.io.terminal.screen.selection = word;
|
||||
self.setSelection(word);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -1715,7 +1821,7 @@ fn dragLeftClickDouble(
|
||||
} else {
|
||||
sel.end = word.end;
|
||||
}
|
||||
self.io.terminal.screen.selection = sel;
|
||||
self.setSelection(sel);
|
||||
}
|
||||
|
||||
/// Triple-click dragging moves the selection one "line" at a time.
|
||||
@ -1730,7 +1836,7 @@ fn dragLeftClickTriple(
|
||||
// We may not have a selection if we started our dbl-click in an area
|
||||
// that had no data, then we dragged our mouse into an area with data.
|
||||
var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse {
|
||||
self.io.terminal.screen.selection = word;
|
||||
self.setSelection(word);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -1740,7 +1846,7 @@ fn dragLeftClickTriple(
|
||||
} else {
|
||||
sel.end = word.end;
|
||||
}
|
||||
self.io.terminal.screen.selection = sel;
|
||||
self.setSelection(sel);
|
||||
}
|
||||
|
||||
fn dragLeftClickSingle(
|
||||
@ -1763,7 +1869,7 @@ fn dragLeftClickSingle(
|
||||
else
|
||||
screen_point.before(sel.start);
|
||||
|
||||
if (reset) self.io.terminal.screen.selection = null;
|
||||
if (reset) self.setSelection(null);
|
||||
}
|
||||
|
||||
// Our logic for determining if the starting cell is selected:
|
||||
@ -1798,10 +1904,10 @@ fn dragLeftClickSingle(
|
||||
else
|
||||
cell_xpos < cell_xboundary;
|
||||
|
||||
self.io.terminal.screen.selection = if (selected) .{
|
||||
self.setSelection(if (selected) .{
|
||||
.start = screen_point,
|
||||
.end = screen_point,
|
||||
} else null;
|
||||
} else null);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -1840,7 +1946,7 @@ fn dragLeftClickSingle(
|
||||
}
|
||||
};
|
||||
|
||||
self.io.terminal.screen.selection = .{ .start = start, .end = screen_point };
|
||||
self.setSelection(.{ .start = start, .end = screen_point });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1850,7 +1956,9 @@ fn dragLeftClickSingle(
|
||||
// We moved! Set the selection end point. The start point should be
|
||||
// set earlier.
|
||||
assert(self.io.terminal.screen.selection != null);
|
||||
self.io.terminal.screen.selection.?.end = screen_point;
|
||||
var sel = self.io.terminal.screen.selection.?;
|
||||
sel.end = screen_point;
|
||||
self.setSelection(sel);
|
||||
}
|
||||
|
||||
fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport {
|
||||
@ -1972,53 +2080,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void
|
||||
};
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
self.rt_surface.setClipboardString(buf) catch |err| {
|
||||
self.rt_surface.setClipboardString(buf, .standard) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
.paste_from_clipboard => {
|
||||
const data = self.rt_surface.getClipboardString() catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
if (data.len > 0) {
|
||||
const bracketed = bracketed: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// With the lock held, we must scroll to the bottom.
|
||||
// We always scroll to the bottom for these inputs.
|
||||
self.scrollToBottom() catch |err| {
|
||||
log.warn("error scrolling to bottom err={}", .{err});
|
||||
};
|
||||
|
||||
break :bracketed self.io.terminal.modes.bracketed_paste;
|
||||
};
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[200~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
data,
|
||||
), .{ .forever = {} });
|
||||
|
||||
if (bracketed) {
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_stable = "\x1B[201~",
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
try self.io_thread.wakeup.notify();
|
||||
}
|
||||
},
|
||||
.paste_from_clipboard => try self.clipboardPaste(.standard, true),
|
||||
|
||||
.increase_font_size => |delta| {
|
||||
log.debug("increase font size={}", .{delta});
|
||||
|
@ -32,6 +32,9 @@ pub const App = struct {
|
||||
/// Userdata that is passed to all the callbacks.
|
||||
userdata: AppUD = null,
|
||||
|
||||
/// True if the selection clipboard is supported.
|
||||
supports_selection_clipboard: bool = false,
|
||||
|
||||
/// Callback called to wakeup the event loop. This should trigger
|
||||
/// a full tick of the app loop.
|
||||
wakeup: *const fn (AppUD) callconv(.C) void,
|
||||
@ -47,10 +50,10 @@ pub const App = struct {
|
||||
/// Read the clipboard value. The return value must be preserved
|
||||
/// by the host until the next call. If there is no valid clipboard
|
||||
/// value then this should return null.
|
||||
read_clipboard: *const fn (SurfaceUD) callconv(.C) ?[*:0]const u8,
|
||||
read_clipboard: *const fn (SurfaceUD, c_int) callconv(.C) ?[*:0]const u8,
|
||||
|
||||
/// Write the clipboard value.
|
||||
write_clipboard: *const fn (SurfaceUD, [*:0]const u8) callconv(.C) void,
|
||||
write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int) callconv(.C) void,
|
||||
|
||||
/// Create a new split view. If the embedder doesn't support split
|
||||
/// views then this can be null.
|
||||
@ -239,13 +242,37 @@ pub const Surface = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getClipboardString(self: *const Surface) ![:0]const u8 {
|
||||
const ptr = self.app.opts.read_clipboard(self.opts.userdata) orelse return "";
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
return switch (clipboard_type) {
|
||||
.standard => true,
|
||||
.selection => self.app.opts.supports_selection_clipboard,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getClipboardString(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) ![:0]const u8 {
|
||||
const ptr = self.app.opts.read_clipboard(
|
||||
self.opts.userdata,
|
||||
@intCast(@intFromEnum(clipboard_type)),
|
||||
) orelse return "";
|
||||
return std.mem.sliceTo(ptr, 0);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void {
|
||||
self.app.opts.write_clipboard(self.opts.userdata, val.ptr);
|
||||
pub fn setClipboardString(
|
||||
self: *const Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) !void {
|
||||
self.app.opts.write_clipboard(
|
||||
self.opts.userdata,
|
||||
val.ptr,
|
||||
@intCast(@intFromEnum(clipboard_type)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setShouldClose(self: *Surface) void {
|
||||
|
@ -25,6 +25,7 @@ const DevMode = @import("../DevMode.zig");
|
||||
// Get native API access on certain platforms so we can do more customization.
|
||||
const glfwNative = glfw.Native(.{
|
||||
.cocoa = builtin.target.isDarwin(),
|
||||
.x11 = builtin.os.tag == .linux,
|
||||
});
|
||||
|
||||
const log = std.log.scoped(.glfw);
|
||||
@ -503,15 +504,39 @@ pub const Surface = struct {
|
||||
/// Read the clipboard. The windowing system is responsible for allocating
|
||||
/// a buffer as necessary. This should be a stable pointer until the next
|
||||
/// time getClipboardString is called.
|
||||
pub fn getClipboardString(self: *const Surface) ![:0]const u8 {
|
||||
pub fn getClipboardString(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) ![:0]const u8 {
|
||||
_ = self;
|
||||
return glfw.getClipboardString() orelse return glfw.mustGetErrorCode();
|
||||
return switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse glfw.mustGetErrorCode(),
|
||||
.selection => selection: {
|
||||
// Not supported except on Linux
|
||||
if (comptime builtin.os.tag != .linux) return "";
|
||||
|
||||
const raw = glfwNative.getX11SelectionString() orelse
|
||||
return glfw.mustGetErrorCode();
|
||||
break :selection std.mem.span(raw);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the clipboard.
|
||||
pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void {
|
||||
pub fn setClipboardString(
|
||||
self: *const Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) !void {
|
||||
_ = self;
|
||||
glfw.setClipboardString(val);
|
||||
switch (clipboard_type) {
|
||||
.standard => glfw.setClipboardString(val),
|
||||
.selection => {
|
||||
// Not supported except on Linux
|
||||
if (comptime builtin.os.tag != .linux) return;
|
||||
glfwNative.setX11SelectionString(val.ptr);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor position from glfw directly is in screen coordinates but
|
||||
|
@ -15,6 +15,9 @@ pub const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
});
|
||||
|
||||
// We need native X11 access to access the primary clipboard.
|
||||
const glfw_native = glfw.Native(.{ .x11 = true });
|
||||
|
||||
/// Compatibility with gobject < 2.74
|
||||
const G_CONNECT_DEFAULT = if (@hasDecl(c, "G_CONNECT_DEFAULT"))
|
||||
c.G_CONNECT_DEFAULT
|
||||
@ -913,14 +916,25 @@ pub const Surface = struct {
|
||||
// ));
|
||||
}
|
||||
|
||||
pub fn getClipboardString(self: *Surface) ![:0]const u8 {
|
||||
const clipboard = c.gtk_widget_get_clipboard(@ptrCast(self.gl_area));
|
||||
|
||||
pub fn getClipboardString(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) ![:0]const u8 {
|
||||
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
||||
const content = c.gdk_clipboard_get_content(clipboard) orelse {
|
||||
// On my machine, this NEVER works, so we fallback to glfw's
|
||||
// implementation...
|
||||
// implementation... I believe this never works because we need to
|
||||
// use the async mechanism with GTK but that doesn't play nice
|
||||
// with what our core expects.
|
||||
log.debug("no GTK clipboard contents, falling back to glfw", .{});
|
||||
return glfw.getClipboardString() orelse return glfw.mustGetErrorCode();
|
||||
return switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse glfw.mustGetErrorCode(),
|
||||
.selection => value: {
|
||||
const raw = glfw_native.getX11SelectionString() orelse
|
||||
return glfw.mustGetErrorCode();
|
||||
break :value std.mem.span(raw);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
c.g_value_unset(&self.clipboard);
|
||||
@ -933,12 +947,22 @@ pub const Surface = struct {
|
||||
return std.mem.sliceTo(ptr, 0);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void {
|
||||
const clipboard = c.gtk_widget_get_clipboard(@ptrCast(self.gl_area));
|
||||
|
||||
pub fn setClipboardString(
|
||||
self: *const Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) !void {
|
||||
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
||||
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
||||
}
|
||||
|
||||
fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboard {
|
||||
return switch (clipboard) {
|
||||
.standard => c.gtk_widget_get_clipboard(widget),
|
||||
.selection => c.gtk_widget_get_primary_clipboard(widget),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
|
||||
return self.cursor_pos;
|
||||
}
|
||||
|
@ -23,3 +23,11 @@ pub const IMEPos = struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
};
|
||||
|
||||
/// The clipboard type.
|
||||
///
|
||||
/// If this is changed, you must also update ghostty.h
|
||||
pub const Clipboard = enum(u1) {
|
||||
standard = 0, // ctrl+c/v
|
||||
selection = 1, // also known as the "primary" clipboard
|
||||
};
|
||||
|
@ -186,6 +186,17 @@ pub const Config = struct {
|
||||
/// This does not affect data sent to the clipboard via "clipboard-write".
|
||||
@"clipboard-trim-trailing-spaces": bool = true,
|
||||
|
||||
/// Whether to automatically copy selected text to the clipboard. "true"
|
||||
/// will only copy on systems that support a selection clipboard.
|
||||
///
|
||||
/// The value "clipboard" will copy to the system clipboard, making this
|
||||
/// work on macOS. Note that middle-click will also paste from the system
|
||||
/// clipboard in this case.
|
||||
///
|
||||
/// Note that if this is disabled, middle-click paste will also be
|
||||
/// disabled.
|
||||
@"copy-on-select": CopyOnSelect = .true,
|
||||
|
||||
/// 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 use a platform-specific default. The default on macOS
|
||||
@ -1375,6 +1386,20 @@ pub const Keybinds = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Options for copy on select behavior.
|
||||
pub const CopyOnSelect = enum {
|
||||
/// Disables copy on select entirely.
|
||||
false,
|
||||
|
||||
/// Copy on select is enabled, but goes to the selection clipboard.
|
||||
/// This is not supported on platforms such as macOS. This is the default.
|
||||
true,
|
||||
|
||||
/// Copy on select is enabled and goes to the system clipboard.
|
||||
clipboard,
|
||||
};
|
||||
|
||||
/// Shell integration values
|
||||
pub const ShellIntegration = enum {
|
||||
none,
|
||||
detect,
|
||||
|
@ -1417,7 +1417,7 @@ const StreamHandler = struct {
|
||||
}
|
||||
|
||||
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
|
||||
// Note: we ignore the "kind" field and always use the primary clipboard.
|
||||
// Note: we ignore the "kind" field and always use the standard clipboard.
|
||||
// iTerm also appears to do this but other terminals seem to only allow
|
||||
// certain. Let's investigate more.
|
||||
|
||||
|
Reference in New Issue
Block a user