From f3996ff0f872938038729574e565aa5100d374a6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 13:41:22 -0700 Subject: [PATCH 1/7] apprt: primary clipboard awareness (selection clipboard) --- src/Surface.zig | 16 ++++++++-------- src/apprt/gtk.zig | 24 ++++++++++++++++++------ src/apprt/structs.zig | 8 ++++++++ src/termio/Exec.zig | 2 +- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index ddbc050e6..fc90c96e3 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -601,11 +601,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); }, }, @@ -725,7 +725,7 @@ fn clipboardRead(self: *const Surface, kind: u8) !void { 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 +755,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 +773,7 @@ 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; }; @@ -1972,7 +1972,7 @@ 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; }; @@ -1980,7 +1980,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void }, .paste_from_clipboard => { - const data = self.rt_surface.getClipboardString() catch |err| { + const data = self.rt_surface.getClipboardString(.standard) catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 5c1465ac1..e7ea3ca73 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -913,9 +913,11 @@ 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... @@ -933,12 +935,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; } diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 9ca2434d4..5e379af71 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -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 +}; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index a02428c73..a3d8a45ab 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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. From 347c60d9bdae824f32a837ee07c661a82642ae60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 14:00:48 -0700 Subject: [PATCH 2/7] selection copies to the selection clipboard --- src/Surface.zig | 155 ++++++++++++++++++++++++++++++---------------- src/apprt/gtk.zig | 1 + 2 files changed, 101 insertions(+), 55 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index fc90c96e3..51960d864 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -719,6 +719,53 @@ 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", .{}); @@ -779,6 +826,36 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) }; } +/// 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_; + + // 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; + + 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, .selection) catch |err| { + log.err("error setting clipboard string err={}", .{err}); + return; + }; +} + /// Change the cell size for the terminal grid. This can happen as /// a result of changing the font size at runtime. fn setCellSize(self: *Surface, size: renderer.CellSize) !void { @@ -905,7 +982,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 +1289,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 +1600,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 +1668,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 +1676,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 +1685,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 +1694,11 @@ pub fn mouseButtonCallback( else => unreachable, } } + + // Middle-click pastes from our selection clipboard + if (button == .middle and action == .press) { + try self.clipboardPaste(.selection, false); + } } pub fn cursorPosCallback( @@ -1705,7 +1787,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 +1797,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 +1812,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 +1822,7 @@ fn dragLeftClickTriple( } else { sel.end = word.end; } - self.io.terminal.screen.selection = sel; + self.setSelection(sel); } fn dragLeftClickSingle( @@ -1763,7 +1845,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 +1880,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 +1922,7 @@ fn dragLeftClickSingle( } }; - self.io.terminal.screen.selection = .{ .start = start, .end = screen_point }; + self.setSelection(.{ .start = start, .end = screen_point }); return; } @@ -1850,7 +1932,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 { @@ -1979,46 +2063,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void } }, - .paste_from_clipboard => { - const data = self.rt_surface.getClipboardString(.standard) 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}); diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index e7ea3ca73..acfb6bdc9 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -940,6 +940,7 @@ pub const Surface = struct { val: [:0]const u8, clipboard_type: apprt.Clipboard, ) !void { + log.warn("SETTING CLIPBOARD: {s}", .{val}); const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); c.gdk_clipboard_set_text(clipboard, val.ptr); } From f012b908b77260b89267f1269542923a58539647 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 14:17:03 -0700 Subject: [PATCH 3/7] apprt/gtk: use glfw for reading primary clipboard (see comment) --- src/apprt/gtk.zig | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index acfb6bdc9..e1f7be92f 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -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 @@ -920,9 +923,18 @@ pub const Surface = struct { 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); @@ -940,7 +952,6 @@ pub const Surface = struct { val: [:0]const u8, clipboard_type: apprt.Clipboard, ) !void { - log.warn("SETTING CLIPBOARD: {s}", .{val}); const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); c.gdk_clipboard_set_text(clipboard, val.ptr); } From ca008df73d5ecd07b3cde82844581f1bf2743901 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 14:22:17 -0700 Subject: [PATCH 4/7] apprt/glfw: support primary clipboard --- src/apprt/glfw.zig | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 0cef7251f..6705dc7e2 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -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); @@ -496,15 +497,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 From afc6a9976f2f8c794a8e979c2091124210db4b29 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 14:29:39 -0700 Subject: [PATCH 5/7] apprt/embedded: support selection clipboard --- include/ghostty.h | 9 +++++++-- macos/Sources/Ghostty/AppState.swift | 14 ++++++++++---- src/apprt/embedded.zig | 26 ++++++++++++++++++++------ src/apprt/glfw.zig | 2 +- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 5b75ea1ad..da767c37f 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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); diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 4f5a1358d..3d15c3bc5 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -57,8 +57,8 @@ extension Ghostty { 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 +170,10 @@ extension Ghostty { ) } - static func readClipboard(_ userdata: UnsafeMutableRawPointer?) -> UnsafePointer? { + static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e) -> UnsafePointer? { + // 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 +183,10 @@ extension Ghostty { return (str as NSString).utf8String } - static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?) { + static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, 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) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 5c4c7bbc5..fe9016ab6 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -47,10 +47,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 +239,27 @@ 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 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 { diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 6705dc7e2..625355cae 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -526,7 +526,7 @@ pub const Surface = struct { .standard => glfw.setClipboardString(val), .selection => { // Not supported except on Linux - if (comptime builtin.os.tag != .linux) return ""; + if (comptime builtin.os.tag != .linux) return; glfwNative.setX11SelectionString(val.ptr); }, } From 5d6086a1b11cfb66e99a7a6b3aa9ce2147c2cb9d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 14:44:24 -0700 Subject: [PATCH 6/7] "copy-on-select" configuation to disable --- src/Surface.zig | 20 ++++++++++++++++++-- src/config.zig | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 51960d864..572fbc721 100644 --- a/src/Surface.zig +++ b/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", @@ -833,6 +835,13 @@ 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 @@ -850,7 +859,7 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) void { }; defer self.alloc.free(buf); - self.rt_surface.setClipboardString(buf, .selection) catch |err| { + self.rt_surface.setClipboardString(buf, clipboard) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; @@ -1697,7 +1706,14 @@ pub fn mouseButtonCallback( // Middle-click pastes from our selection clipboard if (button == .middle and action == .press) { - try self.clipboardPaste(.selection, false); + 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); + } } } diff --git a/src/config.zig b/src/config.zig index b5afc0684..ae41072a0 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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, From 688ab846617b4196fff9726c06506c7c22c65cc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Aug 2023 14:52:22 -0700 Subject: [PATCH 7/7] apprt/embedded: allow noting that selection clipboard is not supported --- include/ghostty.h | 1 + macos/Sources/Ghostty/AppState.swift | 1 + src/Surface.zig | 8 ++++++++ src/apprt/embedded.zig | 13 +++++++++++++ 4 files changed, 23 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index da767c37f..664ae8011 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -253,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; diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 3d15c3bc5..3bc180288 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -54,6 +54,7 @@ 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) }, diff --git a/src/Surface.zig b/src/Surface.zig index 572fbc721..d40a1dfa5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -849,6 +849,14 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) void { 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, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index fe9016ab6..ca6b0eaa9 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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, @@ -239,6 +242,16 @@ pub const Surface = struct { ); } + 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,