gtk: add GSettings generic module and respect gtk-enable-primary-paste on gtk systems (#10328)

# Add GSettings Support for Primary Paste

Implements support for `org.gnome.desktop.interface
gtk-enable-primary-paste` to allow users to disable middle-click paste.
Also refactors GTK Settings access into a reusable generic module.

## Changes

- **NEW**: `src/apprt/gtk/gsettings.zig` - Generic GTK Settings reader
supporting `bool` and `c_int` types, portal-aware for Flatpak/Snap
- **MODIFIED**: `src/apprt/gtk/class/surface.zig` - Reads primary paste
setting and refactors gtk-xft-dpi to use new module

## Behavior
- Setting `false` → Middle-click paste blocked
- Setting `true` or unavailable → Middle-click paste works (default)
- Uses GTK Settings API which automatically uses XDG Desktop Portal in
sandboxed environments

Note: No unit tests added as this is a thin wrapper around GTK Settings
API that's already tested indirectly through surface.zig. Happy to add
tests if desired, though they would require an active display
environment and skip on most CI systems.
This commit is contained in:
Mitchell Hashimoto
2026-01-20 09:49:48 -08:00
committed by GitHub
2 changed files with 122 additions and 7 deletions

View File

@@ -19,6 +19,7 @@ const terminal = @import("../../../terminal/main.zig");
const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig");
const ext = @import("../ext.zig");
const gsettings = @import("../gsettings.zig");
const gtk_key = @import("../key.zig");
const ApprtSurface = @import("../Surface.zig");
const Common = @import("../class.zig").Common;
@@ -674,6 +675,9 @@ pub const Surface = extern struct {
/// The context for this surface (window, tab, or split)
context: apprt.surface.NewSurfaceContext = .window,
/// Whether primary paste (middle-click paste) is enabled.
gtk_enable_primary_paste: bool = true,
pub var offset: c_int = 0;
};
@@ -1511,19 +1515,17 @@ pub const Surface = extern struct {
const xft_dpi_scale = xft_scale: {
// gtk-xft-dpi is font DPI multiplied by 1024. See
// https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
const settings = gtk.Settings.getDefault() orelse break :xft_scale 1.0;
var value = std.mem.zeroes(gobject.Value);
defer value.unset();
_ = value.init(gobject.ext.typeFor(c_int));
settings.as(gobject.Object).getProperty("gtk-xft-dpi", &value);
const gtk_xft_dpi = value.getInt();
const gtk_xft_dpi = gsettings.get(.@"gtk-xft-dpi") orelse {
log.warn("gtk-xft-dpi was not set, using default value", .{});
break :xft_scale 1.0;
};
// Use a value of 1.0 for the XFT DPI scale if the setting is <= 0
// See:
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead
if (gtk_xft_dpi <= 0) {
log.warn("gtk-xft-dpi was not set, using default value", .{});
log.warn("gtk-xft-dpi has invalid value ({}), using default", .{gtk_xft_dpi});
break :xft_scale 1.0;
}
@@ -1767,6 +1769,9 @@ pub const Surface = extern struct {
priv.im_composing = false;
priv.im_len = 0;
// Read GTK primary paste setting
priv.gtk_enable_primary_paste = gsettings.get(.@"gtk-enable-primary-paste") orelse true;
// Set up to handle items being dropped on our surface. Files can be dropped
// from Nautilus and strings can be dropped from many programs. The order
// of these types matter.
@@ -2685,6 +2690,11 @@ pub const Surface = extern struct {
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
if (button == .middle and !priv.gtk_enable_primary_paste) {
return;
}
const consumed = consumed: {
const gtk_mods = event.getModifierState();
const mods = gtk_key.translateMods(gtk_mods);
@@ -2736,6 +2746,10 @@ pub const Surface = extern struct {
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
if (button == .middle and !priv.gtk_enable_primary_paste) {
return;
}
const mods = gtk_key.translateMods(gtk_mods);
const consumed = surface.mouseButtonCallback(
.release,

101
src/apprt/gtk/gsettings.zig Normal file
View File

@@ -0,0 +1,101 @@
const std = @import("std");
const gtk = @import("gtk");
const gobject = @import("gobject");
/// GTK Settings keys with well-defined types.
pub const Key = enum {
@"gtk-enable-primary-paste",
@"gtk-xft-dpi",
@"gtk-font-name",
fn Type(comptime self: Key) type {
return switch (self) {
.@"gtk-enable-primary-paste" => bool,
.@"gtk-xft-dpi" => c_int,
.@"gtk-font-name" => []const u8,
};
}
fn GValueType(comptime self: Key) type {
return switch (self.Type()) {
bool => c_int,
c_int => c_int,
[]const u8 => ?[*:0]const u8,
else => @compileError("Unsupported type for GTK settings"),
};
}
/// Returns true if this setting type requires memory allocation.
/// Types that do not need allocation must be explicitly marked.
fn requiresAllocation(comptime self: Key) bool {
const T = self.Type();
return switch (T) {
bool, c_int => false,
else => true,
};
}
};
/// Reads a GTK setting for non-allocating types.
/// Automatically uses XDG Desktop Portal in Flatpak environments.
/// Returns null if the setting is unavailable.
pub fn get(comptime key: Key) ?key.Type() {
if (comptime key.requiresAllocation()) {
@compileError("Allocating types require an allocator; use getAlloc() instead");
}
const settings = gtk.Settings.getDefault() orelse return null;
return getImpl(settings, null, key) catch unreachable;
}
/// Reads a GTK setting, allocating memory if necessary.
/// Automatically uses XDG Desktop Portal in Flatpak environments.
/// Caller must free returned memory with the provided allocator.
/// Returns null if the setting is unavailable.
pub fn getAlloc(allocator: std.mem.Allocator, comptime key: Key) !?key.Type() {
const settings = gtk.Settings.getDefault() orelse return null;
return getImpl(settings, allocator, key);
}
fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: Key) !?key.Type() {
const GValType = key.GValueType();
var value = gobject.ext.Value.new(GValType);
defer value.unset();
settings.as(gobject.Object).getProperty(@tagName(key).ptr, &value);
return switch (key.Type()) {
bool => value.getInt() != 0,
c_int => value.getInt(),
[]const u8 => blk: {
const alloc = allocator.?;
const ptr = value.getString() orelse break :blk null;
const str = std.mem.span(ptr);
break :blk try alloc.dupe(u8, str);
},
else => @compileError("Unsupported type for GTK settings"),
};
}
test "Key.Type returns correct types" {
try std.testing.expectEqual(bool, Key.@"gtk-enable-primary-paste".Type());
try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".Type());
try std.testing.expectEqual([]const u8, Key.@"gtk-font-name".Type());
}
test "Key.requiresAllocation identifies allocating types" {
try std.testing.expectEqual(false, Key.@"gtk-enable-primary-paste".requiresAllocation());
try std.testing.expectEqual(false, Key.@"gtk-xft-dpi".requiresAllocation());
try std.testing.expectEqual(true, Key.@"gtk-font-name".requiresAllocation());
}
test "Key.GValueType returns correct GObject types" {
try std.testing.expectEqual(c_int, Key.@"gtk-enable-primary-paste".GValueType());
try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".GValueType());
try std.testing.expectEqual(?[*:0]const u8, Key.@"gtk-font-name".GValueType());
}
test "@tagName returns correct GTK property names" {
try std.testing.expectEqualStrings("gtk-enable-primary-paste", @tagName(Key.@"gtk-enable-primary-paste"));
try std.testing.expectEqualStrings("gtk-xft-dpi", @tagName(Key.@"gtk-xft-dpi"));
try std.testing.expectEqualStrings("gtk-font-name", @tagName(Key.@"gtk-font-name"));
}