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_focusable(gl_area, 1);
|
||||||
c.gtk_widget_set_focus_on_click(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.
|
// Inherit the parent's font size if we have a parent.
|
||||||
const font_size: ?font.face.DesiredSize = font_size: {
|
const font_size: ?font.face.DesiredSize = font_size: {
|
||||||
if (!app.config.@"window-inherit-font-size") break :font_size null;
|
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-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, "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(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 {
|
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 {
|
pub fn toggleSplitZoom(self: *Surface) void {
|
||||||
self.setSplitZoom(!self.zoomed_in);
|
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 xdg = @import("xdg.zig");
|
||||||
pub const windows = @import("windows.zig");
|
pub const windows = @import("windows.zig");
|
||||||
pub const macos = @import("macos.zig");
|
pub const macos = @import("macos.zig");
|
||||||
|
pub const shell = @import("shell.zig");
|
||||||
|
|
||||||
// Functions and types
|
// Functions and types
|
||||||
pub const CFReleaseThread = @import("cf_release_thread.zig");
|
pub const CFReleaseThread = @import("cf_release_thread.zig");
|
||||||
@ -48,3 +49,4 @@ pub const open = openpkg.open;
|
|||||||
pub const OpenType = openpkg.Type;
|
pub const OpenType = openpkg.Type;
|
||||||
pub const pipe = pipepkg.pipe;
|
pub const pipe = pipepkg.pipe;
|
||||||
pub const resourcesDir = resourcesdir.resourcesDir;
|
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