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),
|
.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) {
|
.clipboard_write => |req| switch (req) {
|
||||||
.small => |v| try self.clipboardWrite(v.data[0..v.len], .standard),
|
.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 };
|
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 {
|
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
|
||||||
if (!self.config.clipboard_write) {
|
if (!self.config.clipboard_write) {
|
||||||
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
|
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();
|
self.renderer_state.mutex.lock();
|
||||||
defer self.renderer_state.mutex.unlock();
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
|
||||||
// Report mouse events if enabled
|
|
||||||
if (self.io.terminal.flags.mouse_event != .none) report: {
|
if (self.io.terminal.flags.mouse_event != .none) report: {
|
||||||
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
|
||||||
if (mods.shift) break :report;
|
if (mods.shift) break :report;
|
||||||
@ -1572,10 +1496,14 @@ pub fn mouseButtonCallback(
|
|||||||
// selection or highlighting.
|
// selection or highlighting.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For left button clicks we always record some information for
|
// For left button clicks we always record some information for
|
||||||
// selection/highlighting purposes.
|
// selection/highlighting purposes.
|
||||||
if (button == .left and action == .press) {
|
if (button == .left and action == .press) {
|
||||||
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
|
||||||
const pos = try self.rt_surface.getCursorPos();
|
const pos = try self.rt_surface.getCursorPos();
|
||||||
|
|
||||||
// If we move our cursor too much between clicks then we reset
|
// If we move our cursor too much between clicks then we reset
|
||||||
@ -1655,7 +1583,8 @@ pub fn mouseButtonCallback(
|
|||||||
.clipboard => .standard,
|
.clipboard => .standard,
|
||||||
.false => unreachable,
|
.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| {
|
.increase_font_size => |delta| {
|
||||||
log.debug("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_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||||
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||||
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.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);
|
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the clipboard. The windowing system is responsible for allocating
|
/// Start an async clipboard request.
|
||||||
/// a buffer as necessary. This should be a stable pointer until the next
|
pub fn clipboardRequest(
|
||||||
/// time getClipboardString is called.
|
self: *Surface,
|
||||||
pub fn getClipboardString(
|
|
||||||
self: *const Surface,
|
|
||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
) ![:0]const u8 {
|
state: apprt.ClipboardRequest,
|
||||||
_ = self;
|
) !void {
|
||||||
return switch (clipboard_type) {
|
// GLFW can read clipboards immediately so just do that.
|
||||||
.standard => glfw.getClipboardString() orelse glfw.mustGetErrorCode(),
|
const str: []const u8 = switch (clipboard_type) {
|
||||||
|
.standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(),
|
||||||
.selection => selection: {
|
.selection => selection: {
|
||||||
// Not supported except on Linux
|
// 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
|
const raw = glfwNative.getX11SelectionString() orelse
|
||||||
return glfw.mustGetErrorCode();
|
return glfw.mustGetErrorCode();
|
||||||
break :selection std.mem.span(raw);
|
break :selection std.mem.span(raw);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Complete our request
|
||||||
|
try self.core_surface.completeClipboardRequest(state, str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the clipboard.
|
/// Set the clipboard.
|
||||||
|
@ -31,3 +31,13 @@ pub const Clipboard = enum(u1) {
|
|||||||
standard = 0, // ctrl+c/v
|
standard = 0, // ctrl+c/v
|
||||||
selection = 1, // also known as the "primary" clipboard
|
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