Copy formatted text to clipboard with plain, make it configurable (#9418)
Fixes #9397 This makes `copy_to_clipboard` take an optional parameter with the format to copy. **The default has changed to `mixed`,** which will set multiple content types on the clipboard allowing the OS or target application to choose what they prefer. In this case, we set both `text/plain` and `text/html`. This only includes the macOS implementation. The GTK side still needs to be done, but is likely trivial to do. https://github.com/user-attachments/assets/b1b2f5cd-d59a-496e-bb77-86a60571ed7fpull/9423/head
commit
bc0f5e4d57
|
|
@ -45,6 +45,11 @@ typedef enum {
|
|||
GHOSTTY_CLIPBOARD_SELECTION,
|
||||
} ghostty_clipboard_e;
|
||||
|
||||
typedef struct {
|
||||
const char *mime;
|
||||
const char *data;
|
||||
} ghostty_clipboard_content_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
|
||||
|
|
@ -855,8 +860,9 @@ typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
|
|||
void*,
|
||||
ghostty_clipboard_request_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
|
||||
const char*,
|
||||
ghostty_clipboard_e,
|
||||
const ghostty_clipboard_content_s*,
|
||||
size_t,
|
||||
bool);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
|
||||
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ extension Ghostty {
|
|||
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
|
||||
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
|
||||
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
|
||||
write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
|
||||
write_clipboard_cb: { userdata, loc, content, len, confirm in
|
||||
App.writeClipboard(userdata, location: loc, content: content, len: len, confirm: confirm) },
|
||||
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }
|
||||
)
|
||||
|
||||
|
|
@ -276,8 +277,9 @@ extension Ghostty {
|
|||
|
||||
static func writeClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
string: UnsafePointer<CChar>?,
|
||||
location: ghostty_clipboard_e,
|
||||
content: UnsafePointer<ghostty_clipboard_content_s>?,
|
||||
len: Int,
|
||||
confirm: Bool
|
||||
) {}
|
||||
|
||||
|
|
@ -364,23 +366,53 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e, confirm: Bool) {
|
||||
static func writeClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
location: ghostty_clipboard_e,
|
||||
content: UnsafePointer<ghostty_clipboard_content_s>?,
|
||||
len: Int,
|
||||
confirm: Bool
|
||||
) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else { return }
|
||||
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
||||
guard let content = content, len > 0 else { return }
|
||||
|
||||
// Convert the C array to Swift array
|
||||
let contentArray = (0..<len).compactMap { i in
|
||||
Ghostty.ClipboardContent.from(content: content[i])
|
||||
}
|
||||
guard !contentArray.isEmpty else { return }
|
||||
|
||||
// Assert there is only one text/plain entry. For security reasons we need
|
||||
// to guarantee this for now since our confirmation dialog only shows one.
|
||||
assert(contentArray.filter({ $0.mime == "text/plain" }).count <= 1,
|
||||
"clipboard contents should have at most one text/plain entry")
|
||||
|
||||
if !confirm {
|
||||
pasteboard.declareTypes([.string], owner: nil)
|
||||
pasteboard.setString(valueStr, forType: .string)
|
||||
// Declare all types
|
||||
let types = contentArray.compactMap { item in
|
||||
NSPasteboard.PasteboardType(mimeType: item.mime)
|
||||
}
|
||||
pasteboard.declareTypes(types, owner: nil)
|
||||
|
||||
// Set data for each type
|
||||
for item in contentArray {
|
||||
guard let type = NSPasteboard.PasteboardType(mimeType: item.mime) else { continue }
|
||||
pasteboard.setString(item.data, forType: type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For confirmation, use the text/plain content if it exists
|
||||
guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }) else {
|
||||
return
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.confirmClipboard,
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.ConfirmClipboardStrKey: valueStr,
|
||||
Notification.ConfirmClipboardStrKey: textPlainContent.data,
|
||||
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -299,6 +299,23 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClipboardContent {
|
||||
let mime: String
|
||||
let data: String
|
||||
|
||||
static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? {
|
||||
guard let mimePtr = content.mime,
|
||||
let dataPtr = content.data else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ClipboardContent(
|
||||
mime: String(cString: mimePtr),
|
||||
data: String(cString: dataPtr)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// macos-icon
|
||||
enum MacOSIcon: String {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,30 @@
|
|||
import AppKit
|
||||
import GhosttyKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
extension NSPasteboard.PasteboardType {
|
||||
/// Initialize a pasteboard type from a MIME type string
|
||||
init?(mimeType: String) {
|
||||
// Explicit mappings for common MIME types
|
||||
switch mimeType {
|
||||
case "text/plain":
|
||||
self = .string
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Try to get UTType from MIME type
|
||||
guard let utType = UTType(mimeType: mimeType) else {
|
||||
// Fallback: use the MIME type directly as identifier
|
||||
self.init(mimeType)
|
||||
return
|
||||
}
|
||||
|
||||
// Use the UTType's identifier
|
||||
self.init(utType.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSPasteboard {
|
||||
/// The pasteboard to used for Ghostty selection.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// NSPasteboardTests.swift
|
||||
// GhosttyTests
|
||||
//
|
||||
// Tests for NSPasteboard.PasteboardType MIME type conversion.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import AppKit
|
||||
@testable import Ghostty
|
||||
|
||||
struct NSPasteboardTypeExtensionTests {
|
||||
/// Test text/plain MIME type converts to .string
|
||||
@Test func testTextPlainMimeType() async throws {
|
||||
let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/plain")
|
||||
#expect(pasteboardType != nil)
|
||||
#expect(pasteboardType == .string)
|
||||
}
|
||||
|
||||
/// Test text/html MIME type converts to .html
|
||||
@Test func testTextHtmlMimeType() async throws {
|
||||
let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/html")
|
||||
#expect(pasteboardType != nil)
|
||||
#expect(pasteboardType == .html)
|
||||
}
|
||||
|
||||
/// Test image/png MIME type
|
||||
@Test func testImagePngMimeType() async throws {
|
||||
let pasteboardType = NSPasteboard.PasteboardType(mimeType: "image/png")
|
||||
#expect(pasteboardType != nil)
|
||||
#expect(pasteboardType == .png)
|
||||
}
|
||||
}
|
||||
179
src/Surface.zig
179
src/Surface.zig
|
|
@ -1945,7 +1945,10 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
|
|||
// them to confirm the clipboard access. Each app runtime handles this
|
||||
// differently.
|
||||
const confirm = self.config.clipboard_write == .ask;
|
||||
self.rt_surface.setClipboardString(buf, loc, confirm) catch |err| {
|
||||
self.rt_surface.setClipboard(loc, &.{.{
|
||||
.mime = "text/plain",
|
||||
.data = buf,
|
||||
}}, confirm) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
|
@ -1955,19 +1958,101 @@ 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);
|
||||
format: input.Binding.Action.CopyToClipboard,
|
||||
) !void {
|
||||
// Create an arena to simplify memory management here.
|
||||
var arena = ArenaAllocator.init(self.alloc);
|
||||
errdefer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
|
||||
buf,
|
||||
// The options we'll use for all formatting. We'll just override the
|
||||
// emit format.
|
||||
const opts: terminal.formatter.Options = .{
|
||||
.emit = .plain, // We'll override this below
|
||||
.unwrap = true,
|
||||
.trim = self.config.clipboard_trim_trailing_spaces,
|
||||
.background = self.io.terminal.colors.background.get(),
|
||||
.foreground = self.io.terminal.colors.foreground.get(),
|
||||
.palette = &self.io.terminal.colors.palette.current,
|
||||
};
|
||||
|
||||
const ScreenFormatter = terminal.formatter.ScreenFormatter;
|
||||
var aw: std.Io.Writer.Allocating = .init(alloc);
|
||||
var contents: std.ArrayList(apprt.ClipboardContent) = .empty;
|
||||
switch (format) {
|
||||
.plain => {
|
||||
var formatter: ScreenFormatter = .init(&self.io.terminal.screen, opts);
|
||||
formatter.content = .{ .selection = sel };
|
||||
try formatter.format(&aw.writer);
|
||||
try contents.append(alloc, .{
|
||||
.mime = "text/plain",
|
||||
.data = try aw.toOwnedSliceSentinel(0),
|
||||
});
|
||||
},
|
||||
|
||||
.vt => {
|
||||
var formatter: ScreenFormatter = .init(&self.io.terminal.screen, opts: {
|
||||
var copy = opts;
|
||||
copy.emit = .vt;
|
||||
break :opts copy;
|
||||
});
|
||||
formatter.content = .{ .selection = sel };
|
||||
try formatter.format(&aw.writer);
|
||||
try contents.append(alloc, .{
|
||||
.mime = "text/plain",
|
||||
.data = try aw.toOwnedSliceSentinel(0),
|
||||
});
|
||||
},
|
||||
|
||||
.html => {
|
||||
var formatter: ScreenFormatter = .init(&self.io.terminal.screen, opts: {
|
||||
var copy = opts;
|
||||
copy.emit = .html;
|
||||
break :opts copy;
|
||||
});
|
||||
formatter.content = .{ .selection = sel };
|
||||
try formatter.format(&aw.writer);
|
||||
try contents.append(alloc, .{
|
||||
.mime = "text/html",
|
||||
.data = try aw.toOwnedSliceSentinel(0),
|
||||
});
|
||||
},
|
||||
|
||||
.mixed => {
|
||||
var formatter: ScreenFormatter = .init(&self.io.terminal.screen, opts);
|
||||
formatter.content = .{ .selection = sel };
|
||||
try formatter.format(&aw.writer);
|
||||
try contents.append(alloc, .{
|
||||
.mime = "text/plain",
|
||||
.data = try aw.toOwnedSliceSentinel(0),
|
||||
});
|
||||
|
||||
assert(aw.written().len == 0);
|
||||
formatter = .init(&self.io.terminal.screen, opts: {
|
||||
var copy = opts;
|
||||
copy.emit = .html;
|
||||
|
||||
// We purposely don't emit background/foreground for mixed
|
||||
// mode because the HTML contents is often used for rich text
|
||||
// input and with trimmed spaces it looks pretty bad.
|
||||
copy.background = null;
|
||||
copy.foreground = null;
|
||||
|
||||
break :opts copy;
|
||||
});
|
||||
formatter.content = .{ .selection = sel };
|
||||
try formatter.format(&aw.writer);
|
||||
try contents.append(alloc, .{
|
||||
.mime = "text/html",
|
||||
.data = try aw.toOwnedSliceSentinel(0),
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
assert(contents.items.len > 0);
|
||||
for (clipboards) |clipboard| self.rt_surface.setClipboard(
|
||||
clipboard,
|
||||
contents.items,
|
||||
false,
|
||||
) catch |err| {
|
||||
log.err(
|
||||
|
|
@ -1998,9 +2083,11 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
|||
.false => unreachable, // handled above with an early exit
|
||||
|
||||
// Both standard and selection clipboards are set.
|
||||
.clipboard => {
|
||||
self.copySelectionToClipboards(sel, &.{ .standard, .selection });
|
||||
},
|
||||
.clipboard => try self.copySelectionToClipboards(
|
||||
sel,
|
||||
&.{ .standard, .selection },
|
||||
.mixed,
|
||||
),
|
||||
|
||||
// The selection clipboard is set if supported, otherwise the standard.
|
||||
.true => {
|
||||
|
|
@ -2008,7 +2095,11 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
|||
.selection
|
||||
else
|
||||
.standard;
|
||||
self.copySelectionToClipboards(sel, &.{clipboard});
|
||||
try self.copySelectionToClipboards(
|
||||
sel,
|
||||
&.{clipboard},
|
||||
.mixed,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -3746,14 +3837,22 @@ pub fn mouseButtonCallback(
|
|||
},
|
||||
.copy => {
|
||||
if (self.io.terminal.screen.selection) |sel| {
|
||||
self.copySelectionToClipboards(sel, &.{.standard});
|
||||
try self.copySelectionToClipboards(
|
||||
sel,
|
||||
&.{.standard},
|
||||
.mixed,
|
||||
);
|
||||
}
|
||||
|
||||
try self.setSelection(null);
|
||||
try self.queueRender();
|
||||
},
|
||||
.@"copy-or-paste" => if (self.io.terminal.screen.selection) |sel| {
|
||||
self.copySelectionToClipboards(sel, &.{.standard});
|
||||
try self.copySelectionToClipboards(
|
||||
sel,
|
||||
&.{.standard},
|
||||
.mixed,
|
||||
);
|
||||
try self.setSelection(null);
|
||||
try self.queueRender();
|
||||
} else {
|
||||
|
|
@ -4657,23 +4756,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
self.renderer_state.terminal.fullReset();
|
||||
},
|
||||
|
||||
.copy_to_clipboard => {
|
||||
.copy_to_clipboard => |format| {
|
||||
// We can read from the renderer state without holding
|
||||
// the lock because only we will write to this field.
|
||||
if (self.io.terminal.screen.selection) |sel| {
|
||||
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 true;
|
||||
};
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
self.rt_surface.setClipboardString(buf, .standard, false) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
try self.copySelectionToClipboards(
|
||||
sel,
|
||||
&.{.standard},
|
||||
format,
|
||||
);
|
||||
|
||||
// Clear the selection if configured to do so.
|
||||
if (self.config.selection_clear_on_copy) {
|
||||
|
|
@ -4721,7 +4812,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
};
|
||||
defer self.alloc.free(url_text);
|
||||
|
||||
self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| {
|
||||
self.rt_surface.setClipboard(.standard, &.{.{
|
||||
.mime = "text/plain",
|
||||
.data = url_text,
|
||||
}}, false) catch |err| {
|
||||
log.err("error copying url to clipboard err={}", .{err});
|
||||
return false;
|
||||
};
|
||||
|
|
@ -4736,7 +4830,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
const title = self.rt_surface.getTitle() orelse return false;
|
||||
if (title.len == 0) return false;
|
||||
|
||||
self.rt_surface.setClipboardString(title, .standard, false) catch |err| {
|
||||
self.rt_surface.setClipboard(.standard, &.{.{
|
||||
.mime = "text/plain",
|
||||
.data = title,
|
||||
}}, false) catch |err| {
|
||||
log.err("error copying title to clipboard err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
|
|
@ -5273,7 +5370,10 @@ fn writeScreenFile(
|
|||
.copy => {
|
||||
const pathZ = try self.alloc.dupeZ(u8, path);
|
||||
defer self.alloc.free(pathZ);
|
||||
try self.rt_surface.setClipboardString(pathZ, .standard, false);
|
||||
try self.rt_surface.setClipboard(.standard, &.{.{
|
||||
.mime = "text/plain",
|
||||
.data = pathZ,
|
||||
}}, false);
|
||||
},
|
||||
.open => try self.openUrl(.{ .kind = .text, .url = path }),
|
||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||
|
|
@ -5313,11 +5413,10 @@ pub fn completeClipboardRequest(
|
|||
confirmed,
|
||||
),
|
||||
|
||||
.osc_52_write => |clipboard| try self.rt_surface.setClipboardString(
|
||||
data,
|
||||
clipboard,
|
||||
!confirmed,
|
||||
),
|
||||
.osc_52_write => |clipboard| try self.rt_surface.setClipboard(clipboard, &.{.{
|
||||
.mime = "text/plain",
|
||||
.data = data,
|
||||
}}, !confirmed),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub const Target = action.Target;
|
|||
|
||||
pub const ContentScale = structs.ContentScale;
|
||||
pub const Clipboard = structs.Clipboard;
|
||||
pub const ClipboardContent = structs.ClipboardContent;
|
||||
pub const ClipboardRequest = structs.ClipboardRequest;
|
||||
pub const ClipboardRequestType = structs.ClipboardRequestType;
|
||||
pub const ColorScheme = structs.ColorScheme;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,13 @@ pub const App = struct {
|
|||
) callconv(.c) void,
|
||||
|
||||
/// Write the clipboard value.
|
||||
write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.c) void,
|
||||
write_clipboard: *const fn (
|
||||
SurfaceUD,
|
||||
c_int,
|
||||
[*]const CAPI.ClipboardContent,
|
||||
usize,
|
||||
bool,
|
||||
) callconv(.c) void,
|
||||
|
||||
/// Close the current surface given by this function.
|
||||
close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null,
|
||||
|
|
@ -699,16 +705,27 @@ pub const Surface = struct {
|
|||
alloc.destroy(state);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
pub fn setClipboard(
|
||||
self: *const Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const array = try alloc.alloc(CAPI.ClipboardContent, contents.len);
|
||||
defer alloc.free(array);
|
||||
for (contents, 0..) |content, i| {
|
||||
array[i] = .{
|
||||
.mime = content.mime,
|
||||
.data = content.data,
|
||||
};
|
||||
}
|
||||
|
||||
self.app.opts.write_clipboard(
|
||||
self.userdata,
|
||||
val.ptr,
|
||||
@intCast(@intFromEnum(clipboard_type)),
|
||||
array.ptr,
|
||||
array.len,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
|
|
@ -1211,6 +1228,12 @@ pub const CAPI = struct {
|
|||
cell_height_px: u32,
|
||||
};
|
||||
|
||||
// ghostty_clipboard_content_s
|
||||
const ClipboardContent = extern struct {
|
||||
mime: [*:0]const u8,
|
||||
data: [*:0]const u8,
|
||||
};
|
||||
|
||||
// ghostty_text_s
|
||||
const Text = extern struct {
|
||||
tl_px_x: f64,
|
||||
|
|
|
|||
|
|
@ -80,15 +80,15 @@ pub fn clipboardRequest(
|
|||
);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
pub fn setClipboard(
|
||||
self: *Self,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) !void {
|
||||
self.surface.setClipboardString(
|
||||
val,
|
||||
self.surface.setClipboard(
|
||||
clipboard_type,
|
||||
contents,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1111,7 +1111,7 @@ pub const Application = extern struct {
|
|||
self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
|
||||
self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
|
||||
self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
|
||||
self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
|
||||
self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = .mixed });
|
||||
self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
|
||||
self.syncActionAccelerator("win.reset", .{ .reset = {} });
|
||||
self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
|
||||
|
|
|
|||
|
|
@ -1553,16 +1553,16 @@ pub const Surface = extern struct {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
pub fn setClipboard(
|
||||
self: *Self,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) void {
|
||||
Clipboard.set(
|
||||
self,
|
||||
val,
|
||||
clipboard_type,
|
||||
contents,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
|
|
@ -3334,12 +3334,19 @@ const Clipboard = struct {
|
|||
/// Set the clipboard contents.
|
||||
pub fn set(
|
||||
self: *Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) void {
|
||||
const priv = self.private();
|
||||
|
||||
// For GTK, we only support text/plain type to set strings currently.
|
||||
const val: [:0]const u8 = for (contents) |content| {
|
||||
if (std.mem.eql(u8, content.mime, "text/plain")) {
|
||||
break content.data;
|
||||
}
|
||||
} else return;
|
||||
|
||||
// If no confirmation is necessary, set the clipboard.
|
||||
if (!confirm) {
|
||||
const clipboard = get(
|
||||
|
|
|
|||
|
|
@ -1801,7 +1801,7 @@ pub const Window = extern struct {
|
|||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.copy_to_clipboard);
|
||||
self.performBindingAction(.{ .copy_to_clipboard = .mixed });
|
||||
}
|
||||
|
||||
fn actionPaste(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ pub const Clipboard = enum(Backing) {
|
|||
};
|
||||
};
|
||||
|
||||
pub const ClipboardContent = struct {
|
||||
mime: [:0]const u8,
|
||||
data: [:0]const u8,
|
||||
};
|
||||
|
||||
pub const ClipboardRequestType = enum(u8) {
|
||||
paste,
|
||||
osc_52_read,
|
||||
|
|
|
|||
|
|
@ -5651,12 +5651,12 @@ pub const Keybinds = struct {
|
|||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .copy } },
|
||||
.{ .copy_to_clipboard = {} },
|
||||
.{ .copy_to_clipboard = .mixed },
|
||||
);
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .paste } },
|
||||
.{ .paste_from_clipboard = {} },
|
||||
.paste_from_clipboard,
|
||||
);
|
||||
|
||||
// On non-MacOS desktop envs (Windows, KDE, Gnome, Xfce), ctrl+insert is an
|
||||
|
|
@ -5669,7 +5669,7 @@ pub const Keybinds = struct {
|
|||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .physical = .insert }, .mods = .{ .ctrl = true } },
|
||||
.{ .copy_to_clipboard = {} },
|
||||
.{ .copy_to_clipboard = .mixed },
|
||||
);
|
||||
try self.set.put(
|
||||
alloc,
|
||||
|
|
@ -5688,7 +5688,7 @@ pub const Keybinds = struct {
|
|||
try self.set.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'c' }, .mods = mods },
|
||||
.{ .copy_to_clipboard = {} },
|
||||
.{ .copy_to_clipboard = .mixed },
|
||||
.{ .performable = true },
|
||||
);
|
||||
try self.set.put(
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ pub const Action = union(enum) {
|
|||
reset,
|
||||
|
||||
/// Copy the selected text to the clipboard.
|
||||
copy_to_clipboard,
|
||||
copy_to_clipboard: CopyToClipboard,
|
||||
|
||||
/// Paste the contents of the default clipboard.
|
||||
paste_from_clipboard,
|
||||
|
|
@ -889,6 +889,19 @@ pub const Action = union(enum) {
|
|||
u16,
|
||||
};
|
||||
|
||||
pub const CopyToClipboard = enum {
|
||||
plain,
|
||||
vt,
|
||||
html,
|
||||
|
||||
/// This type will mix multiple distinct types with a set content-type
|
||||
/// such as text/html for html, so that the OS/application can choose
|
||||
/// what is best when pasting.
|
||||
mixed,
|
||||
|
||||
pub const default: CopyToClipboard = .mixed;
|
||||
};
|
||||
|
||||
pub const WriteScreenAction = enum {
|
||||
copy,
|
||||
paste,
|
||||
|
|
@ -3239,6 +3252,28 @@ test "parse: set_font_size" {
|
|||
}
|
||||
}
|
||||
|
||||
test "parse: copy to clipboard default" {
|
||||
const testing = std.testing;
|
||||
|
||||
// parameter
|
||||
{
|
||||
const binding = try parseSingle("a=copy_to_clipboard");
|
||||
try testing.expect(binding.action == .copy_to_clipboard);
|
||||
try testing.expectEqual(Action.CopyToClipboard.mixed, binding.action.copy_to_clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: copy to clipboard explicit" {
|
||||
const testing = std.testing;
|
||||
|
||||
// parameter
|
||||
{
|
||||
const binding = try parseSingle("a=copy_to_clipboard:html");
|
||||
try testing.expect(binding.action == .copy_to_clipboard);
|
||||
try testing.expectEqual(Action.CopyToClipboard.html, binding.action.copy_to_clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
test "action: format" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
|
|||
|
|
@ -121,11 +121,15 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
.description = "Reset the terminal to a clean state.",
|
||||
}},
|
||||
|
||||
.copy_to_clipboard => comptime &.{.{
|
||||
.action = .copy_to_clipboard,
|
||||
.copy_to_clipboard => comptime &.{ .{
|
||||
.action = .{ .copy_to_clipboard = .mixed },
|
||||
.title = "Copy to Clipboard",
|
||||
.description = "Copy the selected text to the clipboard.",
|
||||
}},
|
||||
}, .{
|
||||
.action = .{ .copy_to_clipboard = .html },
|
||||
.title = "Copy HTML to Clipboard",
|
||||
.description = "Copy the selected text as HTML to the clipboard.",
|
||||
} },
|
||||
|
||||
.copy_url_to_clipboard => comptime &.{.{
|
||||
.action = .copy_url_to_clipboard,
|
||||
|
|
|
|||
Loading…
Reference in New Issue