mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
Programs can now write the system clipboard through the Kitty clipboard protocol in the macOS app. This also does all the hard work plumbing through core termio/apprt so GTK should be an easy follow. This functionality lets clients copy arbitrary representations (images, HTML, etc.) into the clipboard. Writes honor `clipboard-write`: allow applies silently, deny answers EPERM up front before any data is used, and ask shows the standard confirmation prompt.
318 lines
10 KiB
Zig
318 lines
10 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const apprt = @import("../apprt.zig");
|
|
const build_config = @import("../build_config.zig");
|
|
const App = @import("../App.zig");
|
|
const Surface = @import("../Surface.zig");
|
|
const renderer = @import("../renderer.zig");
|
|
const terminal = @import("../terminal/main.zig");
|
|
const Config = @import("../config.zig").Config;
|
|
const MessageData = @import("../datastruct/main.zig").MessageData;
|
|
|
|
/// The message types that can be sent to a single surface.
|
|
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 = MessageData(u8, 255);
|
|
|
|
/// A fixed-size desktop notification payload sent to the app thread.
|
|
pub const DesktopNotification = struct {
|
|
/// Desktop notification title.
|
|
title: [63:0]u8,
|
|
|
|
/// Desktop notification body.
|
|
body: [255:0]u8,
|
|
|
|
pub fn init(title: []const u8, body: []const u8) DesktopNotification {
|
|
var result: DesktopNotification = undefined;
|
|
copyUtf8Z(result.title.len, &result.title, title);
|
|
copyUtf8Z(result.body.len, &result.body, body);
|
|
return result;
|
|
}
|
|
|
|
/// UTF-8 continuation bytes occupy the range 0x80 through 0xBF.
|
|
fn isUtf8ContinuationByte(byte: u8) bool {
|
|
return switch (byte) {
|
|
0x80...0xBF => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Copy as much of `src` as fits, backing up from a UTF-8 continuation
|
|
/// byte so valid input is never truncated in the middle of a codepoint.
|
|
fn copyUtf8Z(
|
|
comptime capacity: usize,
|
|
dst: *[capacity:0]u8,
|
|
src: []const u8,
|
|
) void {
|
|
var len = @min(src.len, capacity);
|
|
while (len > 0 and
|
|
len < src.len and
|
|
isUtf8ContinuationByte(src[len])) : (len -= 1)
|
|
{}
|
|
|
|
@memcpy(dst[0..len], src[0..len]);
|
|
dst[len] = 0;
|
|
}
|
|
};
|
|
|
|
/// Set the title of the surface.
|
|
/// TODO: we should change this to a "WriteReq" style structure in
|
|
/// the termio message so that we can more efficiently send strings
|
|
/// of any length
|
|
set_title: [256]u8,
|
|
|
|
/// Report the window title back to the terminal
|
|
report_title: ReportTitleStyle,
|
|
|
|
/// Set the mouse shape.
|
|
set_mouse_shape: terminal.MouseShape,
|
|
|
|
/// Read the clipboard and write to the pty.
|
|
clipboard_read: apprt.Clipboard,
|
|
|
|
/// A Kitty clipboard protocol (OSC 5522) read request. The receiver
|
|
/// takes ownership of the request state and must eventually destroy
|
|
/// it.
|
|
kitty_clipboard_read: *apprt.ClipboardRequest.KittyRead,
|
|
|
|
/// A committed Kitty clipboard protocol (OSC 5522) write
|
|
/// transaction. The receiver takes ownership of the request state
|
|
/// and must eventually destroy it.
|
|
kitty_clipboard_write: *apprt.ClipboardRequest.KittyWrite,
|
|
|
|
/// Write the clipboard contents.
|
|
clipboard_write: struct {
|
|
clipboard_type: apprt.Clipboard,
|
|
req: WriteReq,
|
|
},
|
|
|
|
/// Change the configuration to the given configuration. The pointer is
|
|
/// not valid after receiving this message so any config must be used
|
|
/// and derived immediately.
|
|
change_config: *const Config,
|
|
|
|
/// Close the surface. This will only close the current surface that
|
|
/// receives this, not the full application.
|
|
close: void,
|
|
|
|
/// The child process running in the surface has exited. This may trigger
|
|
/// a surface close, it may not. Additional details about the child
|
|
/// command are given in the `ChildExited` struct.
|
|
child_exited: ChildExited,
|
|
|
|
/// Show a desktop notification.
|
|
desktop_notification: DesktopNotification,
|
|
|
|
/// Health status change for the renderer.
|
|
renderer_health: renderer.Health,
|
|
|
|
/// Tell the surface to present itself to the user. This may require raising
|
|
/// a window and switching tabs.
|
|
present_surface: void,
|
|
|
|
/// Notifies the surface that password input has started within
|
|
/// the terminal. This should always be followed by a false value
|
|
/// unless the surface exits.
|
|
password_input: bool,
|
|
|
|
/// A terminal color was changed using OSC sequences.
|
|
color_change: terminal.osc.color.ColoredTarget,
|
|
|
|
/// Notifies the surface that a tick of the timer that is timing
|
|
/// out selection scrolling has occurred. "selection scrolling"
|
|
/// is when the user has clicked and dragged the mouse outside
|
|
/// the viewport of the terminal and the terminal is scrolling
|
|
/// the viewport to follow the mouse cursor.
|
|
selection_scroll_tick: bool,
|
|
|
|
/// The terminal has reported a change in the working directory.
|
|
pwd_change: WriteReq,
|
|
|
|
/// The terminal encountered a bell character.
|
|
ring_bell,
|
|
|
|
/// Report the progress of an action using a GUI element
|
|
progress_report: terminal.osc.Command.ProgressReport,
|
|
|
|
/// A command has started in the shell, start a timer.
|
|
start_command,
|
|
|
|
/// A command has finished in the shell, stop the timer and send out
|
|
/// notifications as appropriate. The optional u8 is the exit code
|
|
/// of the command.
|
|
stop_command: ?u8,
|
|
|
|
/// The scrollbar state changed for the surface.
|
|
scrollbar: terminal.Scrollbar,
|
|
|
|
/// Search progress update
|
|
search_total: ?usize,
|
|
|
|
/// Selected search index change
|
|
search_selected: ?usize,
|
|
|
|
pub const ReportTitleStyle = enum {
|
|
csi_21_t,
|
|
|
|
// This enum is a placeholder for future title styles.
|
|
};
|
|
|
|
pub const ChildExited = extern struct {
|
|
exit_code: u32,
|
|
runtime_ms: u64,
|
|
|
|
/// Make this a valid gobject if we're in a GTK environment.
|
|
pub const getGObjectType = switch (build_config.app_runtime) {
|
|
.gtk,
|
|
=> @import("gobject").ext.defineBoxed(
|
|
ChildExited,
|
|
.{ .name = "GhosttyApprtChildExited" },
|
|
),
|
|
|
|
.none => void,
|
|
};
|
|
};
|
|
};
|
|
|
|
/// A surface mailbox.
|
|
pub const Mailbox = struct {
|
|
surface: *Surface,
|
|
app: App.Mailbox,
|
|
|
|
/// Send a message to the surface.
|
|
pub fn push(
|
|
self: Mailbox,
|
|
msg: Message,
|
|
timeout: App.Mailbox.Queue.Timeout,
|
|
) App.Mailbox.Queue.Size {
|
|
// Surface message sending is actually implemented on the app
|
|
// thread, so we have to rewrap the message with our surface
|
|
// pointer and send it to the app thread.
|
|
return self.app.push(.{
|
|
.surface_message = .{
|
|
.surface = self.surface,
|
|
.message = msg,
|
|
},
|
|
}, timeout);
|
|
}
|
|
};
|
|
|
|
/// Context for new surface creation to determine inheritance behavior
|
|
pub const NewSurfaceContext = enum(c_int) {
|
|
window = 0,
|
|
tab = 1,
|
|
split = 2,
|
|
};
|
|
|
|
pub fn shouldInheritWorkingDirectory(context: NewSurfaceContext, config: *const Config) bool {
|
|
return switch (context) {
|
|
.window => config.@"window-inherit-working-directory",
|
|
.tab => config.@"tab-inherit-working-directory",
|
|
.split => config.@"split-inherit-working-directory",
|
|
};
|
|
}
|
|
|
|
/// Returns a new config for a surface for the given app that should be
|
|
/// used for any new surfaces. The resulting config should be deinitialized
|
|
/// after the surface is initialized.
|
|
pub fn newConfig(
|
|
app: *const App,
|
|
config: *const Config,
|
|
context: NewSurfaceContext,
|
|
) Allocator.Error!Config {
|
|
// Create a shallow clone
|
|
var copy = config.shallowClone(app.alloc);
|
|
|
|
// Our allocator is our config's arena
|
|
const alloc = copy._arena.?.allocator();
|
|
|
|
// Get our previously focused surface for some inherited values.
|
|
const prev = app.focusedSurface();
|
|
if (prev) |p| {
|
|
if (shouldInheritWorkingDirectory(context, config)) {
|
|
if (try p.pwd(alloc)) |pwd| {
|
|
copy.@"working-directory" = .{ .path = pwd };
|
|
}
|
|
}
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
test "DesktopNotification init" {
|
|
const notification = Message.DesktopNotification.init("Title", "Body");
|
|
|
|
try std.testing.expectEqualStrings("Title", std.mem.sliceTo(¬ification.title, 0));
|
|
try std.testing.expectEqualStrings("Body", std.mem.sliceTo(¬ification.body, 0));
|
|
}
|
|
|
|
test "copyUtf8Z handles len at the final byte of every UTF-8 sequence length" {
|
|
const DesktopNotification = Message.DesktopNotification;
|
|
|
|
var dst_1_byte: [1:0]u8 = undefined;
|
|
const src_ending_in_1_byte_codepoint = "ab";
|
|
try std.testing.expect(!DesktopNotification.isUtf8ContinuationByte(
|
|
src_ending_in_1_byte_codepoint[dst_1_byte.len],
|
|
));
|
|
DesktopNotification.copyUtf8Z(
|
|
dst_1_byte.len,
|
|
&dst_1_byte,
|
|
src_ending_in_1_byte_codepoint,
|
|
);
|
|
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_1_byte, 0));
|
|
|
|
var dst_2_bytes: [2:0]u8 = undefined;
|
|
const src_ending_in_2_byte_codepoint = "aЯ";
|
|
try std.testing.expect(DesktopNotification.isUtf8ContinuationByte(
|
|
src_ending_in_2_byte_codepoint[dst_2_bytes.len],
|
|
));
|
|
DesktopNotification.copyUtf8Z(
|
|
dst_2_bytes.len,
|
|
&dst_2_bytes,
|
|
src_ending_in_2_byte_codepoint,
|
|
);
|
|
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_2_bytes, 0));
|
|
|
|
var dst_3_bytes: [3:0]u8 = undefined;
|
|
const src_ending_in_3_byte_codepoint = "a€";
|
|
try std.testing.expect(DesktopNotification.isUtf8ContinuationByte(
|
|
src_ending_in_3_byte_codepoint[dst_3_bytes.len],
|
|
));
|
|
DesktopNotification.copyUtf8Z(
|
|
dst_3_bytes.len,
|
|
&dst_3_bytes,
|
|
src_ending_in_3_byte_codepoint,
|
|
);
|
|
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_3_bytes, 0));
|
|
|
|
var dst_4_bytes: [4:0]u8 = undefined;
|
|
const src_ending_in_4_byte_codepoint = "a😀";
|
|
try std.testing.expect(DesktopNotification.isUtf8ContinuationByte(
|
|
src_ending_in_4_byte_codepoint[dst_4_bytes.len],
|
|
));
|
|
DesktopNotification.copyUtf8Z(
|
|
dst_4_bytes.len,
|
|
&dst_4_bytes,
|
|
src_ending_in_4_byte_codepoint,
|
|
);
|
|
try std.testing.expectEqualStrings("a", std.mem.sliceTo(&dst_4_bytes, 0));
|
|
}
|
|
|
|
test "copyUtf8Z keeps a complete codepoint at the truncation boundary" {
|
|
var dst: [5:0]u8 = undefined;
|
|
|
|
Message.DesktopNotification.copyUtf8Z(dst.len, &dst, "abcЯz");
|
|
|
|
try std.testing.expectEqualStrings("abcЯ", std.mem.sliceTo(&dst, 0));
|
|
}
|
|
|
|
test "copyUtf8Z preserves UTF-8 that fits" {
|
|
var dst: [5:0]u8 = undefined;
|
|
|
|
Message.DesktopNotification.copyUtf8Z(dst.len, &dst, "abcЯ");
|
|
|
|
try std.testing.expectEqualStrings("abcЯ", std.mem.sliceTo(&dst, 0));
|
|
}
|