mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
255 lines
7.9 KiB
Zig
255 lines
7.9 KiB
Zig
/// Clipboard Confirmation Window
|
|
const ClipboardConfirmation = @This();
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const apprt = @import("../../apprt.zig");
|
|
const CoreSurface = @import("../../Surface.zig");
|
|
const App = @import("App.zig");
|
|
const View = @import("View.zig");
|
|
const c = @import("c.zig").c;
|
|
|
|
const log = std.log.scoped(.gtk);
|
|
|
|
app: *App,
|
|
window: *c.GtkWindow,
|
|
view: PrimaryView,
|
|
|
|
data: [:0]u8,
|
|
core_surface: *CoreSurface,
|
|
pending_req: apprt.ClipboardRequest,
|
|
|
|
pub fn create(
|
|
app: *App,
|
|
data: []const u8,
|
|
core_surface: *CoreSurface,
|
|
request: apprt.ClipboardRequest,
|
|
) !void {
|
|
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
|
|
|
|
const alloc = app.core_app.alloc;
|
|
const self = try alloc.create(ClipboardConfirmation);
|
|
errdefer alloc.destroy(self);
|
|
|
|
try self.init(
|
|
app,
|
|
data,
|
|
core_surface,
|
|
request,
|
|
);
|
|
|
|
app.clipboard_confirmation_window = self;
|
|
}
|
|
|
|
/// Not public because this should be called by the GTK lifecycle.
|
|
fn destroy(self: *ClipboardConfirmation) void {
|
|
const alloc = self.app.core_app.alloc;
|
|
self.app.clipboard_confirmation_window = null;
|
|
alloc.free(self.data);
|
|
alloc.destroy(self);
|
|
}
|
|
|
|
fn init(
|
|
self: *ClipboardConfirmation,
|
|
app: *App,
|
|
data: []const u8,
|
|
core_surface: *CoreSurface,
|
|
request: apprt.ClipboardRequest,
|
|
) !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, titleText(request));
|
|
c.gtk_window_set_default_size(gtk_window, 550, 275);
|
|
c.gtk_window_set_resizable(gtk_window, 0);
|
|
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
|
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "clipboard-confirmation-window");
|
|
_ = c.g_signal_connect_data(
|
|
window,
|
|
"destroy",
|
|
c.G_CALLBACK(>kDestroy),
|
|
self,
|
|
null,
|
|
c.G_CONNECT_DEFAULT,
|
|
);
|
|
|
|
// Set some state
|
|
self.* = .{
|
|
.app = app,
|
|
.window = gtk_window,
|
|
.view = undefined,
|
|
.data = try app.core_app.alloc.dupeZ(u8, data),
|
|
.core_surface = core_surface,
|
|
.pending_req = request,
|
|
};
|
|
|
|
// Show the window
|
|
const view = try PrimaryView.init(self, data);
|
|
self.view = view;
|
|
c.gtk_window_set_child(@ptrCast(window), view.root);
|
|
_ = c.gtk_widget_grab_focus(view.buttons.cancel_button);
|
|
|
|
c.gtk_widget_show(window);
|
|
|
|
// Block the main window from input.
|
|
// This will auto-revert when the window is closed.
|
|
c.gtk_window_set_modal(gtk_window, 1);
|
|
}
|
|
|
|
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud orelse return));
|
|
self.destroy();
|
|
}
|
|
|
|
const PrimaryView = struct {
|
|
root: *c.GtkWidget,
|
|
text: *c.GtkTextView,
|
|
buttons: ButtonsView,
|
|
|
|
pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView {
|
|
// All our widgets
|
|
const label = c.gtk_label_new(promptText(root.pending_req));
|
|
const buf = unsafeBuffer(data);
|
|
defer c.g_object_unref(buf);
|
|
const buttons = try ButtonsView.init(root);
|
|
const text_scroll = c.gtk_scrolled_window_new();
|
|
errdefer c.g_object_unref(text_scroll);
|
|
const text = c.gtk_text_view_new_with_buffer(buf);
|
|
errdefer c.g_object_unref(text);
|
|
c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text);
|
|
|
|
// Create our view
|
|
const view = try View.init(&.{
|
|
.{ .name = "label", .widget = label },
|
|
.{ .name = "text", .widget = text_scroll },
|
|
.{ .name = "buttons", .widget = buttons.root },
|
|
}, &vfl);
|
|
errdefer view.deinit();
|
|
|
|
// We can do additional settings once the layout is setup
|
|
c.gtk_label_set_wrap(@ptrCast(label), 1);
|
|
c.gtk_text_view_set_editable(@ptrCast(text), 0);
|
|
c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0);
|
|
c.gtk_text_view_set_top_margin(@ptrCast(text), 8);
|
|
c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8);
|
|
c.gtk_text_view_set_left_margin(@ptrCast(text), 8);
|
|
c.gtk_text_view_set_right_margin(@ptrCast(text), 8);
|
|
c.gtk_text_view_set_monospace(@ptrCast(text), 1);
|
|
|
|
return .{ .root = view.root, .text = @ptrCast(text), .buttons = buttons };
|
|
}
|
|
|
|
/// Returns the GtkTextBuffer for the data that was unsafe.
|
|
fn unsafeBuffer(data: []const u8) *c.GtkTextBuffer {
|
|
const buf = c.gtk_text_buffer_new(null);
|
|
errdefer c.g_object_unref(buf);
|
|
|
|
c.gtk_text_buffer_insert_at_cursor(buf, data.ptr, @intCast(data.len));
|
|
|
|
return buf;
|
|
}
|
|
|
|
const vfl = [_][*:0]const u8{
|
|
"H:|-8-[label]-8-|",
|
|
"H:|[text]|",
|
|
"H:|[buttons]|",
|
|
"V:|[label(<=80)][text(>=100)]-[buttons]-|",
|
|
};
|
|
};
|
|
|
|
const ButtonsView = struct {
|
|
root: *c.GtkWidget,
|
|
confirm_button: *c.GtkWidget,
|
|
cancel_button: *c.GtkWidget,
|
|
|
|
pub fn init(root: *ClipboardConfirmation) !ButtonsView {
|
|
const cancel_text, const confirm_text = switch (root.pending_req) {
|
|
.paste => .{ "Cancel", "Paste" },
|
|
.osc_52_read, .osc_52_write => .{ "Deny", "Allow" },
|
|
};
|
|
|
|
const cancel_button = c.gtk_button_new_with_label(cancel_text);
|
|
errdefer c.g_object_unref(cancel_button);
|
|
|
|
const confirm_button = c.gtk_button_new_with_label(confirm_text);
|
|
errdefer c.g_object_unref(confirm_button);
|
|
|
|
// Create our view
|
|
const view = try View.init(&.{
|
|
.{ .name = "cancel", .widget = cancel_button },
|
|
.{ .name = "confirm", .widget = confirm_button },
|
|
}, &vfl);
|
|
|
|
// Signals
|
|
_ = c.g_signal_connect_data(
|
|
cancel_button,
|
|
"clicked",
|
|
c.G_CALLBACK(>kCancelClick),
|
|
root,
|
|
null,
|
|
c.G_CONNECT_DEFAULT,
|
|
);
|
|
_ = c.g_signal_connect_data(
|
|
confirm_button,
|
|
"clicked",
|
|
c.G_CALLBACK(>kConfirmClick),
|
|
root,
|
|
null,
|
|
c.G_CONNECT_DEFAULT,
|
|
);
|
|
|
|
return .{ .root = view.root, .confirm_button = confirm_button, .cancel_button = cancel_button };
|
|
}
|
|
|
|
fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
|
|
c.gtk_window_destroy(@ptrCast(self.window));
|
|
}
|
|
|
|
fn gtkConfirmClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
|
// Requeue the paste with force.
|
|
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud));
|
|
self.core_surface.completeClipboardRequest(
|
|
self.pending_req,
|
|
self.data,
|
|
true,
|
|
) catch |err| {
|
|
std.log.err("Failed to requeue clipboard request: {}", .{err});
|
|
};
|
|
|
|
c.gtk_window_destroy(@ptrCast(self.window));
|
|
}
|
|
|
|
const vfl = [_][*:0]const u8{
|
|
"H:[cancel]-8-[confirm]-8-|",
|
|
};
|
|
};
|
|
|
|
/// The title of the window, based on the reason the prompt is being shown.
|
|
fn titleText(req: apprt.ClipboardRequest) [:0]const u8 {
|
|
return switch (req) {
|
|
.paste => "Warning: Potentially Unsafe Paste",
|
|
.osc_52_read, .osc_52_write => "Authorize Clipboard Access",
|
|
};
|
|
}
|
|
|
|
/// The text to display in the prompt window, based on the reason the prompt
|
|
/// is being shown.
|
|
fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
|
|
return switch (req) {
|
|
.paste =>
|
|
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
|
|
,
|
|
.osc_52_read =>
|
|
\\An application is attempting to read from the clipboard.
|
|
\\The current clipboard contents are shown below.
|
|
,
|
|
.osc_52_write =>
|
|
\\An application is attempting to write to the clipboard.
|
|
\\The content to write is shown below.
|
|
,
|
|
};
|
|
}
|