mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
OSC 52: Clipboard Control (#52)
This adds support for OSC 52 -- applications can read/write the clipboard. Due to the security risk of this, the default configuration allows for writing but _not reading_. This is configurable using two new settings: `clipboard-read` and `clipboard-write` (both booleans).
This commit is contained in:

committed by
GitHub

parent
173aff1e80
commit
56de5846f4
@ -592,9 +592,80 @@ pub fn handleMessage(self: *Window, 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_write => |req| switch (req) {
|
||||||
|
.small => |v| try self.clipboardWrite(v.data[0..v.len]),
|
||||||
|
.stable => |v| try self.clipboardWrite(v),
|
||||||
|
.alloc => |v| {
|
||||||
|
defer v.alloc.free(v.data);
|
||||||
|
try self.clipboardWrite(v.data);
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clipboardRead(self: *const Window, kind: u8) !void {
|
||||||
|
if (!self.config.@"clipboard-read") {
|
||||||
|
log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = glfw.getClipboardString() 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.send() catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clipboardWrite(self: *const Window, data: []const u8) !void {
|
||||||
|
if (!self.config.@"clipboard-write") {
|
||||||
|
log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dec = std.base64.standard.Decoder;
|
||||||
|
|
||||||
|
// Build buffer
|
||||||
|
const size = try dec.calcSizeForSlice(data);
|
||||||
|
var buf = try self.alloc.allocSentinel(u8, size, 0);
|
||||||
|
defer self.alloc.free(buf);
|
||||||
|
buf[buf.len] = 0;
|
||||||
|
|
||||||
|
// Decode
|
||||||
|
try dec.decode(buf, data);
|
||||||
|
assert(buf[buf.len] == 0);
|
||||||
|
|
||||||
|
glfw.setClipboardString(buf) catch |err| {
|
||||||
|
log.err("error setting clipboard string err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Change the cell size for the terminal grid. This can happen as
|
/// Change the cell size for the terminal grid. This can happen as
|
||||||
/// a result of changing the font size at runtime.
|
/// a result of changing the font size at runtime.
|
||||||
fn setCellSize(self: *Window, size: renderer.CellSize) !void {
|
fn setCellSize(self: *Window, size: renderer.CellSize) !void {
|
||||||
|
@ -141,6 +141,12 @@ pub const Config = struct {
|
|||||||
/// specified in the configuration "font-size" will be used.
|
/// specified in the configuration "font-size" will be used.
|
||||||
@"window-inherit-font-size": bool = true,
|
@"window-inherit-font-size": bool = true,
|
||||||
|
|
||||||
|
/// Whether to allow programs running in the terminal to read/write to
|
||||||
|
/// the system clipboard (OSC 52, for googling). The default is to
|
||||||
|
/// disallow clipboard reading but allow writing.
|
||||||
|
@"clipboard-read": bool = false,
|
||||||
|
@"clipboard-write": bool = true,
|
||||||
|
|
||||||
/// Additional configuration files to read.
|
/// Additional configuration files to read.
|
||||||
@"config-file": RepeatableString = .{},
|
@"config-file": RepeatableString = .{},
|
||||||
|
|
||||||
|
@ -567,6 +567,31 @@ test "osc: change window title" {
|
|||||||
|
|
||||||
const cmd = a[0].?.osc_dispatch;
|
const cmd = a[0].?.osc_dispatch;
|
||||||
try testing.expect(cmd == .change_window_title);
|
try testing.expect(cmd == .change_window_title);
|
||||||
|
try testing.expectEqualStrings("abc", cmd.change_window_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "osc: change window title (end in esc)" {
|
||||||
|
var p = init();
|
||||||
|
_ = p.next(0x1B);
|
||||||
|
_ = p.next(']');
|
||||||
|
_ = p.next('0');
|
||||||
|
_ = p.next(';');
|
||||||
|
_ = p.next('a');
|
||||||
|
_ = p.next('b');
|
||||||
|
_ = p.next('c');
|
||||||
|
|
||||||
|
{
|
||||||
|
const a = p.next(0x1B);
|
||||||
|
_ = p.next('\\');
|
||||||
|
try testing.expect(p.state == .ground);
|
||||||
|
try testing.expect(a[0].? == .osc_dispatch);
|
||||||
|
try testing.expect(a[1] == null);
|
||||||
|
try testing.expect(a[2] == null);
|
||||||
|
|
||||||
|
const cmd = a[0].?.osc_dispatch;
|
||||||
|
try testing.expect(cmd == .change_window_title);
|
||||||
|
try testing.expectEqualStrings("abc", cmd.change_window_title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,11 +326,7 @@ pub const Parser = struct {
|
|||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
.string => {
|
.string => self.complete = true,
|
||||||
// Complete once we receive one character since we have
|
|
||||||
// at least SOME value for the expected string value.
|
|
||||||
self.complete = true;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +348,10 @@ pub const Parser = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endString(self: *Parser) void {
|
||||||
|
self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx];
|
||||||
|
}
|
||||||
|
|
||||||
/// End the sequence and return the command, if any. If the return value
|
/// End the sequence and return the command, if any. If the return value
|
||||||
/// is null, then no valid command was found.
|
/// is null, then no valid command was found.
|
||||||
pub fn end(self: *Parser) ?Command {
|
pub fn end(self: *Parser) ?Command {
|
||||||
@ -364,7 +364,7 @@ pub const Parser = struct {
|
|||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.semantic_exit_code => self.endSemanticExitCode(),
|
.semantic_exit_code => self.endSemanticExitCode(),
|
||||||
.semantic_option_value => self.endSemanticOptionValue(),
|
.semantic_option_value => self.endSemanticOptionValue(),
|
||||||
.string => self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx],
|
.string => self.endString(),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,6 +464,12 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.clipboard_contents => |clip| {
|
||||||
|
if (@hasDecl(T, "clipboardContents")) {
|
||||||
|
try self.handler.clipboardContents(clip.kind, clip.data);
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
else => if (@hasDecl(T, "oscUnimplemented"))
|
else => if (@hasDecl(T, "oscUnimplemented"))
|
||||||
try self.handler.oscUnimplemented(cmd)
|
try self.handler.oscUnimplemented(cmd)
|
||||||
else
|
else
|
||||||
|
@ -815,4 +815,26 @@ const StreamHandler = struct {
|
|||||||
.set_title = buf,
|
.set_title = buf,
|
||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
|
||||||
|
// Note: we ignore the "kind" field and always use the primary clipboard.
|
||||||
|
// iTerm also appears to do this but other terminals seem to only allow
|
||||||
|
// certain. Let's investigate more.
|
||||||
|
|
||||||
|
// Get clipboard contents
|
||||||
|
if (data.len == 1 and data[0] == '?') {
|
||||||
|
_ = self.window_mailbox.push(.{
|
||||||
|
.clipboard_read = kind,
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write clipboard contents
|
||||||
|
_ = self.window_mailbox.push(.{
|
||||||
|
.clipboard_write = try Window.Message.WriteReq.init(
|
||||||
|
self.alloc,
|
||||||
|
data,
|
||||||
|
),
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
const Window = @import("../Window.zig");
|
const Window = @import("../Window.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
|
const termio = @import("../termio.zig");
|
||||||
|
|
||||||
/// The message types that can be sent to a single window.
|
/// The message types that can be sent to a single window.
|
||||||
pub const Message = union(enum) {
|
pub const Message = union(enum) {
|
||||||
|
/// Represents a write request. Magic number comes from the max size
|
||||||
|
/// we want this union to be.
|
||||||
|
pub const WriteReq = termio.MessageData(u8, 256);
|
||||||
|
|
||||||
/// Set the title of the window.
|
/// Set the title of the window.
|
||||||
/// TODO: we should change this to a "WriteReq" style structure in
|
/// TODO: we should change this to a "WriteReq" style structure in
|
||||||
/// the termio message so that we can more efficiently send strings
|
/// the termio message so that we can more efficiently send strings
|
||||||
@ -12,6 +17,12 @@ pub const Message = union(enum) {
|
|||||||
|
|
||||||
/// Change the cell size.
|
/// Change the cell size.
|
||||||
cell_size: renderer.CellSize,
|
cell_size: renderer.CellSize,
|
||||||
|
|
||||||
|
/// Read the clipboard and write to the pty.
|
||||||
|
clipboard_read: u8,
|
||||||
|
|
||||||
|
/// Write the clipboard contents.
|
||||||
|
clipboard_write: WriteReq,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A window mailbox.
|
/// A window mailbox.
|
||||||
|
Reference in New Issue
Block a user