gtk: implement OSC 52 prompts

This commit is contained in:
Gregory Anders
2023-11-10 12:04:53 -06:00
parent 86245ff0cf
commit 960a1bb091
8 changed files with 151 additions and 64 deletions

View File

@ -688,21 +688,21 @@ 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| { .clipboard_read => |clipboard| {
if (self.config.clipboard_read == .deny) { if (self.config.clipboard_read == .deny) {
log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}); log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{});
return; return;
} }
try self.startClipboardRequest(.standard, .{ .osc_52 = kind }); try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard });
}, },
.clipboard_write => |req| switch (req) { .clipboard_write => |w| switch (w.req) {
.small => |v| try self.clipboardWrite(v.data[0..v.len], .standard), .small => |v| try self.clipboardWrite(v.data[0..v.len], w.clipboard_type),
.stable => |v| try self.clipboardWrite(v, .standard), .stable => |v| try self.clipboardWrite(v, w.clipboard_type),
.alloc => |v| { .alloc => |v| {
defer v.alloc.free(v.data); defer v.alloc.free(v.data);
try self.clipboardWrite(v.data, .standard); try self.clipboardWrite(v.data, w.clipboard_type);
}, },
}, },
@ -856,6 +856,9 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
}; };
assert(buf[buf.len] == 0); assert(buf[buf.len] == 0);
// When clipboard-write is "ask" a prompt is displayed to the user asking
// them to confirm the clipboard access. Each app runtime handles this
// differently.
const confirm = self.config.clipboard_write == .ask; const confirm = self.config.clipboard_write == .ask;
self.rt_surface.setClipboardString(buf, loc, confirm) catch |err| { self.rt_surface.setClipboardString(buf, loc, confirm) catch |err| {
log.err("error setting clipboard string err={}", .{err}); log.err("error setting clipboard string err={}", .{err});
@ -2516,19 +2519,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
/// data is defined as data that contains newlines, though this definition /// data is defined as data that contains newlines, though this definition
/// may change later to detect other scenarios. /// may change later to detect other scenarios.
/// ///
/// - For OSC 52 pastes no prompt is shown to the user if `confirmed` is true. /// - For OSC 52 reads and writes no prompt is shown to the user if
/// `confirmed` is true.
/// ///
/// If `confirmed` is false and either unsafe data is detected or the /// If `confirmed` is false then this may return either an UnsafePaste or
/// `clipboard-read` option is set to `ask`, this will return error.UnsafePaste. /// UnauthorizedPaste error, depending on the type of clipboard request.
pub fn completeClipboardRequest( pub fn completeClipboardRequest(
self: *Surface, self: *Surface,
req: apprt.ClipboardRequest, req: apprt.ClipboardRequest,
data: []const u8, data: [:0]const u8,
confirmed: bool, confirmed: bool,
) !void { ) !void {
switch (req) { switch (req) {
.paste => try self.completeClipboardPaste(data, confirmed), .paste => try self.completeClipboardPaste(data, confirmed),
.osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind, confirmed), .osc_52_read => |clipboard| try self.completeClipboardReadOSC52(data, clipboard, confirmed),
.osc_52_write => |clipboard| try self.rt_surface.setClipboardString(data, clipboard, !confirmed),
} }
} }
@ -2541,13 +2546,16 @@ fn startClipboardRequest(
) !void { ) !void {
switch (req) { switch (req) {
.paste => {}, // always allowed .paste => {}, // always allowed
.osc_52 => if (self.config.clipboard_read == .deny) { .osc_52_read => if (self.config.clipboard_read == .deny) {
log.info( log.info(
"application attempted to read clipboard, but 'clipboard-read' is set to deny", "application attempted to read clipboard, but 'clipboard-read' is set to deny",
.{}, .{},
); );
return; return;
}, },
// No clipboard write code paths travel through this function
.osc_52_write => unreachable,
} }
try self.rt_surface.clipboardRequest(loc, req); try self.rt_surface.clipboardRequest(loc, req);
@ -2642,7 +2650,12 @@ fn completeClipboardPaste(
try self.io_thread.wakeup.notify(); try self.io_thread.wakeup.notify();
} }
fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8, confirmed: bool) !void { fn completeClipboardReadOSC52(
self: *Surface,
data: []const u8,
clipboard_type: apprt.Clipboard,
confirmed: bool,
) !void {
// We should never get here if clipboard-read is set to deny // We should never get here if clipboard-read is set to deny
assert(self.config.clipboard_read != .deny); assert(self.config.clipboard_read != .deny);
@ -2660,6 +2673,11 @@ fn completeClipboardReadOSC52(self: *Surface, data: []const u8, kind: u8, confir
var buf = try self.alloc.alloc(u8, size + 9); // const for OSC var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
defer self.alloc.free(buf); defer self.alloc.free(buf);
const kind: u8 = switch (clipboard_type) {
.standard => 'c',
.selection => 's',
};
// Wrap our data with the OSC code // Wrap our data with the OSC code
const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind}); const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind});
assert(prefix.len == 7); assert(prefix.len == 7);

View File

@ -656,15 +656,14 @@ pub const Surface = struct {
state: apprt.ClipboardRequest, state: apprt.ClipboardRequest,
) !void { ) !void {
// GLFW can read clipboards immediately so just do that. // GLFW can read clipboards immediately so just do that.
const str: []const u8 = switch (clipboard_type) { const str: [:0]const u8 = switch (clipboard_type) {
.standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(), .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) break :selection ""; if (comptime builtin.os.tag != .linux) break :selection "";
const raw = glfwNative.getX11SelectionString() orelse break :selection glfwNative.getX11SelectionString() orelse
return glfw.mustGetErrorCode(); return glfw.mustGetErrorCode();
break :selection std.mem.span(raw);
}, },
}; };

View File

@ -24,7 +24,7 @@ const build_options = @import("build_options");
const Surface = @import("Surface.zig"); const Surface = @import("Surface.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const c = @import("c.zig"); const c = @import("c.zig");
const inspector = @import("inspector.zig"); const inspector = @import("inspector.zig");
const key = @import("key.zig"); const key = @import("key.zig");
@ -49,8 +49,8 @@ menu: ?*c.GMenu = null,
/// The configuration errors window, if it is currently open. /// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null, config_errors_window: ?*ConfigErrorsWindow = null,
/// The unsafe paste window, if it is currently open. /// The clipboard confirmation window, if it is currently open.
unsafe_paste_window: ?*UnsafePasteWindow = null, clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
/// This is set to false when the main loop should exit. /// This is set to false when the main loop should exit.
running: bool = true, running: bool = true,

View File

@ -1,11 +1,12 @@
/// Unsafe Paste Window /// Clipboard Confirmation Window
const UnsafePaste = @This(); const ClipboardConfirmation = @This();
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const CoreSurface = @import("../../Surface.zig"); const CoreSurface = @import("../../Surface.zig");
const ClipboardRequest = @import("../structs.zig").ClipboardRequest; const ClipboardRequest = @import("../structs.zig").ClipboardRequest;
const ClipboardPromptReason = @import("../structs.zig").ClipboardPromptReason;
const App = @import("App.zig"); const App = @import("App.zig");
const View = @import("View.zig"); const View = @import("View.zig");
const c = @import("c.zig"); const c = @import("c.zig");
@ -16,9 +17,10 @@ app: *App,
window: *c.GtkWindow, window: *c.GtkWindow,
view: PrimaryView, view: PrimaryView,
data: []u8, data: [:0]u8,
core_surface: CoreSurface, core_surface: CoreSurface,
pending_req: ClipboardRequest, pending_req: ClipboardRequest,
reason: ClipboardPromptReason,
pub fn create( pub fn create(
app: *App, app: *App,
@ -26,40 +28,49 @@ pub fn create(
core_surface: CoreSurface, core_surface: CoreSurface,
request: ClipboardRequest, request: ClipboardRequest,
) !void { ) !void {
if (app.unsafe_paste_window != null) return error.WindowAlreadyExists; if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
const alloc = app.core_app.alloc; const alloc = app.core_app.alloc;
const self = try alloc.create(UnsafePaste); const self = try alloc.create(ClipboardConfirmation);
errdefer alloc.destroy(self); errdefer alloc.destroy(self);
const reason: ClipboardPromptReason = switch (request) {
.paste => .unsafe,
.osc_52_read => .read,
.osc_52_write => .write,
};
try self.init( try self.init(
app, app,
data, data,
core_surface, core_surface,
request, request,
reason,
); );
app.unsafe_paste_window = self; app.clipboard_confirmation_window = self;
} }
/// Not public because this should be called by the GTK lifecycle. /// Not public because this should be called by the GTK lifecycle.
fn destroy(self: *UnsafePaste) void { fn destroy(self: *ClipboardConfirmation) void {
const alloc = self.app.core_app.alloc; const alloc = self.app.core_app.alloc;
self.app.unsafe_paste_window = null; self.app.clipboard_confirmation_window = null;
alloc.destroy(self); alloc.destroy(self);
} }
fn init( fn init(
self: *UnsafePaste, self: *ClipboardConfirmation,
app: *App, app: *App,
data: []const u8, data: []const u8,
core_surface: CoreSurface, core_surface: CoreSurface,
request: ClipboardRequest, request: ClipboardRequest,
reason: ClipboardPromptReason,
) !void { ) !void {
// Create the window // Create the window
const window = c.gtk_window_new(); const window = c.gtk_window_new();
const gtk_window: *c.GtkWindow = @ptrCast(window); const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window); errdefer c.gtk_window_destroy(gtk_window);
c.gtk_window_set_title(gtk_window, "Warning: Potentially Unsafe Paste"); c.gtk_window_set_title(gtk_window, titleText(reason));
c.gtk_window_set_default_size(gtk_window, 550, 275); c.gtk_window_set_default_size(gtk_window, 550, 275);
c.gtk_window_set_resizable(gtk_window, 0); c.gtk_window_set_resizable(gtk_window, 0);
_ = c.g_signal_connect_data( _ = c.g_signal_connect_data(
@ -76,9 +87,10 @@ fn init(
.app = app, .app = app,
.window = gtk_window, .window = gtk_window,
.view = undefined, .view = undefined,
.data = try app.core_app.alloc.dupe(u8, data), .data = try app.core_app.alloc.dupeZ(u8, data),
.core_surface = core_surface, .core_surface = core_surface,
.pending_req = request, .pending_req = request,
.reason = reason,
}; };
// Show the window // Show the window
@ -93,7 +105,7 @@ fn init(
} }
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *UnsafePaste = @ptrCast(@alignCast(ud orelse return)); const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud orelse return));
self.destroy(); self.destroy();
} }
@ -101,12 +113,9 @@ const PrimaryView = struct {
root: *c.GtkWidget, root: *c.GtkWidget,
text: *c.GtkTextView, text: *c.GtkTextView,
pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView { pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView {
// All our widgets // All our widgets
const label = c.gtk_label_new( const label = c.gtk_label_new(promptText(root.reason));
"Pasting this text into the terminal may be dangerous as " ++
"it looks like some commands may be executed.",
);
const buf = unsafeBuffer(data); const buf = unsafeBuffer(data);
defer c.g_object_unref(buf); defer c.g_object_unref(buf);
const buttons = try ButtonsView.init(root); const buttons = try ButtonsView.init(root);
@ -157,20 +166,26 @@ const PrimaryView = struct {
const ButtonsView = struct { const ButtonsView = struct {
root: *c.GtkWidget, root: *c.GtkWidget,
pub fn init(root: *UnsafePaste) !ButtonsView { pub fn init(root: *ClipboardConfirmation) !ButtonsView {
const cancel_button = c.gtk_button_new_with_label("Cancel"); const cancel_text, const confirm_text = switch (root.reason) {
.unsafe => .{ "Cancel", "Paste" },
.read, .write => .{ "Deny", "Allow" },
_ => unreachable,
};
const cancel_button = c.gtk_button_new_with_label(cancel_text);
errdefer c.g_object_unref(cancel_button); errdefer c.g_object_unref(cancel_button);
const paste_button = c.gtk_button_new_with_label("Paste"); const confirm_button = c.gtk_button_new_with_label(confirm_text);
errdefer c.g_object_unref(paste_button); errdefer c.g_object_unref(confirm_button);
// TODO: Focus on the paste button // TODO: Focus on the paste button
// c.gtk_widget_grab_focus(paste_button); // c.gtk_widget_grab_focus(confirm_button);
// Create our view // Create our view
const view = try View.init(&.{ const view = try View.init(&.{
.{ .name = "cancel", .widget = cancel_button }, .{ .name = "cancel", .widget = cancel_button },
.{ .name = "paste", .widget = paste_button }, .{ .name = "confirm", .widget = confirm_button },
}, &vfl); }, &vfl);
// Signals // Signals
@ -183,9 +198,9 @@ const ButtonsView = struct {
c.G_CONNECT_DEFAULT, c.G_CONNECT_DEFAULT,
); );
_ = c.g_signal_connect_data( _ = c.g_signal_connect_data(
paste_button, confirm_button,
"clicked", "clicked",
c.G_CALLBACK(&gtkPasteClick), c.G_CALLBACK(&gtkConfirmClick),
root, root,
null, null,
c.G_CONNECT_DEFAULT, c.G_CONNECT_DEFAULT,
@ -195,13 +210,13 @@ const ButtonsView = struct {
} }
fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *UnsafePaste = @ptrCast(@alignCast(ud)); const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
c.gtk_window_destroy(@ptrCast(self.window)); c.gtk_window_destroy(@ptrCast(self.window));
} }
fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { fn gtkConfirmClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
// Requeue the paste with force. // Requeue the paste with force.
const self: *UnsafePaste = @ptrCast(@alignCast(ud)); const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
self.core_surface.completeClipboardRequest( self.core_surface.completeClipboardRequest(
self.pending_req, self.pending_req,
self.data, self.data,
@ -214,6 +229,34 @@ const ButtonsView = struct {
} }
const vfl = [_][*:0]const u8{ const vfl = [_][*:0]const u8{
"H:[cancel]-8-[paste]-8-|", "H:[cancel]-8-[confirm]-8-|",
}; };
}; };
/// The title of the window, based on the reason the prompt is being shown.
fn titleText(reason: ClipboardPromptReason) [:0]const u8 {
return switch (reason) {
.unsafe => "Warning: Potentially Unsafe Paste",
.read, .write => "Authorize Clipboard Access",
_ => unreachable,
};
}
/// The text to display in the prompt window, based on the reason the prompt
/// is being shown.
fn promptText(reason: ClipboardPromptReason) [:0]const u8 {
return switch (reason) {
.unsafe =>
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
,
.read =>
\\An appliclication is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
,
.write =>
\\An application is attempting to write to the clipboard.
\\The content to write is shown below.
,
_ => unreachable,
};
}

View File

@ -13,7 +13,7 @@ const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig"); const App = @import("App.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const inspector = @import("inspector.zig"); const inspector = @import("inspector.zig");
const gtk_key = @import("key.zig"); const gtk_key = @import("key.zig");
const c = @import("c.zig"); const c = @import("c.zig");
@ -517,10 +517,19 @@ pub fn setClipboardString(
clipboard_type: apprt.Clipboard, clipboard_type: apprt.Clipboard,
confirm: bool, confirm: bool,
) !void { ) !void {
// TODO: implement confirmation dialog when clipboard-write is "ask" if (!confirm) {
_ = confirm; const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); c.gdk_clipboard_set_text(clipboard, val.ptr);
c.gdk_clipboard_set_text(clipboard, val.ptr); } else {
ClipboardConfirmationWindow.create(
self.app,
val,
self.core_surface,
.{ .osc_52_write = clipboard_type },
) catch |window_err| {
log.err("failed to create clipboard confirmation window err={}", .{window_err});
};
}
} }
const ClipboardRequest = struct { const ClipboardRequest = struct {
@ -557,15 +566,17 @@ fn gtkClipboardRead(
str, str,
false, false,
) catch |err| switch (err) { ) catch |err| switch (err) {
error.UnsafePaste => { error.UnsafePaste,
error.UnauthorizedPaste,
=> {
// Create a dialog and ask the user if they want to paste anyway. // Create a dialog and ask the user if they want to paste anyway.
UnsafePasteWindow.create( ClipboardConfirmationWindow.create(
self.app, self.app,
str, str,
self.core_surface, self.core_surface,
req.state, req.state,
) catch |window_err| { ) catch |window_err| {
log.err("failed to create unsafe paste window err={}", .{window_err}); log.err("failed to create clipboard confirmation window err={}", .{window_err});
}; };
return; return;
}, },

View File

@ -38,8 +38,11 @@ pub const ClipboardRequest = union(enum) {
/// A direct paste of clipboard contents. /// A direct paste of clipboard contents.
paste: void, paste: void,
/// A request to read clipboard contents via OSC 52.
osc_52_read: Clipboard,
/// A request to write clipboard contents via OSC 52. /// A request to write clipboard contents via OSC 52.
osc_52: u8, osc_52_write: Clipboard,
}; };
/// The reason for displaying a clipboard prompt to the user /// The reason for displaying a clipboard prompt to the user

View File

@ -1,3 +1,4 @@
const apprt = @import("../apprt.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const Surface = @import("../Surface.zig"); const Surface = @import("../Surface.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
@ -24,10 +25,13 @@ pub const Message = union(enum) {
cell_size: renderer.CellSize, cell_size: renderer.CellSize,
/// Read the clipboard and write to the pty. /// Read the clipboard and write to the pty.
clipboard_read: u8, clipboard_read: apprt.Clipboard,
/// Write the clipboard contents. /// Write the clipboard contents.
clipboard_write: WriteReq, clipboard_write: struct {
clipboard_type: apprt.Clipboard,
req: WriteReq,
},
/// Change the configuration to the given configuration. The pointer is /// Change the configuration to the given configuration. The pointer is
/// not valid after receiving this message so any config must be used /// not valid after receiving this message so any config must be used

View File

@ -2094,20 +2094,29 @@ const StreamHandler = struct {
// iTerm also appears to do this but other terminals seem to only allow // iTerm also appears to do this but other terminals seem to only allow
// certain. Let's investigate more. // certain. Let's investigate more.
const clipboard_type: apprt.Clipboard = switch (kind) {
'c' => .standard,
's' => .selection,
else => .standard,
};
// Get clipboard contents // Get clipboard contents
if (data.len == 1 and data[0] == '?') { if (data.len == 1 and data[0] == '?') {
_ = self.ev.surface_mailbox.push(.{ _ = self.ev.surface_mailbox.push(.{
.clipboard_read = kind, .clipboard_read = clipboard_type,
}, .{ .forever = {} }); }, .{ .forever = {} });
return; return;
} }
// Write clipboard contents // Write clipboard contents
_ = self.ev.surface_mailbox.push(.{ _ = self.ev.surface_mailbox.push(.{
.clipboard_write = try apprt.surface.Message.WriteReq.init( .clipboard_write = .{
self.alloc, .req = try apprt.surface.Message.WriteReq.init(
data, self.alloc,
), data,
),
.clipboard_type = clipboard_type,
},
}, .{ .forever = {} }); }, .{ .forever = {} });
} }