mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
core: move clipboard to async process
This commit is contained in:
205
src/Surface.zig
205
src/Surface.zig
@ -537,7 +537,14 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
|
||||
.cell_size => |size| try self.setCellSize(size),
|
||||
|
||||
.clipboard_read => |kind| try self.clipboardRead(kind),
|
||||
.clipboard_read => |kind| {
|
||||
if (!self.config.clipboard_read) {
|
||||
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
try self.startClipboardRequest(.standard, .{ .osc_52 = kind });
|
||||
},
|
||||
|
||||
.clipboard_write => |req| switch (req) {
|
||||
.small => |v| try self.clipboardWrite(v.data[0..v.len], .standard),
|
||||
@ -663,89 +670,6 @@ 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.get(.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(.standard) catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// Even if the clipboard data is empty we reply, since presumably
|
||||
// the client app is expecting a reply. We first allocate our buffer.
|
||||
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
|
||||
const enc = std.base64.standard.Encoder;
|
||||
const size = enc.calcSize(data.len);
|
||||
var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
// Wrap our data with the OSC code
|
||||
const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind});
|
||||
assert(prefix.len == 7);
|
||||
buf[buf.len - 2] = '\x1b';
|
||||
buf[buf.len - 1] = '\\';
|
||||
|
||||
// Do the base64 encoding
|
||||
const encoded = enc.encode(buf[prefix.len..], data);
|
||||
assert(encoded.len == size);
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
buf,
|
||||
), .{ .forever = {} });
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
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", .{});
|
||||
@ -1541,10 +1465,10 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
}
|
||||
|
||||
// Report mouse events if enabled
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// Report mouse events if enabled
|
||||
if (self.io.terminal.flags.mouse_event != .none) report: {
|
||||
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
||||
if (mods.shift) break :report;
|
||||
@ -1572,10 +1496,14 @@ pub fn mouseButtonCallback(
|
||||
// selection or highlighting.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For left button clicks we always record some information for
|
||||
// selection/highlighting purposes.
|
||||
if (button == .left and action == .press) {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
|
||||
// If we move our cursor too much between clicks then we reset
|
||||
@ -1655,7 +1583,8 @@ pub fn mouseButtonCallback(
|
||||
.clipboard => .standard,
|
||||
.false => unreachable,
|
||||
};
|
||||
try self.clipboardPaste(clipboard, false);
|
||||
|
||||
try self.startClipboardRequest(clipboard, .{ .paste = {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2022,7 +1951,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void
|
||||
}
|
||||
},
|
||||
|
||||
.paste_from_clipboard => try self.clipboardPaste(.standard, true),
|
||||
.paste_from_clipboard => try self.startClipboardRequest(
|
||||
.standard,
|
||||
.{ .paste = {} },
|
||||
),
|
||||
|
||||
.increase_font_size => |delta| {
|
||||
log.debug("increase font size={}", .{delta});
|
||||
@ -2202,6 +2134,103 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this to complete a clipboard request sent to apprt. This should
|
||||
/// only be called once for each request. The data is immediately copied so
|
||||
/// it is safe to free the data after this call.
|
||||
pub fn completeClipboardRequest(
|
||||
self: *Surface,
|
||||
req: apprt.ClipboardRequest,
|
||||
data: []const u8,
|
||||
) !void {
|
||||
switch (req) {
|
||||
.paste => try self.completeClipboardPaste(data),
|
||||
.osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind),
|
||||
}
|
||||
}
|
||||
|
||||
/// This starts a clipboard request, with some basic validation. For example,
|
||||
/// an OSC 52 request is not actually requested if OSC 52 is disabled.
|
||||
fn startClipboardRequest(
|
||||
self: *Surface,
|
||||
loc: apprt.Clipboard,
|
||||
req: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
switch (req) {
|
||||
.paste => {}, // always allowed
|
||||
.osc_52 => if (!self.config.clipboard_read) {
|
||||
log.info(
|
||||
"application attempted to read clipboard, but 'clipboard-read' setting is off",
|
||||
.{},
|
||||
);
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
try self.rt_surface.clipboardRequest(loc, req);
|
||||
}
|
||||
|
||||
fn completeClipboardPaste(self: *Surface, data: []const u8) !void {
|
||||
if (data.len == 0) return;
|
||||
|
||||
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.get(.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();
|
||||
}
|
||||
|
||||
fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8) !void {
|
||||
// Even if the clipboard data is empty we reply, since presumably
|
||||
// the client app is expecting a reply. We first allocate our buffer.
|
||||
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
|
||||
const enc = std.base64.standard.Encoder;
|
||||
const size = enc.calcSize(data.len);
|
||||
var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
// Wrap our data with the OSC code
|
||||
const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind});
|
||||
assert(prefix.len == 7);
|
||||
buf[buf.len - 2] = '\x1b';
|
||||
buf[buf.len - 1] = '\\';
|
||||
|
||||
// Do the base64 encoding
|
||||
const encoded = enc.encode(buf[prefix.len..], data);
|
||||
assert(encoded.len == size);
|
||||
|
||||
_ = self.io_thread.mailbox.push(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
buf,
|
||||
), .{ .forever = {} });
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
|
||||
|
@ -547,25 +547,27 @@ pub const Surface = struct {
|
||||
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Start an async clipboard request.
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) ![:0]const u8 {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse glfw.mustGetErrorCode(),
|
||||
state: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
// GLFW can read clipboards immediately so just do that.
|
||||
const str: []const u8 = switch (clipboard_type) {
|
||||
.standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(),
|
||||
.selection => selection: {
|
||||
// Not supported except on Linux
|
||||
if (comptime builtin.os.tag != .linux) return "";
|
||||
if (comptime builtin.os.tag != .linux) break :selection "";
|
||||
|
||||
const raw = glfwNative.getX11SelectionString() orelse
|
||||
return glfw.mustGetErrorCode();
|
||||
break :selection std.mem.span(raw);
|
||||
},
|
||||
};
|
||||
|
||||
// Complete our request
|
||||
try self.core_surface.completeClipboardRequest(state, str);
|
||||
}
|
||||
|
||||
/// Set the clipboard.
|
||||
|
@ -31,3 +31,13 @@ pub const Clipboard = enum(u1) {
|
||||
standard = 0, // ctrl+c/v
|
||||
selection = 1, // also known as the "primary" clipboard
|
||||
};
|
||||
|
||||
/// Clipboard request. This is used to request clipboard contents and must
|
||||
/// be sent as a response to a ClipboardRequest event.
|
||||
pub const ClipboardRequest = union(enum) {
|
||||
/// A direct paste of clipboard contents.
|
||||
paste: void,
|
||||
|
||||
/// A request to write clipboard contents via OSC 52.
|
||||
osc_52: u8,
|
||||
};
|
||||
|
Reference in New Issue
Block a user