From 960a1bb091cc2081e25eb076fe2ad505873b88dc Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Fri, 10 Nov 2023 12:04:53 -0600 Subject: [PATCH] gtk: implement OSC 52 prompts --- src/Surface.zig | 44 +++++--- src/apprt/glfw.zig | 5 +- src/apprt/gtk/App.zig | 6 +- ...ow.zig => ClipboardConfirmationWindow.zig} | 101 +++++++++++++----- src/apprt/gtk/Surface.zig | 27 +++-- src/apprt/structs.zig | 5 +- src/apprt/surface.zig | 8 +- src/termio/Exec.zig | 19 +++- 8 files changed, 151 insertions(+), 64 deletions(-) rename src/apprt/gtk/{UnsafePasteWindow.zig => ClipboardConfirmationWindow.zig} (63%) diff --git a/src/Surface.zig b/src/Surface.zig index e5e154ecd..01e70d3bf 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -688,21 +688,21 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .cell_size => |size| try self.setCellSize(size), - .clipboard_read => |kind| { + .clipboard_read => |clipboard| { if (self.config.clipboard_read == .deny) { log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}); return; } - try self.startClipboardRequest(.standard, .{ .osc_52 = kind }); + try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard }); }, - .clipboard_write => |req| switch (req) { - .small => |v| try self.clipboardWrite(v.data[0..v.len], .standard), - .stable => |v| try self.clipboardWrite(v, .standard), + .clipboard_write => |w| switch (w.req) { + .small => |v| try self.clipboardWrite(v.data[0..v.len], w.clipboard_type), + .stable => |v| try self.clipboardWrite(v, w.clipboard_type), .alloc => |v| { 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); + // 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; self.rt_surface.setClipboardString(buf, loc, confirm) catch |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 /// 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 -/// `clipboard-read` option is set to `ask`, this will return error.UnsafePaste. +/// If `confirmed` is false then this may return either an UnsafePaste or +/// UnauthorizedPaste error, depending on the type of clipboard request. pub fn completeClipboardRequest( self: *Surface, req: apprt.ClipboardRequest, - data: []const u8, + data: [:0]const u8, confirmed: bool, ) !void { switch (req) { .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 { switch (req) { .paste => {}, // always allowed - .osc_52 => if (self.config.clipboard_read == .deny) { + .osc_52_read => if (self.config.clipboard_read == .deny) { log.info( "application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}, ); return; }, + + // No clipboard write code paths travel through this function + .osc_52_write => unreachable, } try self.rt_surface.clipboardRequest(loc, req); @@ -2642,7 +2650,12 @@ fn completeClipboardPaste( 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 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 defer self.alloc.free(buf); + const kind: u8 = switch (clipboard_type) { + .standard => 'c', + .selection => 's', + }; + // Wrap our data with the OSC code const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind}); assert(prefix.len == 7); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 9f1bb5a4d..7bf5a9e98 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -656,15 +656,14 @@ pub const Surface = struct { state: apprt.ClipboardRequest, ) !void { // 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(), .selection => selection: { // Not supported except on Linux if (comptime builtin.os.tag != .linux) break :selection ""; - const raw = glfwNative.getX11SelectionString() orelse + break :selection glfwNative.getX11SelectionString() orelse return glfw.mustGetErrorCode(); - break :selection std.mem.span(raw); }, }; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 2456d2ada..b70cae258 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -24,7 +24,7 @@ const build_options = @import("build_options"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); -const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); +const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); const c = @import("c.zig"); const inspector = @import("inspector.zig"); const key = @import("key.zig"); @@ -49,8 +49,8 @@ menu: ?*c.GMenu = null, /// The configuration errors window, if it is currently open. config_errors_window: ?*ConfigErrorsWindow = null, -/// The unsafe paste window, if it is currently open. -unsafe_paste_window: ?*UnsafePasteWindow = null, +/// The clipboard confirmation window, if it is currently open. +clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null, /// This is set to false when the main loop should exit. running: bool = true, diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig similarity index 63% rename from src/apprt/gtk/UnsafePasteWindow.zig rename to src/apprt/gtk/ClipboardConfirmationWindow.zig index e3582e892..321a51128 100644 --- a/src/apprt/gtk/UnsafePasteWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -1,11 +1,12 @@ -/// Unsafe Paste Window -const UnsafePaste = @This(); +/// Clipboard Confirmation Window +const ClipboardConfirmation = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const CoreSurface = @import("../../Surface.zig"); const ClipboardRequest = @import("../structs.zig").ClipboardRequest; +const ClipboardPromptReason = @import("../structs.zig").ClipboardPromptReason; const App = @import("App.zig"); const View = @import("View.zig"); const c = @import("c.zig"); @@ -16,9 +17,10 @@ app: *App, window: *c.GtkWindow, view: PrimaryView, -data: []u8, +data: [:0]u8, core_surface: CoreSurface, pending_req: ClipboardRequest, +reason: ClipboardPromptReason, pub fn create( app: *App, @@ -26,40 +28,49 @@ pub fn create( core_surface: CoreSurface, request: ClipboardRequest, ) !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 self = try alloc.create(UnsafePaste); + const self = try alloc.create(ClipboardConfirmation); errdefer alloc.destroy(self); + + const reason: ClipboardPromptReason = switch (request) { + .paste => .unsafe, + .osc_52_read => .read, + .osc_52_write => .write, + }; + try self.init( app, data, core_surface, request, + reason, ); - app.unsafe_paste_window = self; + app.clipboard_confirmation_window = self; } /// 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; - self.app.unsafe_paste_window = null; + self.app.clipboard_confirmation_window = null; alloc.destroy(self); } fn init( - self: *UnsafePaste, + self: *ClipboardConfirmation, app: *App, data: []const u8, core_surface: CoreSurface, request: ClipboardRequest, + reason: ClipboardPromptReason, ) !void { // Create the window const window = c.gtk_window_new(); const gtk_window: *c.GtkWindow = @ptrCast(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_resizable(gtk_window, 0); _ = c.g_signal_connect_data( @@ -76,9 +87,10 @@ fn init( .app = app, .window = gtk_window, .view = undefined, - .data = try app.core_app.alloc.dupe(u8, data), + .data = try app.core_app.alloc.dupeZ(u8, data), .core_surface = core_surface, .pending_req = request, + .reason = reason, }; // Show the window @@ -93,7 +105,7 @@ fn init( } 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(); } @@ -101,12 +113,9 @@ const PrimaryView = struct { root: *c.GtkWidget, text: *c.GtkTextView, - pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView { + pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView { // All our widgets - const label = c.gtk_label_new( - "Pasting this text into the terminal may be dangerous as " ++ - "it looks like some commands may be executed.", - ); + const label = c.gtk_label_new(promptText(root.reason)); const buf = unsafeBuffer(data); defer c.g_object_unref(buf); const buttons = try ButtonsView.init(root); @@ -157,20 +166,26 @@ const PrimaryView = struct { const ButtonsView = struct { root: *c.GtkWidget, - pub fn init(root: *UnsafePaste) !ButtonsView { - const cancel_button = c.gtk_button_new_with_label("Cancel"); + pub fn init(root: *ClipboardConfirmation) !ButtonsView { + 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); - const paste_button = c.gtk_button_new_with_label("Paste"); - errdefer c.g_object_unref(paste_button); + const confirm_button = c.gtk_button_new_with_label(confirm_text); + errdefer c.g_object_unref(confirm_button); // TODO: Focus on the paste button - // c.gtk_widget_grab_focus(paste_button); + // c.gtk_widget_grab_focus(confirm_button); // Create our view const view = try View.init(&.{ .{ .name = "cancel", .widget = cancel_button }, - .{ .name = "paste", .widget = paste_button }, + .{ .name = "confirm", .widget = confirm_button }, }, &vfl); // Signals @@ -183,9 +198,9 @@ const ButtonsView = struct { c.G_CONNECT_DEFAULT, ); _ = c.g_signal_connect_data( - paste_button, + confirm_button, "clicked", - c.G_CALLBACK(>kPasteClick), + c.G_CALLBACK(>kConfirmClick), root, null, c.G_CONNECT_DEFAULT, @@ -195,13 +210,13 @@ const ButtonsView = struct { } 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)); } - fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + fn gtkConfirmClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { // Requeue the paste with force. - const self: *UnsafePaste = @ptrCast(@alignCast(ud)); + const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud)); self.core_surface.completeClipboardRequest( self.pending_req, self.data, @@ -214,6 +229,34 @@ const ButtonsView = struct { } 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, + }; +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 0e967771e..9cc253b63 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -13,7 +13,7 @@ const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); const Window = @import("Window.zig"); -const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); +const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); @@ -517,10 +517,19 @@ pub fn setClipboardString( clipboard_type: apprt.Clipboard, confirm: bool, ) !void { - // TODO: implement confirmation dialog when clipboard-write is "ask" - _ = confirm; - const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); - c.gdk_clipboard_set_text(clipboard, val.ptr); + if (!confirm) { + const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type); + 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 { @@ -557,15 +566,17 @@ fn gtkClipboardRead( str, false, ) catch |err| switch (err) { - error.UnsafePaste => { + error.UnsafePaste, + error.UnauthorizedPaste, + => { // Create a dialog and ask the user if they want to paste anyway. - UnsafePasteWindow.create( + ClipboardConfirmationWindow.create( self.app, str, self.core_surface, req.state, ) 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; }, diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 013441857..438a96376 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -38,8 +38,11 @@ pub const ClipboardRequest = union(enum) { /// A direct paste of clipboard contents. paste: void, + /// A request to read clipboard contents via OSC 52. + osc_52_read: Clipboard, + /// 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 diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 58e3cea9b..b86fbf54b 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -1,3 +1,4 @@ +const apprt = @import("../apprt.zig"); const App = @import("../App.zig"); const Surface = @import("../Surface.zig"); const renderer = @import("../renderer.zig"); @@ -24,10 +25,13 @@ pub const Message = union(enum) { cell_size: renderer.CellSize, /// Read the clipboard and write to the pty. - clipboard_read: u8, + clipboard_read: apprt.Clipboard, /// 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 /// not valid after receiving this message so any config must be used diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 7a409e8f7..670848cea 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2094,20 +2094,29 @@ const StreamHandler = struct { // iTerm also appears to do this but other terminals seem to only allow // certain. Let's investigate more. + const clipboard_type: apprt.Clipboard = switch (kind) { + 'c' => .standard, + 's' => .selection, + else => .standard, + }; + // Get clipboard contents if (data.len == 1 and data[0] == '?') { _ = self.ev.surface_mailbox.push(.{ - .clipboard_read = kind, + .clipboard_read = clipboard_type, }, .{ .forever = {} }); return; } // Write clipboard contents _ = self.ev.surface_mailbox.push(.{ - .clipboard_write = try apprt.surface.Message.WriteReq.init( - self.alloc, - data, - ), + .clipboard_write = .{ + .req = try apprt.surface.Message.WriteReq.init( + self.alloc, + data, + ), + .clipboard_type = clipboard_type, + }, }, .{ .forever = {} }); }