mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 17:26:09 +03:00
Merge abbc31a3cb7af5fac9ae63b7ec693d31bb8ae7f2 into 015efcf9e562ac89da49d50ba8b2b2612ef13d2a
This commit is contained in:
@ -44,6 +44,7 @@ const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
|||||||
const CloseDialog = @import("CloseDialog.zig");
|
const CloseDialog = @import("CloseDialog.zig");
|
||||||
const GlobalShortcuts = @import("GlobalShortcuts.zig");
|
const GlobalShortcuts = @import("GlobalShortcuts.zig");
|
||||||
const Split = @import("Split.zig");
|
const Split = @import("Split.zig");
|
||||||
|
const OpenURI = @import("portal.zig").OpenURI;
|
||||||
const inspector = @import("inspector.zig");
|
const inspector = @import("inspector.zig");
|
||||||
const key = @import("key.zig");
|
const key = @import("key.zig");
|
||||||
const winprotopkg = @import("winproto.zig");
|
const winprotopkg = @import("winproto.zig");
|
||||||
@ -105,6 +106,8 @@ custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .{},
|
|||||||
|
|
||||||
global_shortcuts: ?GlobalShortcuts,
|
global_shortcuts: ?GlobalShortcuts,
|
||||||
|
|
||||||
|
open_uri: OpenURI = undefined,
|
||||||
|
|
||||||
/// The timer used to quit the application after the last window is closed.
|
/// The timer used to quit the application after the last window is closed.
|
||||||
quit_timer: union(enum) {
|
quit_timer: union(enum) {
|
||||||
off: void,
|
off: void,
|
||||||
@ -448,6 +451,8 @@ pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
|
|||||||
.css_provider = css_provider,
|
.css_provider = css_provider,
|
||||||
.global_shortcuts = .init(core_app.alloc, gio_app),
|
.global_shortcuts = .init(core_app.alloc, gio_app),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try self.open_uri.init(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminate the application. The application will not be restarted after
|
// Terminate the application. The application will not be restarted after
|
||||||
@ -471,6 +476,7 @@ pub fn terminate(self: *App) void {
|
|||||||
if (self.global_shortcuts) |*shortcuts| shortcuts.deinit();
|
if (self.global_shortcuts) |*shortcuts| shortcuts.deinit();
|
||||||
|
|
||||||
self.config.deinit();
|
self.config.deinit();
|
||||||
|
self.open_uri.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform a given action. Returns `true` if the action was able to be
|
/// Perform a given action. Returns `true` if the action was able to be
|
||||||
@ -1849,17 +1855,32 @@ fn openConfig(self: *App) !bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn openUrl(
|
fn openUrl(
|
||||||
app: *App,
|
self: *App,
|
||||||
value: apprt.action.OpenUrl,
|
value: apprt.action.OpenUrl,
|
||||||
) void {
|
) void {
|
||||||
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
|
if (std.mem.startsWith(u8, value.url, "/")) {
|
||||||
|
self.openUrlFallback(value.kind, value.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, value.url, "file://")) {
|
||||||
|
self.openUrlFallback(value.kind, value.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.open_uri.start(value) catch |err| {
|
||||||
|
log.err("unable to open uri err={}", .{err});
|
||||||
|
self.openUrlFallback(value.kind, value.url);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn openUrlFallback(self: *App, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
|
||||||
// Fallback to the minimal cross-platform way of opening a URL.
|
// Fallback to the minimal cross-platform way of opening a URL.
|
||||||
// This is always a safe fallback and enables for example Windows
|
// This is always a safe fallback and enables for example Windows
|
||||||
// to open URLs (GTK on Windows via WSL is a thing).
|
// to open URLs (GTK on Windows via WSL is a thing).
|
||||||
internal_os.open(
|
internal_os.open(
|
||||||
app.core_app.alloc,
|
self.core_app.alloc,
|
||||||
value.kind,
|
kind,
|
||||||
value.url,
|
url,
|
||||||
) catch |err| log.warn("unable to open url: {}", .{err});
|
) catch |err| log.warn("unable to open url: {}", .{err});
|
||||||
}
|
}
|
||||||
|
63
src/apprt/gtk/portal.zig
Normal file
63
src/apprt/gtk/portal.zig
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const gio = @import("gio");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const OpenURI = @import("portal/OpenURI.zig");
|
||||||
|
|
||||||
|
/// Generate a token suitable for use in requests to the XDG Desktop Portal
|
||||||
|
pub fn generateToken() usize {
|
||||||
|
return std.crypto.random.int(usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the XDG portal request path for the current Ghostty instance.
|
||||||
|
///
|
||||||
|
/// If this sounds like nonsense, see `request` for an explanation as to
|
||||||
|
/// why we need to do this.
|
||||||
|
pub fn getRequestPath(alloc: Allocator, dbus: *gio.DBusConnection, token: usize) (Allocator.Error || std.fmt.BufPrintError || error{NoDBusUniqueName})![:0]const u8 {
|
||||||
|
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
|
||||||
|
// for the syntax XDG portals expect.
|
||||||
|
|
||||||
|
var token_buf: [16]u8 = undefined;
|
||||||
|
const token_string = try std.fmt.bufPrint(&token_buf, "{x:0>16}", .{token});
|
||||||
|
|
||||||
|
// Get the unique name from D-Bus and strip the leading `:`
|
||||||
|
const unique_name = try alloc.dupe(u8, std.mem.span(
|
||||||
|
dbus.getUniqueName() orelse {
|
||||||
|
return error.NoDBusUniqueName;
|
||||||
|
},
|
||||||
|
)[1..]);
|
||||||
|
defer alloc.free(unique_name);
|
||||||
|
|
||||||
|
// Sanitize the unique name by replacing every `.` with `_`. In effect, this
|
||||||
|
// will turn a unique name like `1.192` into `1_192`.
|
||||||
|
std.mem.replaceScalar(u8, unique_name, '.', '_');
|
||||||
|
|
||||||
|
const object_path = try std.mem.joinZ(
|
||||||
|
alloc,
|
||||||
|
"/",
|
||||||
|
&.{
|
||||||
|
"/org/freedesktop/portal/desktop/request",
|
||||||
|
unique_name, // Remove leading `:`
|
||||||
|
token_string,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return object_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try and parse the token out of a request path.
|
||||||
|
pub fn parseRequestPath(request_path: []const u8) ?usize {
|
||||||
|
const index = std.mem.lastIndexOfScalar(u8, request_path, '/') orelse return null;
|
||||||
|
const token = request_path[index + 1 ..];
|
||||||
|
return std.fmt.parseUnsigned(usize, token, 16) catch return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseRequestPath" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
try testing.expectEqual(0x75af01a79c6fea34, parseRequestPath("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34").?);
|
||||||
|
try testing.expectEqual(null, parseRequestPath("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fGa34"));
|
||||||
|
try testing.expectEqual(null, parseRequestPath("75af01a79c6fea34"));
|
||||||
|
}
|
452
src/apprt/gtk/portal/OpenURI.zig
Normal file
452
src/apprt/gtk/portal/OpenURI.zig
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
//! Use DBus to call the XDG Desktop Portal to open an URI.
|
||||||
|
//! See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html#org-freedesktop-portal-openuri-openuri
|
||||||
|
const OpenURI = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const gio = @import("gio");
|
||||||
|
const glib = @import("glib");
|
||||||
|
const gobject = @import("gobject");
|
||||||
|
|
||||||
|
const App = @import("../App.zig");
|
||||||
|
const portal = @import("../portal.zig");
|
||||||
|
const apprt = @import("../../../apprt.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.openuri);
|
||||||
|
|
||||||
|
/// The GTK app that we "belong" to.
|
||||||
|
app: *App,
|
||||||
|
|
||||||
|
/// Connection to the D-Bus session bus that we'll use for all of our messaging.
|
||||||
|
dbus: *gio.DBusConnection,
|
||||||
|
|
||||||
|
/// Mutex to protect modification of the entries map or the cleanup timer.
|
||||||
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
|
/// Map to store data about any in-flight calls to the portal.
|
||||||
|
entries: std.AutoArrayHashMapUnmanaged(usize, *Entry) = .empty,
|
||||||
|
|
||||||
|
/// Used to manage a timer to clean up any orphan entries in the map.
|
||||||
|
cleanup_timer: ?c_uint = null,
|
||||||
|
|
||||||
|
/// Data about any in-flight calls to the portal.
|
||||||
|
pub const Entry = struct {
|
||||||
|
/// When the request started.
|
||||||
|
start: std.time.Instant,
|
||||||
|
/// A token used by the portal to identify requests and responses. The
|
||||||
|
/// actual format of the token does not really matter as long as it can be
|
||||||
|
/// used as part of a D-Bus object path. `usize` was chosen since it's easy
|
||||||
|
/// to hash and to generate random tokens.
|
||||||
|
token: usize,
|
||||||
|
/// The "kind" of URI. Unused here, but we may need to pass it on to the
|
||||||
|
/// fallback URL opener if the D-Bus method fails.
|
||||||
|
kind: apprt.action.OpenUrl.Kind,
|
||||||
|
/// A copy of the URI that we are opening. We need our own copy since the
|
||||||
|
/// method calls are asynchronous and the original may have been freed by
|
||||||
|
/// the time we need it.
|
||||||
|
uri: [:0]const u8,
|
||||||
|
/// Used to manage a scription to a D-Bus signal, which is how the XDG
|
||||||
|
/// Portal reports results of the method call.
|
||||||
|
subscription: ?c_uint = null,
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Entry, alloc: Allocator) void {
|
||||||
|
alloc.free(self.uri);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Errors = error{
|
||||||
|
/// Could not get a D-Bus connection
|
||||||
|
DBusConnectionRequired,
|
||||||
|
/// The D-Bus connection did not have a unique name. This _should_ be
|
||||||
|
/// impossible, but is handled for safety's sake.
|
||||||
|
NoDBusUniqueName,
|
||||||
|
/// The system was unable to give us the time.
|
||||||
|
TimerUnavailable,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
self: *OpenURI,
|
||||||
|
app: *App,
|
||||||
|
) error{DBusConnectionRequired}!void {
|
||||||
|
const gio_app = app.app.as(gio.Application);
|
||||||
|
const dbus = gio_app.getDbusConnection() orelse {
|
||||||
|
return error.DBusConnectionRequired;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.app = app,
|
||||||
|
.dbus = dbus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *OpenURI) void {
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
self.stopCleanupTimer();
|
||||||
|
|
||||||
|
for (self.entries.entries.items(.value)) |entry| {
|
||||||
|
entry.deinit(alloc);
|
||||||
|
alloc.destroy(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.entries.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the D-Bus method call to the XDG Desktop portal. The result of the
|
||||||
|
/// method call will be reported asynchronously.
|
||||||
|
pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || std.fmt.BufPrintError || Errors)!void {
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
const token = portal.generateToken();
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
// Create an entry that is used to track the results of the D-Bus method
|
||||||
|
// call.
|
||||||
|
const entry = entry: {
|
||||||
|
const entry = try alloc.create(Entry);
|
||||||
|
errdefer alloc.destroy(entry);
|
||||||
|
entry.* = .{
|
||||||
|
.start = std.time.Instant.now() catch return error.TimerUnavailable,
|
||||||
|
.token = token,
|
||||||
|
.kind = value.kind,
|
||||||
|
.uri = try alloc.dupeZ(u8, value.url),
|
||||||
|
};
|
||||||
|
errdefer entry.deinit(alloc);
|
||||||
|
try self.entries.putNoClobber(alloc, token, entry);
|
||||||
|
break :entry entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
errdefer {
|
||||||
|
_ = self.entries.swapRemove(token);
|
||||||
|
entry.deinit(alloc);
|
||||||
|
alloc.destroy(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.startCleanupTimer();
|
||||||
|
|
||||||
|
try self.subscribeToResponse(entry);
|
||||||
|
errdefer self.unsubscribeFromResponse(entry);
|
||||||
|
|
||||||
|
try self.sendRequest(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to the D-Bus signal that will contain the results of our method
|
||||||
|
/// call to the portal. This must be called with the mutex locked.
|
||||||
|
fn subscribeToResponse(self: *OpenURI, entry: *Entry) (Allocator.Error || std.fmt.BufPrintError || Errors)!void {
|
||||||
|
assert(!self.mutex.tryLock());
|
||||||
|
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
if (entry.subscription != null) return;
|
||||||
|
|
||||||
|
const request_path = try portal.getRequestPath(alloc, self.dbus, entry.token);
|
||||||
|
defer alloc.free(request_path);
|
||||||
|
|
||||||
|
entry.subscription = self.dbus.signalSubscribe(
|
||||||
|
null,
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"Response",
|
||||||
|
request_path,
|
||||||
|
null,
|
||||||
|
.{},
|
||||||
|
responseReceived,
|
||||||
|
self,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unsubscribe to the D-Bus signal that contains the result of the method call.
|
||||||
|
/// This will prevent a response from being processed multiple times. This must
|
||||||
|
/// be called when the mutex is locked.
|
||||||
|
fn unsubscribeFromResponse(self: *OpenURI, entry: *Entry) void {
|
||||||
|
assert(!self.mutex.tryLock());
|
||||||
|
|
||||||
|
// Unsubscribe from the response signal
|
||||||
|
if (entry.subscription) |subscription| {
|
||||||
|
self.dbus.signalUnsubscribe(subscription);
|
||||||
|
entry.subscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the D-Bus method call to the portal. The mutex must be locked when this
|
||||||
|
/// is called.
|
||||||
|
fn sendRequest(self: *OpenURI, entry: *Entry) Allocator.Error!void {
|
||||||
|
assert(!self.mutex.tryLock());
|
||||||
|
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
const payload = payload: {
|
||||||
|
const builder_type = glib.VariantType.new("(ssa{sv})");
|
||||||
|
defer builder_type.free();
|
||||||
|
|
||||||
|
// Initialize our builder to build up our parameters
|
||||||
|
var builder: glib.VariantBuilder = undefined;
|
||||||
|
builder.init(builder_type);
|
||||||
|
errdefer builder.clear();
|
||||||
|
|
||||||
|
// parent window - empty string means we have no window
|
||||||
|
builder.add("s", "");
|
||||||
|
|
||||||
|
// URI to open
|
||||||
|
builder.add("s", entry.uri.ptr);
|
||||||
|
|
||||||
|
// Options
|
||||||
|
{
|
||||||
|
const options = glib.VariantType.new("a{sv}");
|
||||||
|
defer glib.free(options);
|
||||||
|
|
||||||
|
builder.open(options);
|
||||||
|
defer builder.close();
|
||||||
|
|
||||||
|
{
|
||||||
|
const option = glib.VariantType.new("{sv}");
|
||||||
|
defer glib.free(option);
|
||||||
|
|
||||||
|
builder.open(option);
|
||||||
|
defer builder.close();
|
||||||
|
|
||||||
|
builder.add("s", "handle_token");
|
||||||
|
|
||||||
|
const token = try std.fmt.allocPrintZ(alloc, "{x:0<16}", .{entry.token});
|
||||||
|
defer alloc.free(token);
|
||||||
|
|
||||||
|
const handle_token = glib.Variant.newString(token.ptr);
|
||||||
|
builder.add("v", handle_token);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const option = glib.VariantType.new("{sv}");
|
||||||
|
defer glib.free(option);
|
||||||
|
|
||||||
|
builder.open(option);
|
||||||
|
defer builder.close();
|
||||||
|
|
||||||
|
builder.add("s", "writable");
|
||||||
|
|
||||||
|
const writable = glib.Variant.newBoolean(@intFromBool(false));
|
||||||
|
builder.add("v", writable);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const option = glib.VariantType.new("{sv}");
|
||||||
|
defer glib.free(option);
|
||||||
|
|
||||||
|
builder.open(option);
|
||||||
|
defer builder.close();
|
||||||
|
|
||||||
|
builder.add("s", "ask");
|
||||||
|
|
||||||
|
const ask = glib.Variant.newBoolean(@intFromBool(false));
|
||||||
|
builder.add("v", ask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :payload builder.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
// We're expecting an object path back from the method call.
|
||||||
|
const reply_type = glib.VariantType.new("(o)");
|
||||||
|
defer reply_type.free();
|
||||||
|
|
||||||
|
self.dbus.call(
|
||||||
|
"org.freedesktop.portal.Desktop",
|
||||||
|
"/org/freedesktop/portal/desktop",
|
||||||
|
"org.freedesktop.portal.OpenURI",
|
||||||
|
"OpenURI",
|
||||||
|
payload,
|
||||||
|
reply_type,
|
||||||
|
.{},
|
||||||
|
-1,
|
||||||
|
null,
|
||||||
|
requestCallback,
|
||||||
|
self,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process the result of the original method call. Receiving this result does
|
||||||
|
/// not indicate that the that the method call succeeded but it may contain an
|
||||||
|
/// error message that is useful to log for debugging purposes.
|
||||||
|
fn requestCallback(
|
||||||
|
_: ?*gobject.Object,
|
||||||
|
result: *gio.AsyncResult,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.c) void {
|
||||||
|
const self: *OpenURI = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
|
||||||
|
var err_: ?*glib.Error = null;
|
||||||
|
defer if (err_) |err| err.free();
|
||||||
|
|
||||||
|
const reply_ = self.dbus.callFinish(result, &err_);
|
||||||
|
|
||||||
|
if (err_) |err| {
|
||||||
|
log.err("Open URI request failed={s} ({})", .{
|
||||||
|
err.f_message orelse "(unknown)",
|
||||||
|
err.f_code,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reply = reply_ orelse {
|
||||||
|
log.err("D-Bus method call returned a null value!", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer reply.unref();
|
||||||
|
|
||||||
|
const reply_type = glib.VariantType.new("(o)");
|
||||||
|
defer reply_type.free();
|
||||||
|
|
||||||
|
if (reply.isOfType(reply_type) == 0) {
|
||||||
|
log.warn("Reply from D-Bus method call does not contain an object path!", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var object_path_: ?[*:0]const u8 = null;
|
||||||
|
reply.get("(o)", &object_path_);
|
||||||
|
|
||||||
|
const object_path = object_path_ orelse {
|
||||||
|
log.err("D-Bus method call did not return an object path", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = portal.parseRequestPath(std.mem.span(object_path)) orelse {
|
||||||
|
log.warn("Unable to parse token from the object path {s}", .{object_path});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check to see if the request path returned matches a token that we sent.
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
if (!self.entries.contains(token)) {
|
||||||
|
log.warn("Token {x:0<16} not found in the map!", .{token});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the response signal from the portal. This should contain the actual
|
||||||
|
/// results of the method call (success or failure).
|
||||||
|
fn responseReceived(
|
||||||
|
_: *gio.DBusConnection,
|
||||||
|
_: ?[*:0]const u8,
|
||||||
|
object_path: [*:0]const u8,
|
||||||
|
_: [*:0]const u8,
|
||||||
|
_: [*:0]const u8,
|
||||||
|
params: *glib.Variant,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.c) void {
|
||||||
|
const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
|
||||||
|
log.err("OpenURI response received with null userdata", .{});
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
const token = portal.parseRequestPath(std.mem.span(object_path)) orelse {
|
||||||
|
log.warn("invalid object path: {s}", .{std.mem.span(object_path)});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
const entry = (self.entries.fetchSwapRemove(token) orelse {
|
||||||
|
log.warn("no entry for token {x:0<16}", .{token});
|
||||||
|
return;
|
||||||
|
}).value;
|
||||||
|
|
||||||
|
defer {
|
||||||
|
entry.deinit(alloc);
|
||||||
|
alloc.destroy(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.unsubscribeFromResponse(entry);
|
||||||
|
|
||||||
|
var response: u32 = 0;
|
||||||
|
var results: ?*glib.Variant = null;
|
||||||
|
params.get("(u@a{sv})", &response, &results);
|
||||||
|
|
||||||
|
switch (response) {
|
||||||
|
0 => {
|
||||||
|
log.debug("open uri successful", .{});
|
||||||
|
},
|
||||||
|
1 => {
|
||||||
|
log.debug("open uri request was cancelled by the user", .{});
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
log.warn("open uri request ended unexpectedly", .{});
|
||||||
|
self.app.openUrlFallback(entry.kind, entry.uri);
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
log.err("unrecognized response code={}", .{response});
|
||||||
|
self.app.openUrlFallback(entry.kind, entry.uri);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait this number of seconds and then clean up any orphaned entries.
|
||||||
|
const cleanup_timeout = 30;
|
||||||
|
|
||||||
|
/// If there is an active cleanup timer, cancel it. This must be called with the
|
||||||
|
/// mutex locked
|
||||||
|
fn stopCleanupTimer(self: *OpenURI) void {
|
||||||
|
assert(!self.mutex.tryLock());
|
||||||
|
|
||||||
|
if (self.cleanup_timer) |timer| {
|
||||||
|
if (glib.Source.remove(timer) == 0) {
|
||||||
|
log.warn("unable to remove cleanup timer source={d}", .{timer});
|
||||||
|
}
|
||||||
|
self.cleanup_timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a timer to clean up any entries that have not received a timely
|
||||||
|
/// response. If there is already a timer it will be stopped and replaced with a
|
||||||
|
/// new one. This must be called with the mutex locked.
|
||||||
|
fn startCleanupTimer(self: *OpenURI) void {
|
||||||
|
assert(!self.mutex.tryLock());
|
||||||
|
|
||||||
|
self.stopCleanupTimer();
|
||||||
|
self.cleanup_timer = glib.timeoutAddSeconds(cleanup_timeout + 1, cleanup, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cleanup timer is used to free up any entries that may have failed
|
||||||
|
/// to get a response in a timely manner.
|
||||||
|
fn cleanup(ud: ?*anyopaque) callconv(.c) c_int {
|
||||||
|
const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
|
||||||
|
log.warn("cleanup called with null userdata", .{});
|
||||||
|
return @intFromBool(glib.SOURCE_REMOVE);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
self.cleanup_timer = null;
|
||||||
|
|
||||||
|
const now = std.time.Instant.now() catch {
|
||||||
|
// `now()` should never fail, but if it does, don't crash, just return.
|
||||||
|
// This might cause a small memory leak in rare circumstances but it
|
||||||
|
// should get cleaned up the next time a URL is clicked.
|
||||||
|
return @intFromBool(glib.SOURCE_REMOVE);
|
||||||
|
};
|
||||||
|
|
||||||
|
loop: while (true) {
|
||||||
|
for (self.entries.entries.items(.value)) |entry| {
|
||||||
|
if (now.since(entry.start) > cleanup_timeout * std.time.ns_per_s) {
|
||||||
|
self.unsubscribeFromResponse(entry);
|
||||||
|
_ = self.entries.swapRemove(entry.token);
|
||||||
|
entry.deinit(alloc);
|
||||||
|
alloc.destroy(entry);
|
||||||
|
continue :loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return @intFromBool(glib.SOURCE_REMOVE);
|
||||||
|
}
|
Reference in New Issue
Block a user