apprt/gtk-ng: desktop notifications, open config, open url, present terminal (#8105)
Implements a number of minor apprt actions. More or less directly ported. Some notes added for future improvements given the new architecture.pull/8115/head
commit
d4c825186e
|
|
@ -497,6 +497,8 @@ pub const Application = extern struct {
|
|||
value.config,
|
||||
),
|
||||
|
||||
.desktop_notification => Action.desktopNotification(self, target, value),
|
||||
|
||||
.goto_tab => return Action.gotoTab(target, value),
|
||||
|
||||
.mouse_over_link => Action.mouseOverLink(target, value),
|
||||
|
|
@ -515,14 +517,20 @@ pub const Application = extern struct {
|
|||
},
|
||||
),
|
||||
|
||||
.open_config => return Action.openConfig(self),
|
||||
|
||||
.open_url => Action.openUrl(self, value),
|
||||
|
||||
.pwd => Action.pwd(target, value),
|
||||
|
||||
.present_terminal => return Action.presentTerminal(target),
|
||||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.quit => self.quit(),
|
||||
|
||||
.quit_timer => try Action.quitTimer(self, value),
|
||||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.reload_config => try Action.reloadConfig(self, target, value),
|
||||
|
||||
.render => Action.render(target),
|
||||
|
|
@ -540,22 +548,20 @@ pub const Application = extern struct {
|
|||
.toggle_tab_overview => return Action.toggleTabOverview(target),
|
||||
|
||||
// Unimplemented but todo on gtk-ng branch
|
||||
.initial_size,
|
||||
.size_limit,
|
||||
.prompt_title,
|
||||
.toggle_command_palette,
|
||||
.inspector,
|
||||
// TODO: splits
|
||||
.new_split,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
.goto_split,
|
||||
.open_config,
|
||||
.inspector,
|
||||
.desktop_notification,
|
||||
.present_terminal,
|
||||
.initial_size,
|
||||
.size_limit,
|
||||
.toggle_split_zoom,
|
||||
.toggle_window_decorations,
|
||||
.prompt_title,
|
||||
// TODO: winproto
|
||||
.toggle_quick_terminal,
|
||||
.toggle_command_palette,
|
||||
.open_url,
|
||||
.toggle_window_decorations,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
|
|
@ -825,6 +831,8 @@ pub const Application = extern struct {
|
|||
const actions = .{
|
||||
.{ "new-window", actionNewWindow, null },
|
||||
.{ "new-window-command", actionNewWindow, as_variant_type },
|
||||
.{ "open-config", actionOpenConfig, null },
|
||||
.{ "present-surface", actionPresentSurface, t_variant_type },
|
||||
.{ "quit", actionQuit, null },
|
||||
.{ "reload-config", actionReloadConfig, null },
|
||||
};
|
||||
|
|
@ -1145,6 +1153,58 @@ pub const Application = extern struct {
|
|||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
pub fn actionOpenConfig(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
_ = self.core().mailbox.push(.open_config, .forever);
|
||||
}
|
||||
|
||||
fn actionPresentSurface(
|
||||
_: *gio.SimpleAction,
|
||||
parameter_: ?*glib.Variant,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const parameter = parameter_ orelse return;
|
||||
|
||||
const t = glib.ext.VariantType.newFor(u64);
|
||||
defer glib.VariantType.free(t);
|
||||
|
||||
// Make sure that we've receiived a u64 from the system.
|
||||
if (glib.Variant.isOfType(parameter, t) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert that u64 to pointer to a core surface. A value of zero
|
||||
// means that there was no target surface for the notification so
|
||||
// we don't focus any surface.
|
||||
//
|
||||
// This is admittedly SUPER SUS and we should instead do what we
|
||||
// do on macOS which is generate a UUID per surface and then pass
|
||||
// that around. But, we do validate the pointer below so at worst
|
||||
// this may result in focusing the wrong surface if the pointer was
|
||||
// reused for a surface.
|
||||
const ptr_int = parameter.getUint64();
|
||||
if (ptr_int == 0) return;
|
||||
const surface: *CoreSurface = @ptrFromInt(ptr_int);
|
||||
|
||||
// Send a message through the core app mailbox rather than presenting the
|
||||
// surface directly so that it can validate that the surface pointer is
|
||||
// valid. We could get an invalid pointer if a desktop notification outlives
|
||||
// a Ghostty instance and a new one starts up, or there are multiple Ghostty
|
||||
// instances running.
|
||||
_ = self.core().mailbox.push(
|
||||
.{
|
||||
.surface_message = .{
|
||||
.surface = surface,
|
||||
.message = .present_surface,
|
||||
},
|
||||
},
|
||||
.forever,
|
||||
);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------
|
||||
// Boilerplate/Noise
|
||||
|
||||
|
|
@ -1218,6 +1278,48 @@ const Action = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn desktopNotification(
|
||||
self: *Application,
|
||||
target: apprt.Target,
|
||||
n: apprt.action.DesktopNotification,
|
||||
) void {
|
||||
// TODO: We should move the surface target to a function call
|
||||
// on Surface and emit a signal that embedders can connect to. This
|
||||
// will let us handle notifications differently depending on where
|
||||
// a surface is presented. At the time of writing this, we always
|
||||
// want to show the notification AND the logic below was directly
|
||||
// ported from "legacy" GTK so this is fine, but I want to leave this
|
||||
// note so we can do it one day.
|
||||
|
||||
// Set a default title if we don't already have one
|
||||
const t = switch (n.title.len) {
|
||||
0 => "Ghostty",
|
||||
else => n.title,
|
||||
};
|
||||
|
||||
const notification = gio.Notification.new(t);
|
||||
defer notification.unref();
|
||||
notification.setBody(n.body);
|
||||
|
||||
const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
|
||||
defer icon.unref();
|
||||
notification.setIcon(icon.as(gio.Icon));
|
||||
|
||||
const pointer = glib.Variant.newUint64(switch (target) {
|
||||
.app => 0,
|
||||
.surface => |v| @intFromPtr(v),
|
||||
});
|
||||
notification.setDefaultActionAndTargetValue(
|
||||
"app.present-surface",
|
||||
pointer,
|
||||
);
|
||||
|
||||
// We set the notification ID to the body content. If the content is the
|
||||
// same, this notification may replace a previous notification
|
||||
const gio_app = self.as(gio.Application);
|
||||
gio_app.sendNotification(n.body, notification);
|
||||
}
|
||||
|
||||
pub fn gotoTab(
|
||||
target: apprt.Target,
|
||||
tab: apprt.action.GotoTab,
|
||||
|
|
@ -1375,6 +1477,37 @@ const Action = struct {
|
|||
gtk.Window.present(win.as(gtk.Window));
|
||||
}
|
||||
|
||||
pub fn openConfig(self: *Application) bool {
|
||||
// Get the config file path
|
||||
const alloc = self.allocator();
|
||||
const path = configpkg.edit.openPath(alloc) catch |err| {
|
||||
log.warn("error getting config file path: {}", .{err});
|
||||
return false;
|
||||
};
|
||||
defer alloc.free(path);
|
||||
|
||||
// Open it using openURL. "path" isn't actually a URL but
|
||||
// at the time of writing that works just fine for GTK.
|
||||
openUrl(self, .{ .kind = .text, .url = path });
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn openUrl(
|
||||
self: *Application,
|
||||
value: apprt.action.OpenUrl,
|
||||
) void {
|
||||
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
|
||||
|
||||
// Fallback to the minimal cross-platform way of opening a URL.
|
||||
// This is always a safe fallback and enables for example Windows
|
||||
// to open URLs (GTK on Windows via WSL is a thing).
|
||||
internal_os.open(
|
||||
self.allocator(),
|
||||
value.kind,
|
||||
value.url,
|
||||
) catch |err| log.warn("unable to open url: {}", .{err});
|
||||
}
|
||||
|
||||
pub fn pwd(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.Pwd,
|
||||
|
|
@ -1403,6 +1536,18 @@ const Action = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn presentTerminal(
|
||||
target: apprt.Target,
|
||||
) bool {
|
||||
return switch (target) {
|
||||
.app => false,
|
||||
.surface => |v| surface: {
|
||||
v.rt_surface.surface.present();
|
||||
break :surface true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn progressReport(
|
||||
target: apprt.Target,
|
||||
value: terminal.osc.Command.ProgressReport,
|
||||
|
|
|
|||
|
|
@ -285,6 +285,19 @@ pub const Surface = extern struct {
|
|||
);
|
||||
};
|
||||
|
||||
/// Emitted when the focus wants to be brought to the top and
|
||||
/// focused.
|
||||
pub const @"present-request" = struct {
|
||||
pub const name = "present-request";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when this surface requests its container to toggle its
|
||||
/// fullscreen state.
|
||||
pub const @"toggle-fullscreen" = struct {
|
||||
|
|
@ -578,6 +591,17 @@ pub const Surface = extern struct {
|
|||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
||||
|
||||
/// Request that this terminal come to the front and become focused.
|
||||
/// It is up to the embedding widget to react to this.
|
||||
pub fn present(self: *Self) void {
|
||||
signals.@"present-request".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Key press event (press or release).
|
||||
///
|
||||
/// At a high level, we want to construct an `input.KeyEvent` and
|
||||
|
|
@ -2173,6 +2197,7 @@ pub const Surface = extern struct {
|
|||
signals.bell.impl.register(.{});
|
||||
signals.@"clipboard-read".impl.register(.{});
|
||||
signals.@"clipboard-write".impl.register(.{});
|
||||
signals.@"present-request".impl.register(.{});
|
||||
signals.@"toggle-fullscreen".impl.register(.{});
|
||||
signals.@"toggle-maximize".impl.register(.{});
|
||||
|
||||
|
|
|
|||
|
|
@ -955,6 +955,13 @@ pub const Window = extern struct {
|
|||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.@"present-request".connect(
|
||||
surface,
|
||||
*Self,
|
||||
surfacePresentRequest,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = Surface.signals.@"clipboard-write".connect(
|
||||
surface,
|
||||
*Self,
|
||||
|
|
@ -1093,6 +1100,50 @@ pub const Window = extern struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn surfacePresentRequest(
|
||||
surface: *Surface,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Verify that this surface is actually in this window.
|
||||
{
|
||||
const surface_window = ext.getAncestor(
|
||||
Self,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn(
|
||||
"present request called for non-existent surface",
|
||||
.{},
|
||||
);
|
||||
return;
|
||||
};
|
||||
if (surface_window != self) {
|
||||
log.warn(
|
||||
"present request called for surface in different window",
|
||||
.{},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the tab for this surface.
|
||||
const tab = ext.getAncestor(
|
||||
Tab,
|
||||
surface.as(gtk.Widget),
|
||||
) orelse {
|
||||
log.warn("present request surface not found", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the page that contains this tab
|
||||
const priv = self.private();
|
||||
const tab_view = priv.tab_view;
|
||||
const page = tab_view.getPage(tab.as(gtk.Widget));
|
||||
tab_view.setSelectedPage(page);
|
||||
|
||||
// Grab focus
|
||||
surface.grabFocus();
|
||||
}
|
||||
|
||||
fn surfaceToggleFullscreen(
|
||||
surface: *Surface,
|
||||
self: *Self,
|
||||
|
|
|
|||
Loading…
Reference in New Issue