mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
gtk: implement dropping files and strings (#4211)
This allows dropping files and strings onto Ghostty in the GTK apprt. If you drop files onto Ghostty it will be pasted as a list of shell-escaped paths separated by newlines. If you drop a string onto Ghostty it will paste the string. Normal rules for pasting (bracketed pasts, unsafe pastes) apply.
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
95
src/os/shell.zig
Normal file
95
src/os/shell.zig
Normal file
@ -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());
|
||||
}
|
Reference in New Issue
Block a user