gtk: implement sensitive content reveal on paste confirmation (#6054)

Fixes https://github.com/ghostty-org/ghostty/issues/4947 for gtk
This PR implements the senstive content hiding when displaying the paste
confirmation dialog in secure input mode.

Following changes are implemented:
- in the blueprint for each dialog add a show/hide button that is not
visible by default, and a Revealer that is revealed by default
- save the `secure_input` action value for each surface in the GTK apprt
- pass the value when initializing the paste confirmation dialog
- in the dialog code, alter the visibility of the content and
reveal/hide buttons based on secure input flag value

Demo:


https://github.com/user-attachments/assets/c91cbd3d-ed3b-464d-b4cf-e51fe7aa23b7

I feel like this is already a nearly full implementation, but I'm
leaving this as a draft for now, since i need to look into blueprints
for Adwaita 1.2, and verify if it behaves properly when the dialog is in
not-sensitive input mode and in OSC52 mode.
This commit is contained in:
Jeffrey C. Ollie
2025-03-05 14:27:13 -06:00
committed by GitHub
13 changed files with 563 additions and 116 deletions

View File

@ -508,12 +508,12 @@ pub fn performAction(
.quit_timer => self.quitTimer(value),
.prompt_title => try self.promptTitle(target),
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
.secure_input => self.setSecureInput(target, value),
// Unimplemented
.close_all_windows,
.toggle_visibility,
.cell_size,
.secure_input,
.key_sequence,
.render_inspector,
.renderer_health,
@ -1415,6 +1415,15 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
window.present();
}
fn setSecureInput(_: *App, target: apprt.Target, value: apprt.action.SecureInput) void {
switch (target) {
.app => {},
.surface => |surface| {
surface.rt_surface.setSecureInput(value);
},
}
}
fn quit(self: *App) void {
// If we're already not running, do nothing.
if (!self.running) return;

View File

@ -25,12 +25,17 @@ dialog: *DialogType,
data: [:0]u8,
core_surface: *CoreSurface,
pending_req: apprt.ClipboardRequest,
text_view: *gtk.TextView,
text_view_scroll: *gtk.ScrolledWindow,
reveal_button: *gtk.Button,
hide_button: *gtk.Button,
pub fn create(
app: *App,
data: []const u8,
core_surface: *CoreSurface,
request: apprt.ClipboardRequest,
is_secure_input: bool,
) !void {
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
@ -43,6 +48,7 @@ pub fn create(
data,
core_surface,
request,
is_secure_input,
);
app.clipboard_confirmation_window = self;
@ -62,6 +68,7 @@ fn init(
data: []const u8,
core_surface: *CoreSurface,
request: apprt.ClipboardRequest,
is_secure_input: bool,
) !void {
var builder = switch (DialogType) {
adw.AlertDialog => switch (request) {
@ -79,6 +86,10 @@ fn init(
defer builder.deinit();
const dialog = builder.getObject(DialogType, "clipboard_confirmation_window").?;
const text_view = builder.getObject(gtk.TextView, "text_view").?;
const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
const hide_button = builder.getObject(gtk.Button, "hide_button").?;
const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?;
const copy = try app.core_app.alloc.dupeZ(u8, data);
errdefer app.core_app.alloc.free(copy);
@ -88,15 +99,39 @@ fn init(
.data = copy,
.core_surface = core_surface,
.pending_req = request,
.text_view = text_view,
.text_view_scroll = text_view_scroll,
.reveal_button = reveal_button,
.hide_button = hide_button,
};
const text_view = builder.getObject(gtk.TextView, "text_view").?;
const buffer = gtk.TextBuffer.new(null);
errdefer buffer.unref();
buffer.insertAtCursor(copy.ptr, @intCast(copy.len));
text_view.setBuffer(buffer);
if (is_secure_input) {
text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
self.text_view.as(gtk.Widget).addCssClass("blurred");
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
_ = gtk.Button.signals.clicked.connect(
reveal_button,
*ClipboardConfirmation,
gtkRevealButtonClicked,
self,
.{},
);
_ = gtk.Button.signals.clicked.connect(
hide_button,
*ClipboardConfirmation,
gtkHideButtonClicked,
self,
.{},
);
}
switch (DialogType) {
adw.AlertDialog => {
const parent: ?*gtk.Widget = widget: {
@ -152,3 +187,19 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation)
}
self.destroy();
}
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
self.text_view.as(gtk.Widget).removeCssClass("blurred");
self.hide_button.as(gtk.Widget).setVisible(@intFromBool(true));
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
}
fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
self.text_view.as(gtk.Widget).addCssClass("blurred");
self.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
}

View File

@ -308,6 +308,9 @@ context_menu: Menu(Surface, "context_menu", false),
/// True when we have a precision scroll in progress
precision_scroll: bool = false,
/// Flag indicating whether the surface is in secure input mode.
is_secure_input: bool = false,
/// The state of the key event while we're doing IM composition.
/// See gtkKeyPressed for detailed descriptions.
pub const IMKeyEvent = enum {
@ -1163,6 +1166,7 @@ pub fn setClipboardString(
val,
&self.core_surface,
.{ .osc_52_write = clipboard_type },
self.is_secure_input,
) catch |window_err| {
log.err("failed to create clipboard confirmation window err={}", .{window_err});
};
@ -1211,6 +1215,7 @@ fn gtkClipboardRead(
str,
&self.core_surface,
req.state,
self.is_secure_input,
) catch |window_err| {
log.err("failed to create clipboard confirmation window err={}", .{window_err});
};
@ -2231,6 +2236,7 @@ fn doPaste(self: *Surface, data: [:0]const u8) void {
data,
&self.core_surface,
.paste,
self.is_secure_input,
) catch |window_err| {
log.err("failed to create clipboard confirmation window err={}", .{window_err});
};
@ -2323,3 +2329,11 @@ fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncRes
}
}
}
pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void {
switch (value) {
.on => self.is_secure_input = true,
.off => self.is_secure_input = false,
.toggle => self.is_secure_input = !self.is_secure_input,
}
}

View File

@ -63,3 +63,13 @@ window.ssd.no-border-radius {
margin: 0;
padding: 0;
}
.clipboard-content-view {
filter: blur(0px);
transition: filter 0.3s ease;
}
.clipboard-content-view.blurred {
filter: blur(5px);
transition: filter 0.3s ease;
}

View File

@ -14,18 +14,58 @@ Adw.MessageDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
extra-child: Overlay {
styles [
"osd"
]
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view"
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque"
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@ -7,27 +7,68 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<interface domain="com.mitchellh.ghostty">
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
<property name="heading" translatable="yes">Authorize Clipboard Access</property>
<property name="body" translatable="yes">An application is attempting to read from the clipboard. The current clipboard contents are shown below.</property>
<property name="heading" translatable="true">Authorize Clipboard Access</property>
<property name="body" translatable="true">An application is attempting to read from the clipboard. The current clipboard contents are shown below.</property>
<responses>
<response id="cancel" translatable="yes" appearance="suggested">Deny</response>
<response id="ok" translatable="yes" appearance="destructive">Allow</response>
<response id="cancel" translatable="true" appearance="suggested">Deny</response>
<response id="ok" translatable="true" appearance="destructive">Allow</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="width-request">500</property>
<property name="height-request">250</property>
<object class="GtkOverlay">
<style>
<class name="osd"/>
</style>
<child>
<object class="GtkTextView" id="text_view">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
<object class="GtkScrolledWindow" id="text_view_scroll">
<property name="width-request">500</property>
<property name="height-request">250</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
<style>
<class name="clipboard-content-view"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="reveal_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="hide_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<style>
<class name="opaque"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">view-conceal-symbolic</property>
</object>
</child>
</object>
</child>
</object>

View File

@ -14,18 +14,58 @@ Adw.MessageDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
extra-child: Overlay {
styles [
"osd"
]
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view"
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque"
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@ -7,27 +7,68 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<interface domain="com.mitchellh.ghostty">
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
<property name="heading" translatable="yes">Authorize Clipboard Access</property>
<property name="body" translatable="yes">An application is attempting to write to the clipboard. The current clipboard contents are shown below.</property>
<property name="heading" translatable="true">Authorize Clipboard Access</property>
<property name="body" translatable="true">An application is attempting to write to the clipboard. The current clipboard contents are shown below.</property>
<responses>
<response id="cancel" translatable="yes" appearance="suggested">Deny</response>
<response id="ok" translatable="yes" appearance="destructive">Allow</response>
<response id="cancel" translatable="true" appearance="suggested">Deny</response>
<response id="ok" translatable="true" appearance="destructive">Allow</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="width-request">500</property>
<property name="height-request">250</property>
<object class="GtkOverlay">
<style>
<class name="osd"/>
</style>
<child>
<object class="GtkTextView" id="text_view">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
<object class="GtkScrolledWindow" id="text_view_scroll">
<property name="width-request">500</property>
<property name="height-request">250</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
<style>
<class name="clipboard-content-view"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="reveal_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="hide_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<style>
<class name="opaque"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">view-conceal-symbolic</property>
</object>
</child>
</object>
</child>
</object>

View File

@ -14,18 +14,58 @@ Adw.MessageDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
extra-child: Overlay {
styles [
"osd"
]
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view"
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque"
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@ -7,27 +7,68 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<interface domain="com.mitchellh.ghostty">
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
<property name="heading" translatable="yes">Warning: Potentially Unsafe Paste</property>
<property name="body" translatable="yes">Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.</property>
<property name="heading" translatable="true">Warning: Potentially Unsafe Paste</property>
<property name="body" translatable="true">Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.</property>
<responses>
<response id="cancel" translatable="yes" appearance="suggested">Cancel</response>
<response id="ok" translatable="yes" appearance="destructive">Paste</response>
<response id="cancel" translatable="true" appearance="suggested">Cancel</response>
<response id="ok" translatable="true" appearance="destructive">Paste</response>
</responses>
<property name="default-response">cancel</property>
<property name="close-response">cancel</property>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="width-request">500</property>
<property name="height-request">250</property>
<object class="GtkOverlay">
<style>
<class name="osd"/>
</style>
<child>
<object class="GtkTextView" id="text_view">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
<object class="GtkScrolledWindow" id="text_view_scroll">
<property name="width-request">500</property>
<property name="height-request">250</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="cursor-visible">false</property>
<property name="editable">false</property>
<property name="monospace">true</property>
<property name="top-margin">8</property>
<property name="left-margin">8</property>
<property name="bottom-margin">8</property>
<property name="right-margin">8</property>
<style>
<class name="clipboard-content-view"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="reveal_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="hide_button">
<property name="visible">false</property>
<property name="halign">2</property>
<property name="valign">1</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<style>
<class name="opaque"/>
</style>
<child>
<object class="GtkImage">
<property name="icon-name">view-conceal-symbolic</property>
</object>
</child>
</object>
</child>
</object>

View File

@ -14,18 +14,58 @@ Adw.AlertDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
extra-child: Overlay {
styles [
"osd"
]
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view"
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque"
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@ -14,18 +14,58 @@ Adw.AlertDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
extra-child: Overlay {
styles [
"osd"
]
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view"
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque"
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}

View File

@ -10,22 +10,62 @@ Adw.AlertDialog clipboard_confirmation_window {
cancel: _("Cancel") suggested,
ok: _("Paste") destructive
]
default-response: "cancel";
close-response: "cancel";
extra-child: ScrolledWindow {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
extra-child: Overlay {
styles [
"osd"
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view"
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque"
]
Image {
icon-name: "view-conceal-symbolic";
}
}
};
}