core: move clipboard to async process

This commit is contained in:
Mitchell Hashimoto
2023-09-19 10:18:17 -07:00
parent 852249664b
commit b30feeb596
3 changed files with 161 additions and 120 deletions

View File

@ -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");

View File

@ -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.

View File

@ -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,
};