diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 180f986ca..60b119aaa 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -497,6 +497,17 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { c.gtk_widget_set_focusable(gl_area, 1); c.gtk_widget_set_focus_on_click(gl_area, 1); + // Set up to handle items being dropped on our surface. Files can be dropped + // from Nautilus and strings can be dropped from many programs. + const drop_target = c.gtk_drop_target_new(c.G_TYPE_INVALID, c.GDK_ACTION_COPY); + errdefer c.g_object_unref(drop_target); + var drop_target_types = [_]c.GType{ + c.gdk_file_list_get_type(), + c.G_TYPE_STRING, + }; + c.gtk_drop_target_set_gtypes(drop_target, @ptrCast(&drop_target_types), drop_target_types.len); + c.gtk_widget_add_controller(@ptrCast(overlay), @ptrCast(drop_target)); + // Inherit the parent's font size if we have a parent. const font_size: ?font.face.DesiredSize = font_size: { if (!app.config.@"window-inherit-font-size") break :font_size null; @@ -579,6 +590,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { _ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(drop_target, "drop", c.G_CALLBACK(>kDrop), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -2053,3 +2065,95 @@ pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void { pub fn toggleSplitZoom(self: *Surface) void { self.setSplitZoom(!self.zoomed_in); } + +/// Handle items being dropped on our surface. +fn gtkDrop( + _: *c.GtkDropTarget, + value: *c.GValue, + x: f64, + y: f64, + ud: ?*anyopaque, +) callconv(.C) c.gboolean { + _ = x; + _ = y; + const self = userdataSelf(ud.?); + const alloc = self.app.core_app.alloc; + + if (g_value_holds(value, c.G_TYPE_BOXED)) { + var data = std.ArrayList(u8).init(alloc); + defer data.deinit(); + + var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ + .child_writer = data.writer(), + }; + const writer = shell_escape_writer.writer(); + + const fl: *c.GdkFileList = @ptrCast(c.g_value_get_boxed(value)); + var l = c.gdk_file_list_get_files(fl); + + while (l != null) : (l = l.*.next) { + const file: *c.GFile = @ptrCast(l.*.data); + const path = c.g_file_get_path(file) orelse continue; + + writer.writeAll(std.mem.span(path)) catch |err| { + log.err("unable to write path to buffer: {}", .{err}); + continue; + }; + writer.writeAll("\n") catch |err| { + log.err("unable to write to buffer: {}", .{err}); + continue; + }; + } + + const string = data.toOwnedSliceSentinel(0) catch |err| { + log.err("unable to convert to a slice: {}", .{err}); + return 1; + }; + defer alloc.free(string); + + self.doPaste(string); + + return 1; + } + + if (g_value_holds(value, c.G_TYPE_STRING)) { + if (c.g_value_get_string(value)) |string| { + self.doPaste(std.mem.span(string)); + } + return 1; + } + + return 1; +} + +fn doPaste(self: *Surface, data: [:0]const u8) void { + if (data.len == 0) return; + + self.core_surface.completeClipboardRequest(.paste, data, false) catch |err| switch (err) { + error.UnsafePaste, + error.UnauthorizedPaste, + => { + ClipboardConfirmationWindow.create( + self.app, + data, + &self.core_surface, + .paste, + ) catch |window_err| { + log.err("failed to create clipboard confirmation window err={}", .{window_err}); + }; + }, + error.OutOfMemory, + error.NoSpaceLeft, + => log.err("failed to complete clipboard request err={}", .{err}), + }; +} + +/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's +/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it. +fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { + if (value_) |value| { + if (value.*.g_type == g_type) return true; + return c.g_type_check_value_holds(value, g_type) != 0; + } + return false; +} diff --git a/src/os/main.zig b/src/os/main.zig index e652a7981..df6f894f5 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -21,6 +21,7 @@ pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); pub const macos = @import("macos.zig"); +pub const shell = @import("shell.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); @@ -48,3 +49,4 @@ pub const open = openpkg.open; pub const OpenType = openpkg.Type; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; +pub const ShellEscapeWriter = shell.ShellEscapeWriter; diff --git a/src/os/shell.zig b/src/os/shell.zig new file mode 100644 index 000000000..23648a82a --- /dev/null +++ b/src/os/shell.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const testing = std.testing; + +/// Writer that escapes characters that shells treat specially to reduce the +/// risk of injection attacks or other such weirdness. Specifically excludes +/// linefeeds so that they can be used to delineate lists of file paths. +/// +/// T should be a Zig type that follows the `std.io.Writer` interface. +pub fn ShellEscapeWriter(comptime T: type) type { + return struct { + child_writer: T, + + fn write(self: *ShellEscapeWriter(T), data: []const u8) error{Error}!usize { + var count: usize = 0; + for (data) |byte| { + const buf = switch (byte) { + '\\', + '"', + '\'', + '$', + '`', + '*', + '?', + ' ', + '|', + => &[_]u8{ '\\', byte }, + else => &[_]u8{byte}, + }; + self.child_writer.writeAll(buf) catch return error.Error; + count += 1; + } + return count; + } + + const Writer = std.io.Writer(*ShellEscapeWriter(T), error{Error}, write); + + pub fn writer(self: *ShellEscapeWriter(T)) Writer { + return .{ .context = self }; + } + }; +} + +test "shell escape 1" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("abc"); + try testing.expectEqualStrings("abc", fmt.getWritten()); +} + +test "shell escape 2" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("a c"); + try testing.expectEqualStrings("a\\ c", fmt.getWritten()); +} + +test "shell escape 3" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("a?c"); + try testing.expectEqualStrings("a\\?c", fmt.getWritten()); +} + +test "shell escape 4" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("a\\c"); + try testing.expectEqualStrings("a\\\\c", fmt.getWritten()); +} + +test "shell escape 5" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("a|c"); + try testing.expectEqualStrings("a\\|c", fmt.getWritten()); +} + +test "shell escape 6" { + var buf: [128]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; + const writer = shell.writer(); + try writer.writeAll("a\"c"); + try testing.expectEqualStrings("a\\\"c", fmt.getWritten()); +}