mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
gtk: nuke the legacy apprt from orbit
We don't really have any large outstanding regressions on -ng to warrant keeping this alive anymore. ¡Adiós!
This commit is contained in:
53
.github/workflows/test.yml
vendored
53
.github/workflows/test.yml
vendored
@@ -22,7 +22,6 @@ jobs:
|
||||
- build-macos-matrix
|
||||
- build-windows
|
||||
- test
|
||||
- test-gtk
|
||||
- test-gtk-ng
|
||||
- test-sentry-linux
|
||||
- test-macos
|
||||
@@ -492,9 +491,6 @@ jobs:
|
||||
- name: test
|
||||
run: nix develop -c zig build -Dapp-runtime=none test
|
||||
|
||||
- name: Test GTK Build
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs -Demit-webdata
|
||||
|
||||
- name: Test GTK-NG Build
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk-ng -Demit-docs -Demit-webdata
|
||||
|
||||
@@ -502,55 +498,6 @@ jobs:
|
||||
- name: Test System Build
|
||||
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
|
||||
|
||||
test-gtk:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
x11: ["true", "false"]
|
||||
wayland: ["true", "false"]
|
||||
name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
-Dapp-runtime=gtk \
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }} \
|
||||
test
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
-Dapp-runtime=gtk \
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }}
|
||||
|
||||
test-gtk-ng:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@@ -118,7 +118,6 @@
|
||||
/pkg/harfbuzz/ @ghostty-org/font
|
||||
|
||||
# GTK
|
||||
/src/apprt/gtk/ @ghostty-org/gtk
|
||||
/src/apprt/gtk-ng/ @ghostty-org/gtk
|
||||
/src/os/cgroup.zig @ghostty-org/gtk
|
||||
/src/os/flatpak.zig @ghostty-org/gtk
|
||||
|
@@ -16,7 +16,6 @@ const structs = @import("apprt/structs.zig");
|
||||
|
||||
pub const action = @import("apprt/action.zig");
|
||||
pub const ipc = @import("apprt/ipc.zig");
|
||||
pub const gtk = @import("apprt/gtk.zig");
|
||||
pub const gtk_ng = @import("apprt/gtk-ng.zig");
|
||||
pub const none = @import("apprt/none.zig");
|
||||
pub const browser = @import("apprt/browser.zig");
|
||||
@@ -43,7 +42,6 @@ pub const SurfaceSize = structs.SurfaceSize;
|
||||
pub const runtime = switch (build_config.artifact) {
|
||||
.exe => switch (build_config.app_runtime) {
|
||||
.none => none,
|
||||
.gtk => gtk,
|
||||
.@"gtk-ng" => gtk_ng,
|
||||
},
|
||||
.lib => embedded,
|
||||
@@ -64,11 +62,6 @@ pub const Runtime = enum {
|
||||
/// approach to building the application.
|
||||
@"gtk-ng",
|
||||
|
||||
/// GTK-backed. Rich windowed application. GTK is dynamically linked.
|
||||
/// WARNING: Deprecated. This will be removed very soon. All bug fixes
|
||||
/// and features should go into the gtk-ng backend.
|
||||
gtk,
|
||||
|
||||
pub fn default(target: std.Target) Runtime {
|
||||
return switch (target.os.tag) {
|
||||
// The Linux and FreeBSD default is GTK because it is a full
|
||||
|
@@ -542,7 +542,7 @@ pub const InitialSize = extern struct {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
InitialSize,
|
||||
.{ .name = "GhosttyApprtInitialSize" },
|
||||
),
|
||||
|
@@ -1,12 +0,0 @@
|
||||
//! Application runtime that uses GTK4.
|
||||
|
||||
pub const App = @import("gtk/App.zig");
|
||||
pub const Surface = @import("gtk/Surface.zig");
|
||||
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
_ = @import("gtk/inspector.zig");
|
||||
_ = @import("gtk/key.zig");
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
||||
/// Wrapper around GTK's builder APIs that perform some comptime checks.
|
||||
const Builder = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
resource_name: [:0]const u8,
|
||||
builder: ?*gtk.Builder,
|
||||
|
||||
pub fn init(
|
||||
/// The "name" of the resource.
|
||||
comptime name: []const u8,
|
||||
/// The major version of the minimum Adwaita version that is required to use
|
||||
/// this resource.
|
||||
comptime major: u16,
|
||||
/// The minor version of the minimum Adwaita version that is required to use
|
||||
/// this resource.
|
||||
comptime minor: u16,
|
||||
) Builder {
|
||||
const resource_path = comptime resource_path: {
|
||||
const gresource = @import("gresource.zig");
|
||||
// Check to make sure that our file is listed as a
|
||||
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
|
||||
// could crash at runtime when we try and load a nonexistent
|
||||
// GResource.
|
||||
for (gresource.blueprint_files) |file| {
|
||||
if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
|
||||
// Use @embedFile to make sure that the `.blp` file exists
|
||||
// at compile time. Zig _should_ discard the data so that
|
||||
// it doesn't end up in the final executable. At runtime we
|
||||
// will load the data from a GResource.
|
||||
const blp_filename = std.fmt.comptimePrint(
|
||||
"ui/{d}.{d}/{s}.blp",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
_ = @embedFile(blp_filename);
|
||||
break :resource_path std.fmt.comptimePrint(
|
||||
"/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
||||
};
|
||||
|
||||
return .{
|
||||
.resource_name = resource_path,
|
||||
.builder = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
|
||||
class.setTemplateFromResource(self.resource_name);
|
||||
}
|
||||
|
||||
pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T {
|
||||
const builder = builder: {
|
||||
if (self.builder) |builder| break :builder builder;
|
||||
const builder = gtk.Builder.newFromResource(self.resource_name);
|
||||
self.builder = builder;
|
||||
break :builder builder;
|
||||
};
|
||||
|
||||
return gobject.ext.cast(T, builder.getObject(name) orelse return null);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Builder) void {
|
||||
if (self.builder) |builder| builder.unref();
|
||||
}
|
@@ -1,212 +0,0 @@
|
||||
/// Clipboard Confirmation Window
|
||||
const ClipboardConfirmation = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const App = @import("App.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
app: *App,
|
||||
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,
|
||||
remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
|
||||
|
||||
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;
|
||||
|
||||
const alloc = app.core_app.alloc;
|
||||
const self = try alloc.create(ClipboardConfirmation);
|
||||
errdefer alloc.destroy(self);
|
||||
|
||||
try self.init(
|
||||
app,
|
||||
data,
|
||||
core_surface,
|
||||
request,
|
||||
is_secure_input,
|
||||
);
|
||||
|
||||
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,
|
||||
is_secure_input: bool,
|
||||
) !void {
|
||||
var builder: Builder = switch (DialogType) {
|
||||
adw.AlertDialog => switch (request) {
|
||||
.osc_52_read => .init("ccw-osc-52-read", 1, 5),
|
||||
.osc_52_write => .init("ccw-osc-52-write", 1, 5),
|
||||
.paste => .init("ccw-paste", 1, 5),
|
||||
},
|
||||
adw.MessageDialog => switch (request) {
|
||||
.osc_52_read => .init("ccw-osc-52-read", 1, 2),
|
||||
.osc_52_write => .init("ccw-osc-52-write", 1, 2),
|
||||
.paste => .init("ccw-paste", 1, 2),
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
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 remember_choice = if (adw_version.supportsSwitchRow())
|
||||
builder.getObject(adw.SwitchRow, "remember_choice")
|
||||
else
|
||||
null;
|
||||
|
||||
const copy = try app.core_app.alloc.dupeZ(u8, data);
|
||||
errdefer app.core_app.alloc.free(copy);
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.dialog = dialog,
|
||||
.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,
|
||||
.remember_choice = remember_choice,
|
||||
};
|
||||
|
||||
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,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
_ = DialogType.signals.response.connect(
|
||||
dialog,
|
||||
*ClipboardConfirmation,
|
||||
gtkResponse,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => {
|
||||
const parent: ?*gtk.Widget = widget: {
|
||||
const window = core_surface.rt_surface.container.window() orelse break :widget null;
|
||||
break :widget window.window.as(gtk.Widget);
|
||||
};
|
||||
dialog.as(adw.Dialog).present(parent);
|
||||
},
|
||||
adw.MessageDialog => dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
|
||||
const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
|
||||
|
||||
if (is_ok) {
|
||||
self.core_surface.completeClipboardRequest(
|
||||
self.pending_req,
|
||||
self.data,
|
||||
true,
|
||||
) catch |err| {
|
||||
log.err("Failed to requeue clipboard request: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (self.remember_choice) |remember| remember: {
|
||||
if (!adw_version.supportsSwitchRow()) break :remember;
|
||||
if (remember.getActive() == 0) break :remember;
|
||||
|
||||
switch (self.pending_req) {
|
||||
.osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny,
|
||||
.osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny,
|
||||
.paste => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.destroy();
|
||||
}
|
||||
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
|
||||
const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
|
||||
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
|
||||
const response = dialog.chooseFinish(result);
|
||||
self.handleResponse(response);
|
||||
}
|
||||
|
||||
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
self.handleResponse(response);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
const CloseDialog = @This();
|
||||
const std = @import("std");
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
const adw = @import("adw");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../os/main.zig").i18n;
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const adwaita = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.close_dialog);
|
||||
|
||||
// We don't fall back to the GTK Message/AlertDialogs since
|
||||
// we don't plan to support libadw < 1.2 as of time of writing
|
||||
// TODO: Switch to just adw.AlertDialog when we drop Debian 12 support
|
||||
const DialogType = if (adwaita.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
/// Open the dialog when the user requests to close a window/tab/split/etc.
|
||||
/// but there's still one or more running processes inside the target that
|
||||
/// cannot be closed automatically. We then ask the user whether they want
|
||||
/// to terminate existing processes.
|
||||
pub fn show(target: Target) !void {
|
||||
// If we don't have a possible window to ask the user,
|
||||
// in most situations (e.g. when a split isn't attached to a window)
|
||||
// we should just close unconditionally.
|
||||
const dialog_window = target.dialogWindow() orelse {
|
||||
target.close();
|
||||
return;
|
||||
};
|
||||
|
||||
const dialog = switch (DialogType) {
|
||||
adw.AlertDialog => adw.AlertDialog.new(target.title(), target.body()),
|
||||
adw.MessageDialog => adw.MessageDialog.new(dialog_window, target.title(), target.body()),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// AlertDialog and MessageDialog have essentially the same API,
|
||||
// so we can cheat a little here
|
||||
dialog.addResponse("cancel", i18n._("Cancel"));
|
||||
dialog.setCloseResponse("cancel");
|
||||
|
||||
dialog.addResponse("close", i18n._("Close"));
|
||||
dialog.setResponseAppearance("close", .destructive);
|
||||
|
||||
// Need a stable pointer
|
||||
const target_ptr = try target.allocator().create(Target);
|
||||
target_ptr.* = target;
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *Target, responseCallback, target_ptr, .{});
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => dialog.as(adw.Dialog).present(dialog_window.as(gtk.Widget)),
|
||||
adw.MessageDialog => dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn responseCallback(
|
||||
_: *DialogType,
|
||||
response: [*:0]const u8,
|
||||
target: *Target,
|
||||
) callconv(.c) void {
|
||||
const alloc = target.allocator();
|
||||
defer alloc.destroy(target);
|
||||
|
||||
if (std.mem.orderZ(u8, response, "close") == .eq) target.close();
|
||||
}
|
||||
|
||||
/// The target of a close dialog.
|
||||
///
|
||||
/// This is here so that we can consolidate all logic related to
|
||||
/// prompting the user and closing windows/tabs/surfaces/etc.
|
||||
/// together into one struct that is the sole source of truth.
|
||||
pub const Target = union(enum) {
|
||||
app: *App,
|
||||
window: *Window,
|
||||
tab: *Tab,
|
||||
surface: *Surface,
|
||||
|
||||
pub fn title(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("Quit Ghostty?"),
|
||||
.window => i18n._("Close Window?"),
|
||||
.tab => i18n._("Close Tab?"),
|
||||
.surface => i18n._("Close Split?"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn body(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("All terminal sessions will be terminated."),
|
||||
.window => i18n._("All terminal sessions in this window will be terminated."),
|
||||
.tab => i18n._("All terminal sessions in this tab will be terminated."),
|
||||
.surface => i18n._("The currently running process in this split will be terminated."),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dialogWindow(self: Target) ?*gtk.Window {
|
||||
return switch (self) {
|
||||
.app => {
|
||||
// Find the currently focused window. We don't store this
|
||||
// anywhere inside the App structure for some reason, so
|
||||
// we have to query every single open window and see which
|
||||
// one is active (focused and receiving keyboard input)
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
return @ptrCast(@alignCast(focused.f_data));
|
||||
},
|
||||
.window => |v| v.window.as(gtk.Window),
|
||||
.tab => |v| v.window.window.as(gtk.Window),
|
||||
.surface => |v| {
|
||||
const window_ = v.container.window() orelse return null;
|
||||
return window_.window.as(gtk.Window);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn allocator(self: Target) std.mem.Allocator {
|
||||
return switch (self) {
|
||||
.app => |v| v.core_app.alloc,
|
||||
.window => |v| v.app.core_app.alloc,
|
||||
.tab => |v| v.window.app.core_app.alloc,
|
||||
.surface => |v| v.app.core_app.alloc,
|
||||
};
|
||||
}
|
||||
|
||||
fn close(self: Target) void {
|
||||
switch (self) {
|
||||
.app => |v| v.quitNow(),
|
||||
.window => |v| v.window.as(gtk.Window).destroy(),
|
||||
.tab => |v| v.remove(),
|
||||
.surface => |v| v.container.remove(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
|
||||
|
||||
// Confusingly, `isActive` returns 1 when active,
|
||||
// but we want to return 0 to indicate equality.
|
||||
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||
return if (window.isActive() != 0) 0 else -1;
|
||||
}
|
@@ -1,258 +0,0 @@
|
||||
const CommandPalette = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
const inputpkg = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const log = std.log.scoped(.command_palette);
|
||||
|
||||
window: *Window,
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
/// The dialog object containing the palette UI.
|
||||
dialog: *adw.Dialog,
|
||||
|
||||
/// The search input text field.
|
||||
search: *gtk.SearchEntry,
|
||||
|
||||
/// The view containing each result row.
|
||||
view: *gtk.ListView,
|
||||
|
||||
/// The model that provides filtered data for the view to display.
|
||||
model: *gtk.SingleSelection,
|
||||
|
||||
/// The list that serves as the data source of the model.
|
||||
/// This is where all command data is ultimately stored.
|
||||
source: *gio.ListStore,
|
||||
|
||||
pub fn init(self: *CommandPalette, window: *Window) !void {
|
||||
// Register the custom command type *before* initializing the builder
|
||||
// If we don't do this now, the builder will complain that it doesn't know
|
||||
// about this type and fail to initialize
|
||||
_ = Command.getGObjectType();
|
||||
|
||||
var builder = Builder.init("command-palette", 1, 5);
|
||||
defer builder.deinit();
|
||||
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.arena = .init(window.app.core_app.alloc),
|
||||
.dialog = builder.getObject(adw.Dialog, "command-palette").?,
|
||||
.search = builder.getObject(gtk.SearchEntry, "search").?,
|
||||
.view = builder.getObject(gtk.ListView, "view").?,
|
||||
.model = builder.getObject(gtk.SingleSelection, "model").?,
|
||||
.source = builder.getObject(gio.ListStore, "source").?,
|
||||
};
|
||||
|
||||
// Manually take a reference here so that the dialog
|
||||
// remains in memory after closing
|
||||
self.dialog.ref();
|
||||
errdefer self.dialog.unref();
|
||||
|
||||
_ = gtk.SearchEntry.signals.stop_search.connect(
|
||||
self.search,
|
||||
*CommandPalette,
|
||||
searchStopped,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
_ = gtk.SearchEntry.signals.activate.connect(
|
||||
self.search,
|
||||
*CommandPalette,
|
||||
searchActivated,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
_ = gtk.ListView.signals.activate.connect(
|
||||
self.view,
|
||||
*CommandPalette,
|
||||
rowActivated,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
try self.updateConfig(&self.window.app.config);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CommandPalette) void {
|
||||
self.arena.deinit();
|
||||
self.dialog.unref();
|
||||
}
|
||||
|
||||
pub fn toggle(self: *CommandPalette) void {
|
||||
self.dialog.present(self.window.window.as(gtk.Widget));
|
||||
// Focus on the search bar when opening the dialog
|
||||
_ = self.search.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void {
|
||||
// Clear existing binds and clear allocated data
|
||||
self.source.removeAll();
|
||||
_ = self.arena.reset(.retain_capacity);
|
||||
|
||||
for (config.@"command-palette-entry".value.items) |command| {
|
||||
// Filter out actions that are not implemented
|
||||
// or don't make sense for GTK
|
||||
switch (command.action) {
|
||||
.close_all_windows,
|
||||
.toggle_secure_input,
|
||||
.check_for_updates,
|
||||
.redo,
|
||||
.undo,
|
||||
.reset_window_size,
|
||||
.toggle_window_float_on_top,
|
||||
=> continue,
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
const cmd = try Command.new(
|
||||
self.arena.allocator(),
|
||||
command,
|
||||
config.keybind.set,
|
||||
);
|
||||
const cmd_ref = cmd.as(gobject.Object);
|
||||
self.source.append(cmd_ref);
|
||||
cmd_ref.unref();
|
||||
}
|
||||
}
|
||||
|
||||
fn activated(self: *CommandPalette, pos: c_uint) void {
|
||||
// Use self.model and not self.source here to use the list of *visible* results
|
||||
const object = self.model.as(gio.ListModel).getObject(pos) orelse return;
|
||||
const cmd = gobject.ext.cast(Command, object) orelse return;
|
||||
|
||||
// Close before running the action in order to avoid being replaced by another
|
||||
// dialog (such as the change title dialog). If that occurs then the command
|
||||
// palette dialog won't be counted as having closed properly and cannot
|
||||
// receive focus when reopened.
|
||||
_ = self.dialog.close();
|
||||
|
||||
const action = inputpkg.Binding.Action.parse(
|
||||
std.mem.span(cmd.cmd_c.action_key),
|
||||
) catch |err| {
|
||||
log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err });
|
||||
return;
|
||||
};
|
||||
|
||||
self.window.performBindingAction(action);
|
||||
}
|
||||
|
||||
fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
|
||||
// ESC was pressed - close the palette
|
||||
_ = self.dialog.close();
|
||||
}
|
||||
|
||||
fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
|
||||
// If Enter is pressed, activate the selected entry
|
||||
self.activated(self.model.getSelected());
|
||||
}
|
||||
|
||||
fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
|
||||
self.activated(pos);
|
||||
}
|
||||
|
||||
/// Object that wraps around a command.
|
||||
///
|
||||
/// As GTK list models only accept objects that are within the GObject hierarchy,
|
||||
/// we have to construct a wrapper to be easily consumed by the list model.
|
||||
const Command = extern struct {
|
||||
parent: Parent,
|
||||
cmd_c: inputpkg.Command.C,
|
||||
|
||||
pub const getGObjectType = gobject.ext.defineClass(Command, .{
|
||||
.name = "GhosttyCommand",
|
||||
.classInit = Class.init,
|
||||
});
|
||||
|
||||
pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command {
|
||||
const self = gobject.ext.newInstance(Command, .{});
|
||||
var buf: [64]u8 = undefined;
|
||||
|
||||
const action = action: {
|
||||
const trigger = keybinds.getTrigger(cmd.action) orelse break :action null;
|
||||
const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null;
|
||||
break :action try alloc.dupeZ(u8, accel);
|
||||
};
|
||||
|
||||
self.cmd_c = .{
|
||||
.title = cmd.title.ptr,
|
||||
.description = cmd.description.ptr,
|
||||
.action = if (action) |v| v.ptr else "",
|
||||
.action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}),
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn as(self: *Command, comptime T: type) *T {
|
||||
return gobject.ext.as(T, self);
|
||||
}
|
||||
|
||||
pub const Parent = gobject.Object;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent: Parent.Class,
|
||||
|
||||
pub const Instance = Command;
|
||||
|
||||
pub fn init(class: *Class) callconv(.c) void {
|
||||
const info = @typeInfo(inputpkg.Command.C).@"struct";
|
||||
|
||||
// Expose all fields on the Command.C struct as properties
|
||||
// that can be accessed by the GObject type system
|
||||
// (and by extension, blueprints)
|
||||
const properties = comptime props: {
|
||||
var props: [info.fields.len]type = undefined;
|
||||
|
||||
for (info.fields, 0..) |field, i| {
|
||||
const accessor = struct {
|
||||
fn getter(cmd: *Command) ?[:0]const u8 {
|
||||
return std.mem.span(@field(cmd.cmd_c, field.name));
|
||||
}
|
||||
};
|
||||
|
||||
// "Canonicalize" field names into the format GObject expects
|
||||
const prop_name = prop_name: {
|
||||
var buf: [field.name.len:0]u8 = undefined;
|
||||
_ = std.mem.replace(u8, field.name, "_", "-", &buf);
|
||||
break :prop_name buf;
|
||||
};
|
||||
|
||||
props[i] = gobject.ext.defineProperty(
|
||||
&prop_name,
|
||||
Command,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.default = null,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Command,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.getter = &accessor.getter,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break :props props;
|
||||
};
|
||||
|
||||
gobject.ext.registerProperties(class, &properties);
|
||||
}
|
||||
};
|
||||
};
|
@@ -1,102 +0,0 @@
|
||||
/// Configuration errors window.
|
||||
const ConfigErrorsDialog = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
builder: Builder,
|
||||
dialog: *DialogType,
|
||||
error_message: *gtk.TextBuffer,
|
||||
|
||||
pub fn maybePresent(app: *App, window: ?*Window) void {
|
||||
if (app.config._diagnostics.empty()) return;
|
||||
|
||||
const config_errors_dialog = config_errors_dialog: {
|
||||
if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog;
|
||||
|
||||
var builder: Builder = switch (DialogType) {
|
||||
adw.AlertDialog => .init("config-errors-dialog", 1, 5),
|
||||
adw.MessageDialog => .init("config-errors-dialog", 1, 2),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
|
||||
const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
|
||||
|
||||
app.config_errors_dialog = .{
|
||||
.builder = builder,
|
||||
.dialog = dialog,
|
||||
.error_message = error_message,
|
||||
};
|
||||
|
||||
break :config_errors_dialog app.config_errors_dialog.?;
|
||||
};
|
||||
|
||||
{
|
||||
var start = std.mem.zeroes(gtk.TextIter);
|
||||
config_errors_dialog.error_message.getStartIter(&start);
|
||||
|
||||
var end = std.mem.zeroes(gtk.TextIter);
|
||||
config_errors_dialog.error_message.getEndIter(&end);
|
||||
|
||||
config_errors_dialog.error_message.delete(&start, &end);
|
||||
}
|
||||
|
||||
var msg_buf: [4095:0]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&msg_buf);
|
||||
|
||||
for (app.config._diagnostics.items()) |diag| {
|
||||
fbs.reset();
|
||||
diag.write(fbs.writer()) catch |err| {
|
||||
log.warn(
|
||||
"error writing diagnostic to buffer err={}",
|
||||
.{err},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
|
||||
config_errors_dialog.error_message.insertAtCursor("\n", 1);
|
||||
}
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => {
|
||||
const parent = if (window) |w| w.window.as(gtk.Widget) else null;
|
||||
config_errors_dialog.dialog.as(adw.Dialog).present(parent);
|
||||
},
|
||||
adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void {
|
||||
if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit();
|
||||
app.config_errors_dialog = null;
|
||||
|
||||
if (std.mem.orderZ(u8, response, "reload") == .eq) {
|
||||
app.reloadConfig(.app, .{}) catch |err| {
|
||||
log.warn("error reloading config error={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,422 +0,0 @@
|
||||
const GlobalShortcuts = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Binding = @import("../../input.zig").Binding;
|
||||
const key = @import("key.zig");
|
||||
|
||||
const log = std.log.scoped(.global_shortcuts);
|
||||
const Token = [16]u8;
|
||||
|
||||
app: *App,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
dbus: *gio.DBusConnection,
|
||||
|
||||
/// A mapping from a unique ID to an action.
|
||||
/// Currently the unique ID is simply the serialized representation of the
|
||||
/// trigger that was used for the action as triggers are unique in the keymap,
|
||||
/// but this may change in the future.
|
||||
map: std.StringArrayHashMapUnmanaged(Binding.Action) = .{},
|
||||
|
||||
/// The handle of the current global shortcuts portal session,
|
||||
/// as a D-Bus object path.
|
||||
handle: ?[:0]const u8 = null,
|
||||
|
||||
/// The D-Bus signal subscription for the response signal on requests.
|
||||
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
|
||||
response_subscription: c_uint = 0,
|
||||
|
||||
/// The D-Bus signal subscription for the keybind activate signal.
|
||||
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
|
||||
activate_subscription: c_uint = 0,
|
||||
|
||||
pub fn init(alloc: Allocator, gio_app: *gio.Application) ?GlobalShortcuts {
|
||||
const dbus = gio_app.getDbusConnection() orelse return null;
|
||||
|
||||
return .{
|
||||
// To be initialized later
|
||||
.app = undefined,
|
||||
.arena = .init(alloc),
|
||||
.dbus = dbus,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *GlobalShortcuts) void {
|
||||
self.close();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
fn close(self: *GlobalShortcuts) void {
|
||||
if (self.response_subscription != 0) {
|
||||
self.dbus.signalUnsubscribe(self.response_subscription);
|
||||
self.response_subscription = 0;
|
||||
}
|
||||
|
||||
if (self.activate_subscription != 0) {
|
||||
self.dbus.signalUnsubscribe(self.activate_subscription);
|
||||
self.activate_subscription = 0;
|
||||
}
|
||||
|
||||
if (self.handle) |handle| {
|
||||
// Close existing session
|
||||
self.dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
handle,
|
||||
"org.freedesktop.portal.Session",
|
||||
"Close",
|
||||
null,
|
||||
null,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
self.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void {
|
||||
// Ensure we have a valid reference to the app
|
||||
// (it was left uninitialized in `init`)
|
||||
self.app = app;
|
||||
|
||||
// Close any existing sessions
|
||||
self.close();
|
||||
|
||||
// Update map
|
||||
var trigger_buf: [256]u8 = undefined;
|
||||
|
||||
self.map.clearRetainingCapacity();
|
||||
var it = self.app.config.keybind.set.bindings.iterator();
|
||||
|
||||
while (it.next()) |entry| {
|
||||
const leaf = switch (entry.value_ptr.*) {
|
||||
// Global shortcuts can't have leaders
|
||||
.leader => continue,
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
if (!leaf.flags.global) continue;
|
||||
|
||||
const trigger = try key.xdgShortcutFromTrigger(
|
||||
&trigger_buf,
|
||||
entry.key_ptr.*,
|
||||
) orelse continue;
|
||||
|
||||
try self.map.put(
|
||||
self.arena.allocator(),
|
||||
try self.arena.allocator().dupeZ(u8, trigger),
|
||||
leaf.action,
|
||||
);
|
||||
}
|
||||
|
||||
if (self.map.count() > 0) {
|
||||
try self.request(.create_session);
|
||||
}
|
||||
}
|
||||
|
||||
fn shortcutActivated(
|
||||
_: *gio.DBusConnection,
|
||||
_: ?[*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
params: *glib.Variant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const self: *GlobalShortcuts = @ptrCast(@alignCast(ud));
|
||||
|
||||
// 2nd value in the tuple is the activated shortcut ID
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated
|
||||
var shortcut_id: [*:0]const u8 = undefined;
|
||||
params.getChild(1, "&s", &shortcut_id);
|
||||
log.debug("activated={s}", .{shortcut_id});
|
||||
|
||||
const action = self.map.get(std.mem.span(shortcut_id)) orelse return;
|
||||
|
||||
self.app.core_app.performAllAction(self.app, action) catch |err| {
|
||||
log.err("failed to perform action={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
const Method = enum {
|
||||
create_session,
|
||||
bind_shortcuts,
|
||||
|
||||
fn name(self: Method) [:0]const u8 {
|
||||
return switch (self) {
|
||||
.create_session => "CreateSession",
|
||||
.bind_shortcuts => "BindShortcuts",
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct the payload expected by the XDG portal call.
|
||||
fn makePayload(
|
||||
self: Method,
|
||||
shortcuts: *GlobalShortcuts,
|
||||
request_token: [:0]const u8,
|
||||
) ?*glib.Variant {
|
||||
switch (self) {
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
|
||||
.create_session => {
|
||||
var session_token: Token = undefined;
|
||||
return glib.Variant.newParsed(
|
||||
"({'handle_token': <%s>, 'session_handle_token': <%s>},)",
|
||||
request_token.ptr,
|
||||
generateToken(&session_token).ptr,
|
||||
);
|
||||
},
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
|
||||
.bind_shortcuts => {
|
||||
const handle = shortcuts.handle orelse return null;
|
||||
|
||||
const bind_type = glib.VariantType.new("a(sa{sv})");
|
||||
defer glib.free(bind_type);
|
||||
|
||||
var binds: glib.VariantBuilder = undefined;
|
||||
glib.VariantBuilder.init(&binds, bind_type);
|
||||
|
||||
var action_buf: [256]u8 = undefined;
|
||||
|
||||
var it = shortcuts.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const trigger = entry.key_ptr.*.ptr;
|
||||
const action = std.fmt.bufPrintZ(
|
||||
&action_buf,
|
||||
"{}",
|
||||
.{entry.value_ptr.*},
|
||||
) catch continue;
|
||||
|
||||
binds.addParsed(
|
||||
"(%s, {'description': <%s>, 'preferred_trigger': <%s>})",
|
||||
trigger,
|
||||
action.ptr,
|
||||
trigger,
|
||||
);
|
||||
}
|
||||
|
||||
return glib.Variant.newParsed(
|
||||
"(%o, %*, '', {'handle_token': <%s>})",
|
||||
handle.ptr,
|
||||
binds.end(),
|
||||
request_token.ptr,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn onResponse(self: Method, shortcuts: *GlobalShortcuts, vardict: *glib.Variant) void {
|
||||
switch (self) {
|
||||
.create_session => {
|
||||
var handle: ?[*:0]u8 = null;
|
||||
if (vardict.lookup("session_handle", "&s", &handle) == 0) {
|
||||
log.err(
|
||||
"session handle not found in response={s}",
|
||||
.{vardict.print(@intFromBool(true))},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
shortcuts.handle = shortcuts.arena.allocator().dupeZ(u8, std.mem.span(handle.?)) catch {
|
||||
log.err("out of memory: failed to clone session handle", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
log.debug("session_handle={?s}", .{handle});
|
||||
|
||||
// Subscribe to keybind activations
|
||||
shortcuts.activate_subscription = shortcuts.dbus.signalSubscribe(
|
||||
null,
|
||||
"org.freedesktop.portal.GlobalShortcuts",
|
||||
"Activated",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
handle,
|
||||
.{ .match_arg0_path = true },
|
||||
shortcutActivated,
|
||||
shortcuts,
|
||||
null,
|
||||
);
|
||||
|
||||
shortcuts.request(.bind_shortcuts) catch |err| {
|
||||
log.err("failed to bind shortcuts={}", .{err});
|
||||
return;
|
||||
};
|
||||
},
|
||||
.bind_shortcuts => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Submit a request to the global shortcuts portal.
|
||||
fn request(
|
||||
self: *GlobalShortcuts,
|
||||
comptime method: Method,
|
||||
) !void {
|
||||
// NOTE(pluiedev):
|
||||
// XDG Portals are really, really poorly-designed pieces of hot garbage.
|
||||
// How the protocol is _initially_ designed to work is as follows:
|
||||
//
|
||||
// 1. The client calls a method which returns the path of a Request object;
|
||||
// 2. The client waits for the Response signal under said object path;
|
||||
// 3. When the signal arrives, the actual return value and status code
|
||||
// become available for the client for further processing.
|
||||
//
|
||||
// THIS DOES NOT WORK. Once the first two steps are complete, the client
|
||||
// needs to immediately start listening for the third step, but an overeager
|
||||
// server implementation could easily send the Response signal before the
|
||||
// client is even ready, causing communications to break down over a simple
|
||||
// race condition/two generals' problem that even _TCP_ had figured out
|
||||
// decades ago. Worse yet, you get exactly _one_ chance to listen for the
|
||||
// signal, or else your communication attempt so far has all been in vain.
|
||||
//
|
||||
// And they know this. Instead of fixing their freaking protocol, they just
|
||||
// ask clients to manually construct the expected object path and subscribe
|
||||
// to the request signal beforehand, making the whole response value of
|
||||
// the original call COMPLETELY MEANINGLESS.
|
||||
//
|
||||
// Furthermore, this is _entirely undocumented_ aside from one tiny
|
||||
// paragraph under the documentation for the Request interface, and
|
||||
// anyone would be forgiven for missing it without reading the libportal
|
||||
// source code.
|
||||
//
|
||||
// When in Rome, do as the Romans do, I guess...?
|
||||
|
||||
const callbacks = struct {
|
||||
fn gotResponseHandle(
|
||||
source: ?*gobject.Object,
|
||||
res: *gio.AsyncResult,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const dbus_ = gobject.ext.cast(gio.DBusConnection, source.?).?;
|
||||
|
||||
var err: ?*glib.Error = null;
|
||||
defer if (err) |err_| err_.free();
|
||||
|
||||
const params_ = dbus_.callFinish(res, &err) orelse {
|
||||
if (err) |err_| log.err("request failed={s} ({})", .{
|
||||
err_.f_message orelse "(unknown)",
|
||||
err_.f_code,
|
||||
});
|
||||
return;
|
||||
};
|
||||
defer params_.unref();
|
||||
|
||||
// TODO: XDG recommends updating the signal subscription if the actual
|
||||
// returned request path is not the same as the expected request
|
||||
// path, to retain compatibility with older versions of XDG portals.
|
||||
// Although it suffers from the race condition outlined above,
|
||||
// we should still implement this at some point.
|
||||
}
|
||||
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response
|
||||
fn responded(
|
||||
dbus: *gio.DBusConnection,
|
||||
_: ?[*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
params_: *glib.Variant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const self_: *GlobalShortcuts = @ptrCast(@alignCast(ud));
|
||||
|
||||
// Unsubscribe from the response signal
|
||||
if (self_.response_subscription != 0) {
|
||||
dbus.signalUnsubscribe(self_.response_subscription);
|
||||
self_.response_subscription = 0;
|
||||
}
|
||||
|
||||
var response: u32 = 0;
|
||||
var vardict: ?*glib.Variant = null;
|
||||
defer if (vardict) |v| v.unref();
|
||||
params_.get("(u@a{sv})", &response, &vardict);
|
||||
|
||||
switch (response) {
|
||||
0 => {
|
||||
log.debug("request successful", .{});
|
||||
method.onResponse(self_, vardict.?);
|
||||
},
|
||||
1 => log.debug("request was cancelled by user", .{}),
|
||||
2 => log.warn("request ended unexpectedly", .{}),
|
||||
else => log.err("unrecognized response code={}", .{response}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var request_token_buf: Token = undefined;
|
||||
const request_token = generateToken(&request_token_buf);
|
||||
|
||||
const payload = method.makePayload(self, request_token) orelse return;
|
||||
const request_path = try self.getRequestPath(request_token);
|
||||
|
||||
self.response_subscription = self.dbus.signalSubscribe(
|
||||
null,
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
request_path,
|
||||
null,
|
||||
.{},
|
||||
callbacks.responded,
|
||||
self,
|
||||
null,
|
||||
);
|
||||
|
||||
self.dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.GlobalShortcuts",
|
||||
method.name(),
|
||||
payload,
|
||||
null,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
callbacks.gotResponseHandle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate a random token suitable for use in requests.
|
||||
fn generateToken(buf: *Token) [:0]const u8 {
|
||||
// u28 takes up 7 bytes in hex, 8 bytes for "ghostty_" and 1 byte for NUL
|
||||
// 7 + 8 + 1 = 16
|
||||
return std.fmt.bufPrintZ(
|
||||
buf,
|
||||
"ghostty_{x:0<7}",
|
||||
.{std.crypto.random.int(u28)},
|
||||
) catch unreachable;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn getRequestPath(self: *GlobalShortcuts, token: [:0]const u8) ![:0]const u8 {
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
|
||||
// for the syntax XDG portals expect.
|
||||
|
||||
// `getUniqueName` should never return null here as we're using an ordinary
|
||||
// message bus connection. If it doesn't, something is very wrong
|
||||
const unique_name = std.mem.span(self.dbus.getUniqueName().?);
|
||||
|
||||
const object_path = try std.mem.joinZ(self.arena.allocator(), "/", &.{
|
||||
"/org/freedesktop/portal/desktop/request",
|
||||
unique_name[1..], // Remove leading `:`
|
||||
token,
|
||||
});
|
||||
|
||||
// Sanitize the unique name by replacing every `.` with `_`.
|
||||
// In effect, this will turn a unique name like `:1.192` into `1_192`.
|
||||
// Valid D-Bus object path components never contain `.`s anyway, so we're
|
||||
// free to replace all instances of `.` here and avoid extra allocation.
|
||||
std.mem.replaceScalar(u8, object_path, '.', '_');
|
||||
|
||||
return object_path;
|
||||
}
|
@@ -1,470 +0,0 @@
|
||||
const ImguiWidget = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
const cimgui = @import("cimgui");
|
||||
const gl = @import("opengl");
|
||||
|
||||
const key = @import("key.zig");
|
||||
const input = @import("../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_imgui_widget);
|
||||
|
||||
/// This is called every frame to populate the ImGui frame.
|
||||
render_callback: ?*const fn (?*anyopaque) void = null,
|
||||
render_userdata: ?*anyopaque = null,
|
||||
|
||||
/// Our OpenGL widget
|
||||
gl_area: *gtk.GLArea,
|
||||
im_context: *gtk.IMContext,
|
||||
|
||||
/// ImGui Context
|
||||
ig_ctx: *cimgui.c.ImGuiContext,
|
||||
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
instant: ?std.time.Instant = null,
|
||||
|
||||
/// Initialize the widget. This must have a stable pointer for events.
|
||||
pub fn init(self: *ImguiWidget) !void {
|
||||
// Each widget gets its own imgui context so we can have multiple
|
||||
// imgui views in the same application.
|
||||
const ig_ctx = cimgui.c.igCreateContext(null) orelse return error.OutOfMemory;
|
||||
errdefer cimgui.c.igDestroyContext(ig_ctx);
|
||||
cimgui.c.igSetCurrentContext(ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
io.BackendPlatformName = "ghostty_gtk";
|
||||
|
||||
// Our OpenGL area for drawing
|
||||
const gl_area = gtk.GLArea.new();
|
||||
gl_area.setAutoRender(@intFromBool(true));
|
||||
|
||||
// The GL area has to be focusable so that it can receive events
|
||||
gl_area.as(gtk.Widget).setFocusable(@intFromBool(true));
|
||||
gl_area.as(gtk.Widget).setFocusOnClick(@intFromBool(true));
|
||||
|
||||
// Clicks
|
||||
const gesture_click = gtk.GestureClick.new();
|
||||
errdefer gesture_click.unref();
|
||||
gesture_click.as(gtk.GestureSingle).setButton(0);
|
||||
gl_area.as(gtk.Widget).addController(gesture_click.as(gtk.EventController));
|
||||
|
||||
// Mouse movement
|
||||
const ec_motion = gtk.EventControllerMotion.new();
|
||||
errdefer ec_motion.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
|
||||
|
||||
// Scroll events
|
||||
const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes);
|
||||
errdefer ec_scroll.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_scroll.as(gtk.EventController));
|
||||
|
||||
// Focus controller will tell us about focus enter/exit events
|
||||
const ec_focus = gtk.EventControllerFocus.new();
|
||||
errdefer ec_focus.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_focus.as(gtk.EventController));
|
||||
|
||||
// Key event controller will tell us about raw keypress events.
|
||||
const ec_key = gtk.EventControllerKey.new();
|
||||
errdefer ec_key.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_key.as(gtk.EventController));
|
||||
errdefer gl_area.as(gtk.Widget).removeController(ec_key.as(gtk.EventController));
|
||||
|
||||
// The input method context that we use to translate key events into
|
||||
// characters. This doesn't have an event key controller attached because
|
||||
// we call it manually from our own key controller.
|
||||
const im_context = gtk.IMMulticontext.new();
|
||||
errdefer im_context.unref();
|
||||
|
||||
// Signals
|
||||
_ = gtk.Widget.signals.realize.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkRealize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.Widget.signals.unrealize.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkUnrealize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.Widget.signals.destroy.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkDestroy,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GLArea.signals.render.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkRender,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GLArea.signals.resize.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkResize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerKey.signals.key_pressed.connect(
|
||||
ec_key,
|
||||
*ImguiWidget,
|
||||
gtkKeyPressed,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerKey.signals.key_released.connect(
|
||||
ec_key,
|
||||
*ImguiWidget,
|
||||
gtkKeyReleased,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerFocus.signals.enter.connect(
|
||||
ec_focus,
|
||||
*ImguiWidget,
|
||||
gtkFocusEnter,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerFocus.signals.leave.connect(
|
||||
ec_focus,
|
||||
*ImguiWidget,
|
||||
gtkFocusLeave,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GestureClick.signals.pressed.connect(
|
||||
gesture_click,
|
||||
*ImguiWidget,
|
||||
gtkMouseDown,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GestureClick.signals.released.connect(
|
||||
gesture_click,
|
||||
*ImguiWidget,
|
||||
gtkMouseUp,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerMotion.signals.motion.connect(
|
||||
ec_motion,
|
||||
*ImguiWidget,
|
||||
gtkMouseMotion,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerScroll.signals.scroll.connect(
|
||||
ec_scroll,
|
||||
*ImguiWidget,
|
||||
gtkMouseScroll,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.IMContext.signals.commit.connect(
|
||||
im_context,
|
||||
*ImguiWidget,
|
||||
gtkInputCommit,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
self.* = .{
|
||||
.gl_area = gl_area,
|
||||
.im_context = im_context.as(gtk.IMContext),
|
||||
.ig_ctx = ig_ctx,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the widget. This should ONLY be called if the widget gl_area
|
||||
/// was never added to a parent. Otherwise, cleanup automatically happens
|
||||
/// when the widget is destroyed and this should NOT be called.
|
||||
pub fn deinit(self: *ImguiWidget) void {
|
||||
cimgui.c.igDestroyContext(self.ig_ctx);
|
||||
}
|
||||
|
||||
/// This should be called anytime the underlying data for the UI changes
|
||||
/// so that the UI can be refreshed.
|
||||
pub fn queueRender(self: *const ImguiWidget) void {
|
||||
self.gl_area.queueRender();
|
||||
}
|
||||
|
||||
/// Initialize the frame. Expects that the context is already current.
|
||||
fn newFrame(self: *ImguiWidget) !void {
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
// Determine our delta time
|
||||
const now = try std.time.Instant.now();
|
||||
io.DeltaTime = if (self.instant) |prev| delta: {
|
||||
const since_ns = now.since(prev);
|
||||
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
|
||||
break :delta @max(0.00001, since_s);
|
||||
} else (1 / 60);
|
||||
self.instant = now;
|
||||
}
|
||||
|
||||
fn translateMouseButton(button: c_uint) ?c_int {
|
||||
return switch (button) {
|
||||
1 => cimgui.c.ImGuiMouseButton_Left,
|
||||
2 => cimgui.c.ImGuiMouseButton_Middle,
|
||||
3 => cimgui.c.ImGuiMouseButton_Right,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
log.debug("imgui widget destroy", .{});
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
log.debug("gl surface realized", .{});
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
area.makeCurrent();
|
||||
if (area.getError()) |err| {
|
||||
log.err("surface failed to realize: {s}", .{err.f_message orelse "(unknown)"});
|
||||
return;
|
||||
}
|
||||
|
||||
// realize means that our OpenGL context is ready, so we can now
|
||||
// initialize the ImgUI OpenGL backend for our context.
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
|
||||
}
|
||||
|
||||
fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
_ = area;
|
||||
log.debug("gl surface unrealized", .{});
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
cimgui.ImGui_ImplOpenGL3_Shutdown();
|
||||
}
|
||||
|
||||
fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor = area.as(gtk.Widget).getScaleFactor();
|
||||
log.debug("gl resize width={} height={} scale={}", .{
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
});
|
||||
|
||||
// Our display size is always unscaled. We'll do the scaling in the
|
||||
// style instead. This creates crisper looking fonts.
|
||||
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
|
||||
io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
|
||||
|
||||
// Setup a new style and scale it appropriately.
|
||||
const style = cimgui.c.ImGuiStyle_ImGuiStyle();
|
||||
defer cimgui.c.ImGuiStyle_destroy(style);
|
||||
cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
|
||||
const active_style = cimgui.c.igGetStyle();
|
||||
active_style.* = style.*;
|
||||
}
|
||||
|
||||
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
|
||||
// Setup our frame. We render twice because some ImGui behaviors
|
||||
// take multiple renders to process. I don't know how to make this
|
||||
// more efficient.
|
||||
for (0..2) |_| {
|
||||
cimgui.ImGui_ImplOpenGL3_NewFrame();
|
||||
self.newFrame() catch |err| {
|
||||
log.err("failed to setup frame: {}", .{err});
|
||||
return 0;
|
||||
};
|
||||
cimgui.c.igNewFrame();
|
||||
|
||||
// Build our UI
|
||||
if (self.render_callback) |cb| cb(self.render_userdata);
|
||||
|
||||
// Render
|
||||
cimgui.c.igRender();
|
||||
}
|
||||
|
||||
// OpenGL final render
|
||||
gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
|
||||
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
||||
cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn gtkMouseMotion(
|
||||
_: *gtk.EventControllerMotion,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
|
||||
cimgui.c.ImGuiIO_AddMousePosEvent(
|
||||
io,
|
||||
@floatCast(x * scale_factor),
|
||||
@floatCast(y * scale_factor),
|
||||
);
|
||||
self.queueRender();
|
||||
}
|
||||
|
||||
fn gtkMouseDown(
|
||||
gesture: *gtk.GestureClick,
|
||||
_: c_int,
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
|
||||
if (translateMouseButton(gdk_button)) |button| {
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseUp(
|
||||
gesture: *gtk.GestureClick,
|
||||
_: c_int,
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
|
||||
if (translateMouseButton(gdk_button)) |button| {
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseScroll(
|
||||
_: *gtk.EventControllerScroll,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) c_int {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddMouseWheelEvent(
|
||||
io,
|
||||
@floatCast(x),
|
||||
@floatCast(-y),
|
||||
);
|
||||
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
|
||||
}
|
||||
|
||||
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
|
||||
}
|
||||
|
||||
fn gtkInputCommit(
|
||||
_: *gtk.IMMulticontext,
|
||||
bytes: [*:0]u8,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
|
||||
}
|
||||
|
||||
fn gtkKeyPressed(
|
||||
ec_key: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) c_int {
|
||||
return @intFromBool(self.keyEvent(
|
||||
.press,
|
||||
ec_key,
|
||||
keyval,
|
||||
keycode,
|
||||
gtk_mods,
|
||||
));
|
||||
}
|
||||
|
||||
fn gtkKeyReleased(
|
||||
ec_key: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
_ = self.keyEvent(
|
||||
.release,
|
||||
ec_key,
|
||||
keyval,
|
||||
keycode,
|
||||
gtk_mods,
|
||||
);
|
||||
}
|
||||
|
||||
fn keyEvent(
|
||||
self: *ImguiWidget,
|
||||
action: input.Action,
|
||||
ec_key: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) bool {
|
||||
_ = keycode;
|
||||
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
const mods = key.translateMods(gtk_mods);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
|
||||
|
||||
// If our keyval has a key, then we send that key event
|
||||
if (key.keyFromKeyval(keyval)) |inputkey| {
|
||||
if (inputkey.imguiKey()) |imgui_key| {
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to process the event as text
|
||||
if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
|
||||
_ = self.im_context.filterKeypress(event);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
//! Structure for managing GUI progress bar for a surface.
|
||||
const ProgressBar = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Surface = @import("./Surface.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_progress_bar);
|
||||
|
||||
/// The surface that we belong to.
|
||||
surface: *Surface,
|
||||
|
||||
/// Widget for showing progress bar.
|
||||
progress_bar: ?*gtk.ProgressBar = null,
|
||||
|
||||
/// Timer used to remove the progress bar if we have not received an update from
|
||||
/// the TUI in a while.
|
||||
progress_bar_timer: ?c_uint = null,
|
||||
|
||||
pub fn init(surface: *Surface) ProgressBar {
|
||||
return .{
|
||||
.surface = surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ProgressBar) void {
|
||||
self.stopProgressBarTimer();
|
||||
}
|
||||
|
||||
/// Show (or update if it already exists) a GUI progress bar.
|
||||
pub fn handleProgressReport(self: *ProgressBar, value: terminal.osc.Command.ProgressReport) error{}!bool {
|
||||
// Remove the progress bar.
|
||||
if (value.state == .remove) {
|
||||
self.stopProgressBarTimer();
|
||||
self.removeProgressBar();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const progress_bar = self.addProgressBar();
|
||||
self.startProgressBarTimer();
|
||||
|
||||
switch (value.state) {
|
||||
// already handled above
|
||||
.remove => unreachable,
|
||||
|
||||
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
|
||||
// Remove the `error` CSS class so that the progress bar shows as normal.
|
||||
.set => {
|
||||
progress_bar.as(gtk.Widget).removeCssClass("error");
|
||||
if (value.progress) |progress| {
|
||||
progress_bar.setFraction(computeFraction(progress));
|
||||
} else {
|
||||
progress_bar.pulse();
|
||||
}
|
||||
},
|
||||
|
||||
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
|
||||
// Set the `error` CSS class so that the progress bar shows as an error color.
|
||||
.@"error" => {
|
||||
progress_bar.as(gtk.Widget).addCssClass("error");
|
||||
if (value.progress) |progress| {
|
||||
progress_bar.setFraction(computeFraction(progress));
|
||||
} else {
|
||||
progress_bar.pulse();
|
||||
}
|
||||
},
|
||||
|
||||
// The state of progress is unknown, so pulse the progress bar to
|
||||
// indicate that things are still happening.
|
||||
.indeterminate => {
|
||||
progress_bar.pulse();
|
||||
},
|
||||
|
||||
// If a progress value was provided, set the progress bar to that value.
|
||||
// Don't pulse the progress bar as that would indicate that things were
|
||||
// happening. Otherwise this is mainly used to keep the progress bar on
|
||||
// screen instead of timing out.
|
||||
.pause => {
|
||||
if (value.progress) |progress| {
|
||||
progress_bar.setFraction(computeFraction(progress));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Compute a fraction [0.0, 1.0] from the supplied progress, which is clamped
|
||||
/// to [0, 100].
|
||||
fn computeFraction(progress: u8) f64 {
|
||||
return @as(f64, @floatFromInt(std.math.clamp(progress, 0, 100))) / 100.0;
|
||||
}
|
||||
|
||||
test "computeFraction" {
|
||||
try std.testing.expectEqual(1.0, computeFraction(100));
|
||||
try std.testing.expectEqual(1.0, computeFraction(255));
|
||||
try std.testing.expectEqual(0.0, computeFraction(0));
|
||||
try std.testing.expectEqual(0.5, computeFraction(50));
|
||||
}
|
||||
|
||||
/// Add a progress bar to our overlay.
|
||||
fn addProgressBar(self: *ProgressBar) *gtk.ProgressBar {
|
||||
if (self.progress_bar) |progress_bar| return progress_bar;
|
||||
|
||||
const progress_bar = gtk.ProgressBar.new();
|
||||
self.progress_bar = progress_bar;
|
||||
|
||||
const progress_bar_widget = progress_bar.as(gtk.Widget);
|
||||
progress_bar_widget.setHalign(.fill);
|
||||
progress_bar_widget.setValign(.start);
|
||||
progress_bar_widget.addCssClass("osd");
|
||||
|
||||
self.surface.overlay.addOverlay(progress_bar_widget);
|
||||
|
||||
return progress_bar;
|
||||
}
|
||||
|
||||
/// Remove the progress bar from our overlay.
|
||||
fn removeProgressBar(self: *ProgressBar) void {
|
||||
if (self.progress_bar) |progress_bar| {
|
||||
const progress_bar_widget = progress_bar.as(gtk.Widget);
|
||||
self.surface.overlay.removeOverlay(progress_bar_widget);
|
||||
self.progress_bar = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a timer that will remove the progress bar if the TUI forgets to remove
|
||||
/// it.
|
||||
fn startProgressBarTimer(self: *ProgressBar) void {
|
||||
const progress_bar_timeout_seconds = 15;
|
||||
|
||||
// Remove an old timer that hasn't fired yet.
|
||||
self.stopProgressBarTimer();
|
||||
|
||||
self.progress_bar_timer = glib.timeoutAdd(
|
||||
progress_bar_timeout_seconds * std.time.ms_per_s,
|
||||
handleProgressBarTimeout,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
/// Stop any existing timer for removing the progress bar.
|
||||
fn stopProgressBarTimer(self: *ProgressBar) void {
|
||||
if (self.progress_bar_timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove progress bar timer", .{});
|
||||
}
|
||||
self.progress_bar_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// The progress bar hasn't been updated by the TUI recently, remove it.
|
||||
fn handleProgressBarTimeout(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ProgressBar = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
self.progress_bar_timer = null;
|
||||
self.removeProgressBar();
|
||||
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
@@ -1,206 +0,0 @@
|
||||
const ResizeOverlay = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// local copy of configuration data
|
||||
const DerivedConfig = struct {
|
||||
resize_overlay: configpkg.Config.ResizeOverlay,
|
||||
resize_overlay_position: configpkg.Config.ResizeOverlayPosition,
|
||||
resize_overlay_duration: configpkg.Config.Duration,
|
||||
|
||||
pub fn init(config: *const configpkg.Config) DerivedConfig {
|
||||
return .{
|
||||
.resize_overlay = config.@"resize-overlay",
|
||||
.resize_overlay_position = config.@"resize-overlay-position",
|
||||
.resize_overlay_duration = config.@"resize-overlay-duration",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// the surface that we are attached to
|
||||
surface: *Surface,
|
||||
|
||||
/// a copy of the configuration that we need to operate
|
||||
config: DerivedConfig,
|
||||
|
||||
/// If non-null this is the widget on the overlay that shows the size of the
|
||||
/// surface when it is resized.
|
||||
label: ?*gtk.Label = null,
|
||||
|
||||
/// If non-null this is a timer for dismissing the resize overlay.
|
||||
timer: ?c_uint = null,
|
||||
|
||||
/// If non-null this is a timer for dismissing the resize overlay.
|
||||
idler: ?c_uint = null,
|
||||
|
||||
/// If true, the next resize event will be the first one.
|
||||
first: bool = true,
|
||||
|
||||
/// Initialize the ResizeOverlay. This doesn't do anything more than save a
|
||||
/// pointer to the surface that we are a part of as all of the widget creation
|
||||
/// is done later.
|
||||
pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
|
||||
self.* = .{
|
||||
.surface = surface,
|
||||
.config = .init(config),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
|
||||
self.config = .init(config);
|
||||
}
|
||||
|
||||
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
|
||||
/// may not have fired yet.
|
||||
pub fn deinit(self: *ResizeOverlay) void {
|
||||
if (self.idler) |idler| {
|
||||
if (glib.Source.remove(idler) == 0) {
|
||||
log.warn("unable to remove resize overlay idler", .{});
|
||||
}
|
||||
self.idler = null;
|
||||
}
|
||||
|
||||
if (self.timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove resize overlay timer", .{});
|
||||
}
|
||||
self.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// If we're configured to do so, update the text in the resize overlay widget
|
||||
/// and make it visible. Schedule a timer to hide the widget after the delay
|
||||
/// expires.
|
||||
///
|
||||
/// If we're not configured to show the overlay, do nothing.
|
||||
pub fn maybeShow(self: *ResizeOverlay) void {
|
||||
switch (self.config.resize_overlay) {
|
||||
.never => return,
|
||||
.always => {},
|
||||
.@"after-first" => if (self.first) {
|
||||
self.first = false;
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
self.first = false;
|
||||
|
||||
// When updating a widget, wait until GTK is "idle", i.e. not in the middle
|
||||
// of doing any other updates. Since we are called in the middle of resizing
|
||||
// GTK is doing a lot of work rearranging all of the widgets. Not doing this
|
||||
// results in a lot of warnings from GTK and _horrible_ flickering of the
|
||||
// resize overlay.
|
||||
if (self.idler != null) return;
|
||||
self.idler = glib.idleAdd(gtkUpdate, self);
|
||||
}
|
||||
|
||||
/// Actually update the overlay widget. This should only be called from a GTK
|
||||
/// idle handler.
|
||||
fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||
|
||||
// No matter what our idler is complete with this callback
|
||||
self.idler = null;
|
||||
|
||||
const grid_size = self.surface.core_surface.size.grid();
|
||||
var buf: [32]u8 = undefined;
|
||||
const text = std.fmt.bufPrintZ(
|
||||
&buf,
|
||||
"{d} x {d}",
|
||||
.{
|
||||
grid_size.columns,
|
||||
grid_size.rows,
|
||||
},
|
||||
) catch |err| {
|
||||
log.err("unable to format text: {}", .{err});
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (self.label) |label| {
|
||||
// The resize overlay widget already exists, just update it.
|
||||
label.setText(text.ptr);
|
||||
setPosition(label, &self.config);
|
||||
show(label);
|
||||
} else {
|
||||
// Create the resize overlay widget.
|
||||
const label = gtk.Label.new(text.ptr);
|
||||
label.setJustify(gtk.Justification.center);
|
||||
label.setSelectable(0);
|
||||
setPosition(label, &self.config);
|
||||
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.addCssClass("view");
|
||||
widget.addCssClass("size-overlay");
|
||||
widget.setFocusable(0);
|
||||
widget.setCanTarget(0);
|
||||
|
||||
const overlay: *gtk.Overlay = @ptrCast(@alignCast(self.surface.overlay));
|
||||
overlay.addOverlay(widget);
|
||||
|
||||
self.label = label;
|
||||
}
|
||||
|
||||
if (self.timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove size overlay timer", .{});
|
||||
}
|
||||
}
|
||||
|
||||
self.timer = glib.timeoutAdd(
|
||||
self.surface.app.config.@"resize-overlay-duration".asMilliseconds(),
|
||||
gtkTimerExpired,
|
||||
self,
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// This should only be called from a GTK idle handler or timer.
|
||||
fn show(label: *gtk.Label) void {
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.removeCssClass("hidden");
|
||||
}
|
||||
|
||||
// This should only be called from a GTK idle handler or timer.
|
||||
fn hide(label: *gtk.Label) void {
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.addCssClass("hidden");
|
||||
}
|
||||
|
||||
/// Update the position of the resize overlay widget. It might seem excessive to
|
||||
/// do this often, but it should make hot config reloading of the position work.
|
||||
/// This should only be called from a GTK idle handler.
|
||||
fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.setHalign(
|
||||
switch (config.resize_overlay_position) {
|
||||
.center, .@"top-center", .@"bottom-center" => gtk.Align.center,
|
||||
.@"top-left", .@"bottom-left" => gtk.Align.start,
|
||||
.@"top-right", .@"bottom-right" => gtk.Align.end,
|
||||
},
|
||||
);
|
||||
widget.setValign(
|
||||
switch (config.resize_overlay_position) {
|
||||
.center => gtk.Align.center,
|
||||
.@"top-left", .@"top-center", .@"top-right" => gtk.Align.start,
|
||||
.@"bottom-left", .@"bottom-center", .@"bottom-right" => gtk.Align.end,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// If this fires, it means that the delay period has expired and the resize
|
||||
/// overlay widget should be hidden.
|
||||
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||
self.timer = null;
|
||||
if (self.label) |label| hide(label);
|
||||
return 0;
|
||||
}
|
@@ -1,441 +0,0 @@
|
||||
/// Split represents a surface split where two surfaces are shown side-by-side
|
||||
/// within the same window either vertically or horizontally.
|
||||
const Split = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// The split orientation.
|
||||
pub const Orientation = enum {
|
||||
horizontal,
|
||||
vertical,
|
||||
|
||||
pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
|
||||
return switch (direction) {
|
||||
.right, .left => .horizontal,
|
||||
.down, .up => .vertical,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
|
||||
return switch (direction) {
|
||||
.up, .down => .vertical,
|
||||
.left, .right => .horizontal,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Our actual GtkPaned widget
|
||||
paned: *gtk.Paned,
|
||||
|
||||
/// The container for this split panel.
|
||||
container: Surface.Container,
|
||||
|
||||
/// The orientation of this split panel.
|
||||
orientation: Orientation,
|
||||
|
||||
/// The elements of this split panel.
|
||||
top_left: Surface.Container.Elem,
|
||||
bottom_right: Surface.Container.Elem,
|
||||
|
||||
/// Create a new split panel with the given sibling surface in the given
|
||||
/// direction. The direction is where the new surface will be initialized.
|
||||
///
|
||||
/// The sibling surface can be in a split already or it can be within a
|
||||
/// tab. This properly handles updating the surface container so that
|
||||
/// it represents the new split.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
sibling: *Surface,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !*Split {
|
||||
var split = try alloc.create(Split);
|
||||
errdefer alloc.destroy(split);
|
||||
try split.init(sibling, direction);
|
||||
return split;
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
self: *Split,
|
||||
sibling: *Surface,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !void {
|
||||
// If our sibling is too small to be split in half then we don't
|
||||
// allow the split to happen. This avoids a situation where the
|
||||
// split becomes too small.
|
||||
//
|
||||
// This is kind of a hack. Ideally we'd use gtk_widget_set_size_request
|
||||
// properly along the path to ensure minimum sizes. I don't know if
|
||||
// GTK even respects that all but any way GTK does this for us seems
|
||||
// better than this.
|
||||
{
|
||||
// This is the min size of the sibling split. This means the
|
||||
// smallest split is half of this.
|
||||
const multiplier = 4;
|
||||
|
||||
const size = &sibling.core_surface.size;
|
||||
const small = switch (direction) {
|
||||
.right, .left => size.screen.width < size.cell.width * multiplier,
|
||||
.down, .up => size.screen.height < size.cell.height * multiplier,
|
||||
};
|
||||
if (small) return error.SplitTooSmall;
|
||||
}
|
||||
|
||||
// Create the new child surface for the other direction.
|
||||
const alloc = sibling.app.core_app.alloc;
|
||||
var surface = try Surface.create(alloc, sibling.app, .{
|
||||
.parent = &sibling.core_surface,
|
||||
});
|
||||
errdefer surface.destroy(alloc);
|
||||
sibling.dimSurface();
|
||||
sibling.setSplitZoom(false);
|
||||
|
||||
// Create the actual GTKPaned, attach the proper children.
|
||||
const orientation: gtk.Orientation = switch (direction) {
|
||||
.right, .left => .horizontal,
|
||||
.down, .up => .vertical,
|
||||
};
|
||||
const paned = gtk.Paned.new(orientation);
|
||||
errdefer paned.unref();
|
||||
|
||||
// Keep a long-lived reference, which we unref in destroy.
|
||||
paned.ref();
|
||||
|
||||
// Update all of our containers to point to the right place.
|
||||
// The split has to point to where the sibling pointed to because
|
||||
// we're inheriting its parent. The sibling points to its location
|
||||
// in the split, and the surface points to the other location.
|
||||
const container = sibling.container;
|
||||
const tl: *Surface, const br: *Surface = switch (direction) {
|
||||
.right, .down => right_down: {
|
||||
sibling.container = .{ .split_tl = &self.top_left };
|
||||
surface.container = .{ .split_br = &self.bottom_right };
|
||||
break :right_down .{ sibling, surface };
|
||||
},
|
||||
|
||||
.left, .up => left_up: {
|
||||
sibling.container = .{ .split_br = &self.bottom_right };
|
||||
surface.container = .{ .split_tl = &self.top_left };
|
||||
break :left_up .{ surface, sibling };
|
||||
},
|
||||
};
|
||||
|
||||
self.* = .{
|
||||
.paned = paned,
|
||||
.container = container,
|
||||
.top_left = .{ .surface = tl },
|
||||
.bottom_right = .{ .surface = br },
|
||||
.orientation = .fromDirection(direction),
|
||||
};
|
||||
|
||||
// Replace the previous containers element with our split. This allows a
|
||||
// non-split to become a split, a split to become a nested split, etc.
|
||||
container.replace(.{ .split = self });
|
||||
|
||||
// Update our children so that our GL area is properly added to the paned.
|
||||
self.updateChildren();
|
||||
|
||||
// The new surface should always grab focus
|
||||
surface.grabFocus();
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Split, alloc: Allocator) void {
|
||||
self.top_left.deinit(alloc);
|
||||
self.bottom_right.deinit(alloc);
|
||||
|
||||
// Clean up our GTK reference. This will trigger all the destroy callbacks
|
||||
// that are necessary for the surfaces to clean up.
|
||||
self.paned.unref();
|
||||
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
/// Remove the top left child.
|
||||
pub fn removeTopLeft(self: *Split) void {
|
||||
self.removeChild(self.top_left, self.bottom_right);
|
||||
}
|
||||
|
||||
/// Remove the top left child.
|
||||
pub fn removeBottomRight(self: *Split) void {
|
||||
self.removeChild(self.bottom_right, self.top_left);
|
||||
}
|
||||
|
||||
fn removeChild(
|
||||
self: *Split,
|
||||
remove: Surface.Container.Elem,
|
||||
keep: Surface.Container.Elem,
|
||||
) void {
|
||||
const window = self.container.window() orelse return;
|
||||
const alloc = window.app.core_app.alloc;
|
||||
|
||||
// Remove our children since we are going to no longer be a split anyways.
|
||||
// This prevents widgets with multiple parents.
|
||||
self.removeChildren();
|
||||
|
||||
// Our container must become whatever our top left is
|
||||
self.container.replace(keep);
|
||||
|
||||
// Grab focus of the left-over side
|
||||
keep.grabFocus();
|
||||
|
||||
// When a child is removed we are no longer a split, so destroy ourself
|
||||
remove.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
/// Move the divider in the given direction by the given amount.
|
||||
pub fn moveDivider(
|
||||
self: *Split,
|
||||
direction: apprt.action.ResizeSplit.Direction,
|
||||
amount: u16,
|
||||
) void {
|
||||
const min_pos = 10;
|
||||
|
||||
const pos = self.paned.getPosition();
|
||||
const new = switch (direction) {
|
||||
.up, .left => @max(pos - amount, min_pos),
|
||||
.down, .right => new_pos: {
|
||||
const max_pos: u16 = @as(u16, @intFromFloat(self.maxPosition())) - min_pos;
|
||||
break :new_pos @min(pos + amount, max_pos);
|
||||
},
|
||||
};
|
||||
|
||||
self.paned.setPosition(new);
|
||||
}
|
||||
|
||||
/// Equalize the splits in this split panel. Each split is equalized based on
|
||||
/// its weight, i.e. the number of Surfaces it contains.
|
||||
///
|
||||
/// It works recursively by equalizing the children of each split.
|
||||
///
|
||||
/// It returns this split's weight.
|
||||
pub fn equalize(self: *Split) f64 {
|
||||
// Calculate weights of top_left/bottom_right
|
||||
const top_left_weight = self.top_left.equalize();
|
||||
const bottom_right_weight = self.bottom_right.equalize();
|
||||
const weight = top_left_weight + bottom_right_weight;
|
||||
|
||||
// Ratio of top_left weight to overall weight, which gives the split ratio
|
||||
const ratio = top_left_weight / weight;
|
||||
|
||||
// Convert split ratio into new position for divider
|
||||
self.paned.setPosition(@intFromFloat(self.maxPosition() * ratio));
|
||||
|
||||
return weight;
|
||||
}
|
||||
|
||||
// maxPosition returns the maximum position of the GtkPaned, which is the
|
||||
// "max-position" attribute.
|
||||
fn maxPosition(self: *Split) f64 {
|
||||
var value: gobject.Value = std.mem.zeroes(gobject.Value);
|
||||
defer value.unset();
|
||||
|
||||
_ = value.init(gobject.ext.types.int);
|
||||
self.paned.as(gobject.Object).getProperty(
|
||||
"max-position",
|
||||
&value,
|
||||
);
|
||||
|
||||
return @floatFromInt(value.getInt());
|
||||
}
|
||||
|
||||
// This replaces the element at the given pointer with a new element.
|
||||
// The ptr must be either top_left or bottom_right (asserted in debug).
|
||||
// The memory of the old element must be freed or otherwise handled by
|
||||
// the caller.
|
||||
pub fn replace(
|
||||
self: *Split,
|
||||
ptr: *Surface.Container.Elem,
|
||||
new: Surface.Container.Elem,
|
||||
) void {
|
||||
// We can write our element directly. There's nothing special.
|
||||
assert(&self.top_left == ptr or &self.bottom_right == ptr);
|
||||
ptr.* = new;
|
||||
|
||||
// Update our paned children. This will reset the divider
|
||||
// position but we want to keep it in place so save and restore it.
|
||||
const pos = self.paned.getPosition();
|
||||
defer self.paned.setPosition(pos);
|
||||
self.updateChildren();
|
||||
}
|
||||
|
||||
// grabFocus grabs the focus of the top-left element.
|
||||
pub fn grabFocus(self: *Split) void {
|
||||
self.top_left.grabFocus();
|
||||
}
|
||||
|
||||
/// Update the paned children to represent the current state.
|
||||
/// This should be called anytime the top/left or bottom/right
|
||||
/// element is changed.
|
||||
pub fn updateChildren(self: *const Split) void {
|
||||
// We have to set both to null. If we overwrite the pane with
|
||||
// the same value, then GTK bugs out (the GL area unrealizes
|
||||
// and never rerealizes).
|
||||
self.removeChildren();
|
||||
|
||||
// Set our current children
|
||||
self.paned.setStartChild(self.top_left.widget());
|
||||
self.paned.setEndChild(self.bottom_right.widget());
|
||||
}
|
||||
|
||||
/// A mapping of direction to the element (if any) in that direction.
|
||||
pub const DirectionMap = std.EnumMap(
|
||||
apprt.action.GotoSplit,
|
||||
?*Surface,
|
||||
);
|
||||
|
||||
pub const Side = enum { top_left, bottom_right };
|
||||
|
||||
/// Returns the map that can be used to determine elements in various
|
||||
/// directions (primarily for gotoSplit).
|
||||
pub fn directionMap(self: *const Split, from: Side) DirectionMap {
|
||||
var result = DirectionMap.initFull(null);
|
||||
|
||||
if (self.directionPrevious(from)) |prev| {
|
||||
result.put(.previous, prev.surface);
|
||||
if (!prev.wrapped) {
|
||||
result.put(.up, prev.surface);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.directionNext(from)) |next| {
|
||||
result.put(.next, next.surface);
|
||||
if (!next.wrapped) {
|
||||
result.put(.down, next.surface);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.directionLeft(from)) |left| {
|
||||
result.put(.left, left);
|
||||
}
|
||||
|
||||
if (self.directionRight(from)) |right| {
|
||||
result.put(.right, right);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn directionLeft(self: *const Split, from: Side) ?*Surface {
|
||||
switch (from) {
|
||||
.bottom_right => {
|
||||
switch (self.orientation) {
|
||||
.horizontal => return self.top_left.deepestSurface(.bottom_right),
|
||||
.vertical => return directionLeft(
|
||||
self.container.split() orelse return null,
|
||||
.bottom_right,
|
||||
),
|
||||
}
|
||||
},
|
||||
.top_left => return directionLeft(
|
||||
self.container.split() orelse return null,
|
||||
.bottom_right,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn directionRight(self: *const Split, from: Side) ?*Surface {
|
||||
switch (from) {
|
||||
.top_left => {
|
||||
switch (self.orientation) {
|
||||
.horizontal => return self.bottom_right.deepestSurface(.top_left),
|
||||
.vertical => return directionRight(
|
||||
self.container.split() orelse return null,
|
||||
.top_left,
|
||||
),
|
||||
}
|
||||
},
|
||||
.bottom_right => return directionRight(
|
||||
self.container.split() orelse return null,
|
||||
.top_left,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn directionPrevious(self: *const Split, from: Side) ?struct {
|
||||
surface: *Surface,
|
||||
wrapped: bool,
|
||||
} {
|
||||
switch (from) {
|
||||
// From the bottom right, our previous is the deepest surface
|
||||
// in the top-left of our own split.
|
||||
.bottom_right => return .{
|
||||
.surface = self.top_left.deepestSurface(.bottom_right) orelse return null,
|
||||
.wrapped = false,
|
||||
},
|
||||
|
||||
// From the top left its more complicated. It is the de
|
||||
.top_left => {
|
||||
// If we have no parent split then there can be no unwrapped prev.
|
||||
// We can still have a wrapped previous.
|
||||
const parent = self.container.split() orelse return .{
|
||||
.surface = self.bottom_right.deepestSurface(.bottom_right) orelse return null,
|
||||
.wrapped = true,
|
||||
};
|
||||
|
||||
// The previous value is the previous of the side that we are.
|
||||
const side = self.container.splitSide() orelse return null;
|
||||
return switch (side) {
|
||||
.top_left => parent.directionPrevious(.top_left),
|
||||
.bottom_right => parent.directionPrevious(.bottom_right),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn directionNext(self: *const Split, from: Side) ?struct {
|
||||
surface: *Surface,
|
||||
wrapped: bool,
|
||||
} {
|
||||
switch (from) {
|
||||
// From the top left, our next is the earliest surface in the
|
||||
// top-left direction of the bottom-right side of our split. Fun!
|
||||
.top_left => return .{
|
||||
.surface = self.bottom_right.deepestSurface(.top_left) orelse return null,
|
||||
.wrapped = false,
|
||||
},
|
||||
|
||||
// From the bottom right is more compliated. It is the deepest
|
||||
// (last) surface in the
|
||||
.bottom_right => {
|
||||
// If we have no parent split then there can be no next.
|
||||
const parent = self.container.split() orelse return .{
|
||||
.surface = self.top_left.deepestSurface(.top_left) orelse return null,
|
||||
.wrapped = true,
|
||||
};
|
||||
|
||||
// The previous value is the previous of the side that we are.
|
||||
const side = self.container.splitSide() orelse return null;
|
||||
return switch (side) {
|
||||
.top_left => parent.directionNext(.top_left),
|
||||
.bottom_right => parent.directionNext(.bottom_right),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detachTopLeft(self: *const Split) void {
|
||||
self.paned.setStartChild(null);
|
||||
}
|
||||
|
||||
pub fn detachBottomRight(self: *const Split) void {
|
||||
self.paned.setEndChild(null);
|
||||
}
|
||||
|
||||
fn removeChildren(self: *const Split) void {
|
||||
self.detachTopLeft();
|
||||
self.detachBottomRight();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,171 +0,0 @@
|
||||
//! The state associated with a single tab in the window.
|
||||
//!
|
||||
//! A tab can contain one or more terminals due to splits.
|
||||
const Tab = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const GHOSTTY_TAB = "ghostty_tab";
|
||||
|
||||
/// The window that owns this tab.
|
||||
window: *Window,
|
||||
|
||||
/// The tab label. The tab label is the text that appears on the tab.
|
||||
label_text: *gtk.Label,
|
||||
|
||||
/// We'll put our children into this box instead of packing them
|
||||
/// directly, so that we can send the box into `c.g_signal_connect_data`
|
||||
/// for the close button
|
||||
box: *gtk.Box,
|
||||
|
||||
/// The element of this tab so that we can handle splits and so on.
|
||||
elem: Surface.Container.Elem,
|
||||
|
||||
// We'll update this every time a Surface gains focus, so that we have it
|
||||
// when we switch to another Tab. Then when we switch back to this tab, we
|
||||
// can easily re-focus that terminal.
|
||||
focus_child: ?*Surface,
|
||||
|
||||
pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab {
|
||||
var tab = try alloc.create(Tab);
|
||||
errdefer alloc.destroy(tab);
|
||||
try tab.init(window, parent_);
|
||||
return tab;
|
||||
}
|
||||
|
||||
/// Initialize the tab, create a surface, and add it to the window. "self" needs
|
||||
/// to be a stable pointer, since it is used for GTK events.
|
||||
pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.label_text = undefined,
|
||||
.box = undefined,
|
||||
.elem = undefined,
|
||||
.focus_child = null,
|
||||
};
|
||||
|
||||
// Create a Box in which we'll later keep either Surface or Split. Using a
|
||||
// box makes it easier to maintain the tab contents because we never need to
|
||||
// change the root widget of the notebook page (tab).
|
||||
const box = gtk.Box.new(.vertical, 0);
|
||||
errdefer box.unref();
|
||||
const box_widget = box.as(gtk.Widget);
|
||||
box_widget.setHexpand(1);
|
||||
box_widget.setVexpand(1);
|
||||
self.box = box;
|
||||
|
||||
// Create the initial surface since all tabs start as a single non-split
|
||||
var surface = try Surface.create(window.app.core_app.alloc, window.app, .{
|
||||
.parent = parent_,
|
||||
});
|
||||
errdefer surface.unref();
|
||||
surface.container = .{ .tab_ = self };
|
||||
self.elem = .{ .surface = surface };
|
||||
|
||||
// Add Surface to the Tab
|
||||
self.box.append(surface.primaryWidget());
|
||||
|
||||
// Set the userdata of the box to point to this tab.
|
||||
self.box.as(gobject.Object).setData(GHOSTTY_TAB, self);
|
||||
window.notebook.addTab(self, "Ghostty");
|
||||
|
||||
// Attach all events
|
||||
_ = gtk.Widget.signals.destroy.connect(
|
||||
self.box,
|
||||
*Tab,
|
||||
gtkDestroy,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// We need to grab focus after Surface and Tab is added to the window. When
|
||||
// creating a Tab we want to always focus on the widget.
|
||||
surface.grabFocus();
|
||||
}
|
||||
|
||||
/// Deinits tab by deiniting child elem.
|
||||
pub fn deinit(self: *Tab, alloc: Allocator) void {
|
||||
self.elem.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Deinit and deallocate the tab.
|
||||
pub fn destroy(self: *Tab, alloc: Allocator) void {
|
||||
self.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
// TODO: move this
|
||||
/// Replace the surface element that this tab is showing.
|
||||
pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
|
||||
// Remove our previous widget
|
||||
self.box.remove(self.elem.widget());
|
||||
|
||||
// Add our new one
|
||||
self.box.append(elem.widget());
|
||||
self.elem = elem;
|
||||
}
|
||||
|
||||
pub fn setTitleText(self: *Tab, title: [:0]const u8) void {
|
||||
self.window.notebook.setTabTitle(self, title);
|
||||
}
|
||||
|
||||
pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
|
||||
self.window.notebook.setTabTooltip(self, tooltip);
|
||||
}
|
||||
|
||||
/// Remove this tab from the window.
|
||||
pub fn remove(self: *Tab) void {
|
||||
self.window.closeTab(self);
|
||||
}
|
||||
|
||||
/// Helper function to check if any surface in the split hierarchy needs close confirmation
|
||||
fn needsConfirm(elem: Surface.Container.Elem) bool {
|
||||
return switch (elem) {
|
||||
.surface => |s| s.core_surface.needsConfirmQuit(),
|
||||
.split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
|
||||
};
|
||||
}
|
||||
|
||||
/// Close the tab, asking for confirmation if any surface requests it.
|
||||
pub fn closeWithConfirmation(tab: *Tab) void {
|
||||
switch (tab.elem) {
|
||||
.surface => |s| s.closeWithConfirmation(
|
||||
s.core_surface.needsConfirmQuit(),
|
||||
.{ .tab = tab },
|
||||
),
|
||||
.split => |s| {
|
||||
if (!needsConfirm(s.top_left) and !needsConfirm(s.bottom_right)) {
|
||||
tab.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
CloseDialog.show(.{ .tab = tab }) catch |err| {
|
||||
log.err("failed to open close dialog={}", .{err});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void {
|
||||
log.debug("tab box destroy", .{});
|
||||
|
||||
const alloc = self.window.app.core_app.alloc;
|
||||
|
||||
// When our box is destroyed, we want to destroy our tab, too.
|
||||
self.destroy(alloc);
|
||||
}
|
@@ -1,284 +0,0 @@
|
||||
/// An abstraction over the Adwaita tab view to manage all the terminal tabs in
|
||||
/// a window.
|
||||
const TabView = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const glib = @import("glib");
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// our window
|
||||
window: *Window,
|
||||
|
||||
/// the tab view
|
||||
tab_view: *adw.TabView,
|
||||
|
||||
/// Set to true so that the adw close-page handler knows we're forcing
|
||||
/// and to allow a close to happen with no confirm. This is a bit of a hack
|
||||
/// because we currently use GTK alerts to confirm tab close and they
|
||||
/// don't carry with them the ADW state that we are confirming or not.
|
||||
/// Long term we should move to ADW alerts so we can know if we are
|
||||
/// confirming or not.
|
||||
forcing_close: bool = false,
|
||||
|
||||
pub fn init(self: *TabView, window: *Window) void {
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.tab_view = adw.TabView.new(),
|
||||
};
|
||||
self.tab_view.as(gtk.Widget).addCssClass("notebook");
|
||||
|
||||
if (adw_version.atLeast(1, 2, 0)) {
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
self.tab_view.removeShortcuts(.{
|
||||
.alt_digits = true,
|
||||
.alt_zero = true,
|
||||
.control_end = true,
|
||||
.control_home = true,
|
||||
.control_page_down = true,
|
||||
.control_page_up = true,
|
||||
.control_shift_end = true,
|
||||
.control_shift_home = true,
|
||||
.control_shift_page_down = true,
|
||||
.control_shift_page_up = true,
|
||||
.control_shift_tab = true,
|
||||
.control_tab = true,
|
||||
});
|
||||
}
|
||||
|
||||
_ = adw.TabView.signals.page_attached.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwPageAttached,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = adw.TabView.signals.close_page.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwClosePage,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = adw.TabView.signals.create_window.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwTabViewCreateWindow,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwSelectPage,
|
||||
self,
|
||||
.{
|
||||
.detail = "selected-page",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn asWidget(self: *TabView) *gtk.Widget {
|
||||
return self.tab_view.as(gtk.Widget);
|
||||
}
|
||||
|
||||
pub fn nPages(self: *TabView) c_int {
|
||||
return self.tab_view.getNPages();
|
||||
}
|
||||
|
||||
/// Returns the index of the currently selected page.
|
||||
/// Returns null if the notebook has no pages.
|
||||
fn currentPage(self: *TabView) ?c_int {
|
||||
const page = self.tab_view.getSelectedPage() orelse return null;
|
||||
return self.tab_view.getPagePosition(page);
|
||||
}
|
||||
|
||||
/// Returns the currently selected tab or null if there are none.
|
||||
pub fn currentTab(self: *TabView) ?*Tab {
|
||||
const page = self.tab_view.getSelectedPage() orelse return null;
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
return @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return null));
|
||||
}
|
||||
|
||||
pub fn gotoNthTab(self: *TabView, position: c_int) bool {
|
||||
const page_to_select = self.tab_view.getNthPage(position);
|
||||
self.tab_view.setSelectedPage(page_to_select);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
|
||||
return self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
}
|
||||
|
||||
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
|
||||
return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
|
||||
}
|
||||
|
||||
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
|
||||
const page_idx = self.getTabPosition(tab) orelse return false;
|
||||
|
||||
// The next index is the previous or we wrap around.
|
||||
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
|
||||
const max = self.nPages();
|
||||
break :next_idx max -| 1;
|
||||
};
|
||||
|
||||
// Do nothing if we have one tab
|
||||
if (next_idx == page_idx) return false;
|
||||
|
||||
return self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn gotoNextTab(self: *TabView, tab: *Tab) bool {
|
||||
const page_idx = self.getTabPosition(tab) orelse return false;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||
if (next_idx == page_idx) return false;
|
||||
|
||||
return self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
|
||||
const page_idx = self.getTabPosition(tab) orelse return;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
var new_position: c_int = page_idx + position;
|
||||
|
||||
if (new_position < 0) {
|
||||
new_position = max + new_position + 1;
|
||||
} else if (new_position > max) {
|
||||
new_position = new_position - max - 1;
|
||||
}
|
||||
|
||||
if (new_position == page_idx) return;
|
||||
self.reorderPage(tab, new_position);
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
||||
_ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
|
||||
}
|
||||
|
||||
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||
const page = self.getTabPage(tab) orelse return;
|
||||
page.setTitle(title.ptr);
|
||||
}
|
||||
|
||||
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
const page = self.getTabPage(tab) orelse return;
|
||||
page.setTooltip(tooltip.ptr);
|
||||
}
|
||||
|
||||
fn newTabInsertPosition(self: *TabView, tab: *Tab) c_int {
|
||||
const numPages = self.nPages();
|
||||
return switch (tab.window.app.config.@"window-new-tab-position") {
|
||||
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
||||
.end => numPages,
|
||||
};
|
||||
}
|
||||
|
||||
/// Adds a new tab with the given title to the notebook.
|
||||
pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||
const position = self.newTabInsertPosition(tab);
|
||||
const page = self.tab_view.insert(tab.box.as(gtk.Widget), position);
|
||||
self.setTabTitle(tab, title);
|
||||
self.tab_view.setSelectedPage(page);
|
||||
}
|
||||
|
||||
pub fn closeTab(self: *TabView, tab: *Tab) void {
|
||||
// closeTab always expects to close unconditionally so we mark this
|
||||
// as true so that the close_page call below doesn't request
|
||||
// confirmation.
|
||||
self.forcing_close = true;
|
||||
const n = self.nPages();
|
||||
defer {
|
||||
// self becomes invalid if we close the last page because we close
|
||||
// the whole window
|
||||
if (n > 1) self.forcing_close = false;
|
||||
}
|
||||
|
||||
if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
// libadw versions < 1.5.1 leak the final page view
|
||||
// which causes our surface to not properly cleanup. We
|
||||
// unref to force the cleanup. This will trigger a critical
|
||||
// warning from GTK, but I don't know any other workaround.
|
||||
if (!adw_version.atLeast(1, 5, 1)) {
|
||||
tab.box.unref();
|
||||
}
|
||||
|
||||
self.window.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn createWindow(window: *Window) !*Window {
|
||||
const new_window = try Window.create(window.app.core_app.alloc, window.app);
|
||||
new_window.present();
|
||||
return new_window;
|
||||
}
|
||||
|
||||
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void {
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
|
||||
tab.window = self.window;
|
||||
|
||||
self.window.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn adwClosePage(
|
||||
_: *adw.TabView,
|
||||
page: *adw.TabPage,
|
||||
self: *TabView,
|
||||
) callconv(.c) c_int {
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
|
||||
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
|
||||
if (!self.forcing_close) {
|
||||
// We cannot trigger a close directly in here as the page will stay
|
||||
// alive until this handler returns, breaking the assumption where
|
||||
// no pages means they are all destroyed.
|
||||
//
|
||||
// Schedule the close request to happen in the next event cycle.
|
||||
_ = glib.idleAddOnce(glibIdleOnceCloseTab, tab);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *adw.TabView,
|
||||
self: *TabView,
|
||||
) callconv(.c) ?*adw.TabView {
|
||||
const window = createWindow(self.window) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
return window.notebook.tab_view;
|
||||
}
|
||||
|
||||
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void {
|
||||
const page = self.tab_view.getSelectedPage() orelse return;
|
||||
|
||||
// If the tab was previously marked as needing attention
|
||||
// (e.g. due to a bell character), we now unmark that
|
||||
page.setNeedsAttention(@intFromBool(false));
|
||||
|
||||
const title = page.getTitle();
|
||||
self.window.setTitle(std.mem.span(title));
|
||||
}
|
||||
|
||||
fn glibIdleOnceCloseTab(data: ?*anyopaque) callconv(.c) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(data orelse return));
|
||||
tab.closeWithConfirmation();
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
//! Represents the URL hover widgets that show the hovered URL.
|
||||
//!
|
||||
//! To explain a bit how this all works since its split across a few places:
|
||||
//! We create a left/right pair of labels. The left label is shown by default,
|
||||
//! and the right label is hidden. When the mouse enters the left label, we
|
||||
//! show the right label. When the mouse leaves the left label, we hide the
|
||||
//! right label.
|
||||
//!
|
||||
//! The hover and styling is done with a combination of GTK event controllers
|
||||
//! and CSS in style.css.
|
||||
const URLWidget = @This();
|
||||
|
||||
const gtk = @import("gtk");
|
||||
|
||||
/// The label that appears on the bottom left.
|
||||
left: *gtk.Label,
|
||||
|
||||
/// The label that appears on the bottom right.
|
||||
right: *gtk.Label,
|
||||
|
||||
pub fn init(
|
||||
/// The overlay that we will attach our labels to.
|
||||
overlay: *gtk.Overlay,
|
||||
/// The URL to display.
|
||||
str: [:0]const u8,
|
||||
) URLWidget {
|
||||
// Create the left
|
||||
const left = left: {
|
||||
const left = gtk.Label.new(str.ptr);
|
||||
left.setEllipsize(.middle);
|
||||
const widget = left.as(gtk.Widget);
|
||||
widget.addCssClass("view");
|
||||
widget.addCssClass("url-overlay");
|
||||
widget.addCssClass("left");
|
||||
widget.setHalign(.start);
|
||||
widget.setValign(.end);
|
||||
break :left left;
|
||||
};
|
||||
|
||||
// Create the right
|
||||
const right = right: {
|
||||
const right = gtk.Label.new(str.ptr);
|
||||
right.setEllipsize(.middle);
|
||||
const widget = right.as(gtk.Widget);
|
||||
widget.addCssClass("hidden");
|
||||
widget.addCssClass("view");
|
||||
widget.addCssClass("url-overlay");
|
||||
widget.addCssClass("right");
|
||||
widget.setHalign(.end);
|
||||
widget.setValign(.end);
|
||||
break :right right;
|
||||
};
|
||||
|
||||
// Setup our mouse hover event controller for the left label.
|
||||
const ec_motion = gtk.EventControllerMotion.new();
|
||||
errdefer ec_motion.unref();
|
||||
|
||||
left.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
|
||||
|
||||
_ = gtk.EventControllerMotion.signals.enter.connect(
|
||||
ec_motion,
|
||||
*gtk.Label,
|
||||
gtkLeftEnter,
|
||||
right,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerMotion.signals.leave.connect(
|
||||
ec_motion,
|
||||
*gtk.Label,
|
||||
gtkLeftLeave,
|
||||
right,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Show it
|
||||
overlay.addOverlay(left.as(gtk.Widget));
|
||||
overlay.addOverlay(right.as(gtk.Widget));
|
||||
|
||||
return .{
|
||||
.left = left,
|
||||
.right = right,
|
||||
};
|
||||
}
|
||||
|
||||
/// Remove our labels from the overlay.
|
||||
pub fn deinit(self: *URLWidget, overlay: *gtk.Overlay) void {
|
||||
overlay.removeOverlay(self.left.as(gtk.Widget));
|
||||
overlay.removeOverlay(self.right.as(gtk.Widget));
|
||||
}
|
||||
|
||||
/// Change the URL that is displayed.
|
||||
pub fn setText(self: *const URLWidget, str: [:0]const u8) void {
|
||||
self.left.setText(str.ptr);
|
||||
self.right.setText(str.ptr);
|
||||
}
|
||||
|
||||
/// Callback for when the mouse enters the left label. That means that we should
|
||||
/// show the right label. CSS will handle hiding the left label.
|
||||
fn gtkLeftEnter(
|
||||
_: *gtk.EventControllerMotion,
|
||||
_: f64,
|
||||
_: f64,
|
||||
right: *gtk.Label,
|
||||
) callconv(.c) void {
|
||||
right.as(gtk.Widget).removeCssClass("hidden");
|
||||
}
|
||||
|
||||
/// Callback for when the mouse leaves the left label. That means that we should
|
||||
/// hide the right label. CSS will handle showing the left label.
|
||||
fn gtkLeftLeave(
|
||||
_: *gtk.EventControllerMotion,
|
||||
right: *gtk.Label,
|
||||
) callconv(.c) void {
|
||||
right.as(gtk.Widget).addCssClass("hidden");
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,122 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Until the gobject bindings are built at the same time we are building
|
||||
// Ghostty, we need to import `adwaita.h` directly to ensure that the version
|
||||
// macros match the version of `libadwaita` that we are building/linking
|
||||
// against.
|
||||
const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
const adw = @import("adw");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const comptime_version: std.SemanticVersion = .{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
.patch = c.ADW_MICRO_VERSION,
|
||||
};
|
||||
|
||||
pub fn getRuntimeVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = adw.getMajorVersion(),
|
||||
.minor = adw.getMinorVersion(),
|
||||
.patch = adw.getMicroVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logVersion() void {
|
||||
log.info("libadwaita version build={} runtime={}", .{
|
||||
comptime_version,
|
||||
getRuntimeVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies that the running libadwaita version is at least the given
|
||||
/// version. This will return false if Ghostty is configured to not build with
|
||||
/// libadwaita.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it is run in a
|
||||
/// comptime context, it will only check the version in the headers. If it is
|
||||
/// run in a runtime context, it will check the actual version of the library we
|
||||
/// are linked against. So generally you probably want to do both checks!
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the runtime checks
|
||||
/// if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version, we can return
|
||||
// false immediately. This prevents us from compiling against unknown
|
||||
// symbols and makes runtime checks very slightly faster.
|
||||
if (comptime comptime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
return runtimeAtLeast(major, minor, micro);
|
||||
}
|
||||
|
||||
/// Verifies that the libadwaita version at runtime is at least the given version.
|
||||
///
|
||||
/// This function should be used in cases where the only the runtime behavior
|
||||
/// is affected by the version check. For checks which would affect code
|
||||
/// generation, use `atLeast`.
|
||||
pub inline fn runtimeAtLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
|
||||
// because the function gets the actual runtime version.
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
test "versionAtLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
|
||||
pub inline fn supportsDialogs() bool {
|
||||
return atLeast(1, 5, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsTabOverview() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsSwitchRow() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsToolbarView() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsBanner() bool {
|
||||
return atLeast(1, 3, 0);
|
||||
}
|
@@ -1,160 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
const adwaita_version = std.SemanticVersion{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
.patch = c.ADW_MICRO_VERSION,
|
||||
};
|
||||
const required_blueprint_version = std.SemanticVersion{
|
||||
.major = 0,
|
||||
.minor = 16,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer _ = debug_allocator.deinit();
|
||||
const alloc = debug_allocator.allocator();
|
||||
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
_ = it.next();
|
||||
|
||||
const required_adwaita_version = std.SemanticVersion{
|
||||
.major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10),
|
||||
.minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10),
|
||||
.patch = 0,
|
||||
};
|
||||
const output = it.next() orelse return error.NoOutput;
|
||||
const input = it.next() orelse return error.NoInput;
|
||||
|
||||
if (adwaita_version.order(required_adwaita_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`libadwaita` is too old.
|
||||
\\
|
||||
\\Ghostty requires a version {} or newer of `libadwaita` to
|
||||
\\compile this blueprint. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
, .{required_adwaita_version});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
|
||||
{
|
||||
var stdout: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stdout.deinit(alloc);
|
||||
var stderr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stderr.deinit(alloc);
|
||||
|
||||
var blueprint_compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"--version",
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
blueprint_compiler.stdout_behavior = .Pipe;
|
||||
blueprint_compiler.stderr_behavior = .Pipe;
|
||||
try blueprint_compiler.spawn();
|
||||
try blueprint_compiler.collectOutput(
|
||||
alloc,
|
||||
&stdout,
|
||||
&stderr,
|
||||
std.math.maxInt(u16),
|
||||
);
|
||||
const term = blueprint_compiler.wait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) std.process.exit(1);
|
||||
},
|
||||
else => std.process.exit(1),
|
||||
}
|
||||
|
||||
const version = try std.SemanticVersion.parse(std.mem.trim(u8, stdout.items, &std.ascii.whitespace));
|
||||
if (version.order(required_blueprint_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` is the wrong version.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var stdout: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stdout.deinit(alloc);
|
||||
var stderr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stderr.deinit(alloc);
|
||||
|
||||
var blueprint_compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"compile",
|
||||
"--output",
|
||||
output,
|
||||
input,
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
blueprint_compiler.stdout_behavior = .Pipe;
|
||||
blueprint_compiler.stderr_behavior = .Pipe;
|
||||
try blueprint_compiler.spawn();
|
||||
try blueprint_compiler.collectOutput(
|
||||
alloc,
|
||||
&stdout,
|
||||
&stderr,
|
||||
std.math.maxInt(u16),
|
||||
);
|
||||
const term = blueprint_compiler.wait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) {
|
||||
std.debug.print("{s}", .{stderr.items});
|
||||
std.process.exit(1);
|
||||
}
|
||||
},
|
||||
else => {
|
||||
std.debug.print("{s}", .{stderr.items});
|
||||
std.process.exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,205 +0,0 @@
|
||||
/// Contains all the logic for putting the Ghostty process and
|
||||
/// each individual surface into its own cgroup.
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_systemd_cgroup);
|
||||
|
||||
/// Initialize the cgroup for the app. This will create our
|
||||
/// transient scope, initialize the cgroups we use for the app,
|
||||
/// configure them, and return the cgroup path for the app.
|
||||
pub fn init(app: *App) ![]const u8 {
|
||||
const pid = std.os.linux.getpid();
|
||||
const alloc = app.core_app.alloc;
|
||||
|
||||
// Get our initial cgroup. We need this so we can compare
|
||||
// and detect when we've switched to our transient group.
|
||||
const original = try internal_os.cgroup.current(
|
||||
alloc,
|
||||
pid,
|
||||
) orelse "";
|
||||
defer alloc.free(original);
|
||||
|
||||
// Create our transient scope. If this succeeds then the unit
|
||||
// was created, but we may not have moved into it yet, so we need
|
||||
// to do a dumb busy loop to wait for the move to complete.
|
||||
try createScope(app, pid);
|
||||
const transient = transient: while (true) {
|
||||
const current = try internal_os.cgroup.current(
|
||||
alloc,
|
||||
pid,
|
||||
) orelse "";
|
||||
if (!std.mem.eql(u8, original, current)) break :transient current;
|
||||
alloc.free(current);
|
||||
std.time.sleep(25 * std.time.ns_per_ms);
|
||||
};
|
||||
errdefer alloc.free(transient);
|
||||
log.info("transient scope created cgroup={s}", .{transient});
|
||||
|
||||
// Create the app cgroup and put ourselves in it. This is
|
||||
// required because controllers can't be configured while a
|
||||
// process is in a cgroup.
|
||||
try internal_os.cgroup.create(transient, "app", pid);
|
||||
|
||||
// Create a cgroup that will contain all our surfaces. We will
|
||||
// enable the controllers and configure resource limits for surfaces
|
||||
// only on this cgroup so that it doesn't affect our main app.
|
||||
try internal_os.cgroup.create(transient, "surfaces", null);
|
||||
const surfaces = try std.fmt.allocPrint(alloc, "{s}/surfaces", .{transient});
|
||||
defer alloc.free(surfaces);
|
||||
|
||||
// Enable all of our cgroup controllers. If these fail then
|
||||
// we just log. We can't reasonably undo what we've done above
|
||||
// so we log the warning and still return the transient group.
|
||||
// I don't know a scenario where this fails yet.
|
||||
try enableControllers(alloc, transient);
|
||||
try enableControllers(alloc, surfaces);
|
||||
|
||||
// Configure the "high" memory limit. This limit is used instead
|
||||
// of "max" because it's a soft limit that can be exceeded and
|
||||
// can be monitored by things like systemd-oomd to kill if needed,
|
||||
// versus an instant hard kill.
|
||||
if (app.config.@"linux-cgroup-memory-limit") |limit| {
|
||||
try internal_os.cgroup.configureLimit(surfaces, .{
|
||||
.memory_high = limit,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure the "max" pids limit. This is a hard limit and cannot be
|
||||
// exceeded.
|
||||
if (app.config.@"linux-cgroup-processes-limit") |limit| {
|
||||
try internal_os.cgroup.configureLimit(surfaces, .{
|
||||
.pids_max = limit,
|
||||
});
|
||||
}
|
||||
|
||||
return transient;
|
||||
}
|
||||
|
||||
/// Enable all the cgroup controllers for the given cgroup.
|
||||
fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
|
||||
const raw = try internal_os.cgroup.controllers(alloc, cgroup);
|
||||
defer alloc.free(raw);
|
||||
|
||||
// Build our string builder for enabling all controllers
|
||||
var builder = std.ArrayList(u8).init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
// Controllers are space-separated
|
||||
var it = std.mem.splitScalar(u8, raw, ' ');
|
||||
while (it.next()) |controller| {
|
||||
try builder.append('+');
|
||||
try builder.appendSlice(controller);
|
||||
if (it.rest().len > 0) try builder.append(' ');
|
||||
}
|
||||
|
||||
// Enable them all
|
||||
try internal_os.cgroup.configureControllers(
|
||||
cgroup,
|
||||
builder.items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a transient systemd scope unit for the current process.
|
||||
///
|
||||
/// On success this will return the name of the transient scope
|
||||
/// cgroup prefix, allocated with the given allocator.
|
||||
fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
|
||||
const gio_app = app.app.as(gio.Application);
|
||||
const connection = gio_app.getDbusConnection() orelse
|
||||
return error.DbusConnectionRequired;
|
||||
|
||||
const pid: u32 = @intCast(pid_);
|
||||
|
||||
// The unit name needs to be unique. We use the pid for this.
|
||||
var name_buf: [256]u8 = undefined;
|
||||
const name = std.fmt.bufPrintZ(
|
||||
&name_buf,
|
||||
"app-ghostty-transient-{}.scope",
|
||||
.{pid},
|
||||
) catch unreachable;
|
||||
|
||||
const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))");
|
||||
defer glib.free(builder_type);
|
||||
|
||||
// Initialize our builder to build up our parameters
|
||||
var builder: glib.VariantBuilder = undefined;
|
||||
builder.init(builder_type);
|
||||
|
||||
builder.add("s", name.ptr);
|
||||
builder.add("s", "fail");
|
||||
|
||||
{
|
||||
// Properties
|
||||
const properties_type = glib.VariantType.new("a(sv)");
|
||||
defer glib.free(properties_type);
|
||||
|
||||
builder.open(properties_type);
|
||||
defer builder.close();
|
||||
|
||||
// https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
|
||||
const pressure_value = glib.Variant.newString("kill");
|
||||
|
||||
builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value);
|
||||
|
||||
// Delegate
|
||||
const delegate_value = glib.Variant.newBoolean(1);
|
||||
builder.add("(sv)", "Delegate", delegate_value);
|
||||
|
||||
// Pid to move into the unit
|
||||
const pids_value_type = glib.VariantType.new("u");
|
||||
defer glib.free(pids_value_type);
|
||||
|
||||
const pids_value = glib.Variant.newFixedArray(pids_value_type, &pid, 1, @sizeOf(u32));
|
||||
|
||||
builder.add("(sv)", "PIDs", pids_value);
|
||||
}
|
||||
|
||||
{
|
||||
// Aux
|
||||
const aux_type = glib.VariantType.new("a(sa(sv))");
|
||||
defer glib.free(aux_type);
|
||||
|
||||
builder.open(aux_type);
|
||||
defer builder.close();
|
||||
}
|
||||
|
||||
var err: ?*glib.Error = null;
|
||||
defer if (err) |e| e.free();
|
||||
|
||||
const reply_type = glib.VariantType.new("(o)");
|
||||
defer glib.free(reply_type);
|
||||
|
||||
const value = builder.end();
|
||||
|
||||
const reply = connection.callSync(
|
||||
"org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager",
|
||||
"StartTransientUnit",
|
||||
value,
|
||||
reply_type,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| log.err(
|
||||
"creating transient cgroup scope failed code={} err={s}",
|
||||
.{
|
||||
e.f_code,
|
||||
if (e.f_message) |msg| msg else "(no message)",
|
||||
},
|
||||
);
|
||||
return error.DbusCallFailed;
|
||||
};
|
||||
defer reply.unref();
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const glib = @import("glib");
|
||||
|
||||
pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
|
||||
if (comptime build_config.flatpak) {
|
||||
// Only consult Flatpak runtime data for host case.
|
||||
if (internal_os.isFlatpak()) {
|
||||
var result: internal_os.ResourcesDir = .{
|
||||
.app_path = try alloc.dupe(u8, "/app/share/ghostty"),
|
||||
};
|
||||
errdefer alloc.free(result.app_path.?);
|
||||
|
||||
const keyfile = glib.KeyFile.new();
|
||||
defer keyfile.unref();
|
||||
|
||||
if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
|
||||
const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
|
||||
defer glib.free(app_dir.ptr);
|
||||
|
||||
result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return try internal_os.resourcesDir(alloc);
|
||||
}
|
@@ -1,168 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const css_files = [_][]const u8{
|
||||
"style.css",
|
||||
"style-dark.css",
|
||||
"style-hc.css",
|
||||
"style-hc-dark.css",
|
||||
};
|
||||
|
||||
const icons = [_]struct {
|
||||
alias: []const u8,
|
||||
source: []const u8,
|
||||
}{
|
||||
.{
|
||||
.alias = "16x16",
|
||||
.source = "16",
|
||||
},
|
||||
.{
|
||||
.alias = "16x16@2",
|
||||
.source = "32",
|
||||
},
|
||||
.{
|
||||
.alias = "32x32",
|
||||
.source = "32",
|
||||
},
|
||||
.{
|
||||
.alias = "32x32@2",
|
||||
.source = "64",
|
||||
},
|
||||
.{
|
||||
.alias = "128x128",
|
||||
.source = "128",
|
||||
},
|
||||
.{
|
||||
.alias = "128x128@2",
|
||||
.source = "256",
|
||||
},
|
||||
.{
|
||||
.alias = "256x256",
|
||||
.source = "256",
|
||||
},
|
||||
.{
|
||||
.alias = "256x256@2",
|
||||
.source = "512",
|
||||
},
|
||||
.{
|
||||
.alias = "512x512",
|
||||
.source = "512",
|
||||
},
|
||||
.{
|
||||
.alias = "1024x1024",
|
||||
.source = "1024",
|
||||
},
|
||||
};
|
||||
|
||||
pub const VersionedBlueprint = struct {
|
||||
major: u16,
|
||||
minor: u16,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub const blueprint_files = [_]VersionedBlueprint{
|
||||
.{ .major = 1, .minor = 5, .name = "prompt-title-dialog" },
|
||||
.{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" },
|
||||
.{ .major = 1, .minor = 5, .name = "command-palette" },
|
||||
.{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
|
||||
.{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
|
||||
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
|
||||
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
|
||||
.{ .major = 1, .minor = 5, .name = "ccw-paste" },
|
||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-paste" },
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer _ = debug_allocator.deinit();
|
||||
const alloc = debug_allocator.allocator();
|
||||
|
||||
var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer {
|
||||
for (extra_ui_files.items) |item| alloc.free(item);
|
||||
extra_ui_files.deinit(alloc);
|
||||
}
|
||||
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
while (it.next()) |argument| {
|
||||
if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
|
||||
try extra_ui_files.append(alloc, try alloc.dupe(u8, argument));
|
||||
}
|
||||
}
|
||||
|
||||
const writer = std.io.getStdOut().writer();
|
||||
|
||||
try writer.writeAll(
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<gresources>
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty">
|
||||
\\
|
||||
);
|
||||
for (css_files) |css_file| {
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" alias=\"{s}\">src/apprt/gtk/{s}</file>\n",
|
||||
.{ css_file, css_file },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
||||
\\
|
||||
);
|
||||
for (icons) |icon| {
|
||||
try writer.print(
|
||||
" <file alias=\"{s}/apps/com.mitchellh.ghostty.png\">images/gnome/{s}.png</file>\n",
|
||||
.{ icon.alias, icon.source },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
|
||||
\\
|
||||
);
|
||||
for (extra_ui_files.items) |ui_file| {
|
||||
for (blueprint_files) |file| {
|
||||
const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.name });
|
||||
defer alloc.free(expected);
|
||||
if (!std.mem.endsWith(u8, ui_file, expected)) continue;
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{d}.{d}/{s}.ui\">{s}</file>\n",
|
||||
.{ file.major, file.minor, file.name, ui_file },
|
||||
);
|
||||
break;
|
||||
} else return error.BlueprintNotFound;
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\</gresources>
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
pub const dependencies = deps: {
|
||||
const total = css_files.len + icons.len + blueprint_files.len;
|
||||
var deps: [total][]const u8 = undefined;
|
||||
var index: usize = 0;
|
||||
for (css_files) |css_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
|
||||
index += 1;
|
||||
}
|
||||
for (icons) |icon| {
|
||||
deps[index] = std.fmt.comptimePrint("images/gnome/{s}.png", .{icon.source});
|
||||
index += 1;
|
||||
}
|
||||
for (blueprint_files) |blueprint_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{
|
||||
blueprint_file.major,
|
||||
blueprint_file.minor,
|
||||
blueprint_file.name,
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
break :deps deps;
|
||||
};
|
@@ -1,140 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Until the gobject bindings are built at the same time we are building
|
||||
// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version
|
||||
// macros match the version of `gtk4` that we are building/linking against.
|
||||
const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
});
|
||||
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const comptime_version: std.SemanticVersion = .{
|
||||
.major = c.GTK_MAJOR_VERSION,
|
||||
.minor = c.GTK_MINOR_VERSION,
|
||||
.patch = c.GTK_MICRO_VERSION,
|
||||
};
|
||||
|
||||
pub fn getRuntimeVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = gtk.getMajorVersion(),
|
||||
.minor = gtk.getMinorVersion(),
|
||||
.patch = gtk.getMicroVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logVersion() void {
|
||||
log.info("GTK version build={} runtime={}", .{
|
||||
comptime_version,
|
||||
getRuntimeVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies that the GTK version is at least the given version.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it is run in a
|
||||
/// comptime context, it will only check the version in the headers. If it is
|
||||
/// run in a runtime context, it will check the actual version of the library we
|
||||
/// are linked against.
|
||||
///
|
||||
/// This function should be used in cases where the version check would affect
|
||||
/// code generation, such as using symbols that are only available beyond a
|
||||
/// certain version. For checks which only depend on GTK's runtime behavior,
|
||||
/// use `runtimeAtLeast`.
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the runtime checks
|
||||
/// if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version,
|
||||
// we can return false immediately. This prevents us from
|
||||
// compiling against unknown symbols and makes runtime checks
|
||||
// very slightly faster.
|
||||
if (comptime comptime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
return runtimeAtLeast(major, minor, micro);
|
||||
}
|
||||
|
||||
/// Verifies that the GTK version at runtime is at least the given version.
|
||||
///
|
||||
/// This function should be used in cases where the only the runtime behavior
|
||||
/// is affected by the version check. For checks which would affect code
|
||||
/// generation, use `atLeast`.
|
||||
pub inline fn runtimeAtLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
|
||||
// because the function gets the actual runtime version.
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
pub inline fn runtimeUntil(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt;
|
||||
}
|
||||
|
||||
test "atLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
test "runtimeUntil" {
|
||||
const testing = std.testing;
|
||||
|
||||
// This is an array in case we add a comptime variant.
|
||||
const funs = &.{runtimeUntil};
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
const HeaderBar = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const adw = @import("adw");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
/// the Adwaita headerbar widget
|
||||
headerbar: *adw.HeaderBar,
|
||||
|
||||
/// the Window that we belong to
|
||||
window: *Window,
|
||||
|
||||
/// the Adwaita window title widget
|
||||
title: *adw.WindowTitle,
|
||||
|
||||
pub fn init(self: *HeaderBar, window: *Window) void {
|
||||
self.* = .{
|
||||
.headerbar = adw.HeaderBar.new(),
|
||||
.window = window,
|
||||
.title = adw.WindowTitle.new(
|
||||
window.window.as(gtk.Window).getTitle() orelse "Ghostty",
|
||||
"",
|
||||
),
|
||||
};
|
||||
self.headerbar.setTitleWidget(self.title.as(gtk.Widget));
|
||||
}
|
||||
|
||||
pub fn setVisible(self: *const HeaderBar, visible: bool) void {
|
||||
self.headerbar.as(gtk.Widget).setVisible(@intFromBool(visible));
|
||||
}
|
||||
|
||||
pub fn asWidget(self: *const HeaderBar) *gtk.Widget {
|
||||
return self.headerbar.as(gtk.Widget);
|
||||
}
|
||||
|
||||
pub fn packEnd(self: *const HeaderBar, widget: *gtk.Widget) void {
|
||||
self.headerbar.packEnd(widget);
|
||||
}
|
||||
|
||||
pub fn packStart(self: *const HeaderBar, widget: *gtk.Widget) void {
|
||||
self.headerbar.packStart(widget);
|
||||
}
|
||||
|
||||
pub fn setTitle(self: *const HeaderBar, title: [:0]const u8) void {
|
||||
self.window.window.as(gtk.Window).setTitle(title);
|
||||
self.title.setTitle(title);
|
||||
}
|
||||
|
||||
pub fn setSubtitle(self: *const HeaderBar, subtitle: [:0]const u8) void {
|
||||
self.title.setSubtitle(subtitle);
|
||||
}
|
@@ -1,184 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const i18n = @import("../../os/main.zig").i18n;
|
||||
const App = @import("App.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const TerminalWindow = @import("Window.zig");
|
||||
const ImguiWidget = @import("ImguiWidget.zig");
|
||||
const CoreInspector = @import("../../inspector/main.zig").Inspector;
|
||||
|
||||
const log = std.log.scoped(.inspector);
|
||||
|
||||
/// Inspector is the primary stateful object that represents a terminal
|
||||
/// inspector. An inspector is 1:1 with a Surface and is owned by a Surface.
|
||||
/// Closing a surface must close its inspector.
|
||||
pub const Inspector = struct {
|
||||
/// The surface that owns this inspector.
|
||||
surface: *Surface,
|
||||
|
||||
/// The current state of where this inspector is rendered. The Inspector
|
||||
/// is the state of the inspector but this is the state of the GUI.
|
||||
location: LocationState,
|
||||
|
||||
/// This is true if we want to destroy this inspector as soon as the
|
||||
/// location is closed. For example: set this to true, request the
|
||||
/// window be closed, let GTK do its cleanup, then note this to destroy
|
||||
/// the inner state.
|
||||
destroy_on_close: bool = true,
|
||||
|
||||
/// Location where the inspector will be launched.
|
||||
pub const Location = union(LocationKey) {
|
||||
hidden: void,
|
||||
window: void,
|
||||
};
|
||||
|
||||
/// The internal state for each possible location.
|
||||
const LocationState = union(LocationKey) {
|
||||
hidden: void,
|
||||
window: Window,
|
||||
};
|
||||
|
||||
const LocationKey = enum {
|
||||
/// No GUI, but load the inspector state.
|
||||
hidden,
|
||||
|
||||
/// A dedicated window for the inspector.
|
||||
window,
|
||||
};
|
||||
|
||||
/// Create an inspector for the given surface in the given location.
|
||||
pub fn create(surface: *Surface, location: Location) !*Inspector {
|
||||
const alloc = surface.app.core_app.alloc;
|
||||
var ptr = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(ptr);
|
||||
try ptr.init(surface, location);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// Destroy all memory associated with this inspector. You generally
|
||||
/// should NOT call this publicly and should call `close` instead to
|
||||
/// use the GTK lifecycle.
|
||||
pub fn destroy(self: *Inspector) void {
|
||||
assert(self.location == .hidden);
|
||||
const alloc = self.allocator();
|
||||
self.surface.inspector = null;
|
||||
self.deinit();
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn init(self: *Inspector, surface: *Surface, location: Location) !void {
|
||||
self.* = .{
|
||||
.surface = surface,
|
||||
.location = undefined,
|
||||
};
|
||||
|
||||
// Activate the inspector. If it doesn't work we ignore the error
|
||||
// because we can just show an error in the inspector window.
|
||||
self.surface.core_surface.activateInspector() catch |err| {
|
||||
log.err("failed to activate inspector err={}", .{err});
|
||||
};
|
||||
|
||||
switch (location) {
|
||||
.hidden => self.location = .{ .hidden = {} },
|
||||
.window => try self.initWindow(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit(self: *Inspector) void {
|
||||
self.surface.core_surface.deactivateInspector();
|
||||
}
|
||||
|
||||
/// Request the inspector is closed.
|
||||
pub fn close(self: *Inspector) void {
|
||||
switch (self.location) {
|
||||
.hidden => self.locationDidClose(),
|
||||
.window => |v| v.close(),
|
||||
}
|
||||
}
|
||||
|
||||
fn locationDidClose(self: *Inspector) void {
|
||||
self.location = .{ .hidden = {} };
|
||||
if (self.destroy_on_close) self.destroy();
|
||||
}
|
||||
|
||||
pub fn queueRender(self: *const Inspector) void {
|
||||
switch (self.location) {
|
||||
.hidden => {},
|
||||
.window => |v| v.imgui_widget.queueRender(),
|
||||
}
|
||||
}
|
||||
|
||||
fn allocator(self: *const Inspector) Allocator {
|
||||
return self.surface.app.core_app.alloc;
|
||||
}
|
||||
|
||||
fn initWindow(self: *Inspector) !void {
|
||||
self.location = .{ .window = undefined };
|
||||
try self.location.window.init(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// A dedicated window to hold an inspector instance.
|
||||
const Window = struct {
|
||||
inspector: *Inspector,
|
||||
window: *gtk.ApplicationWindow,
|
||||
imgui_widget: ImguiWidget,
|
||||
|
||||
pub fn init(self: *Window, inspector: *Inspector) !void {
|
||||
// Initialize to undefined
|
||||
self.* = .{
|
||||
.inspector = inspector,
|
||||
.window = undefined,
|
||||
.imgui_widget = undefined,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
self.window = .new(inspector.surface.app.app.as(gtk.Application));
|
||||
errdefer self.window.as(gtk.Window).destroy();
|
||||
|
||||
self.window.as(gtk.Window).setTitle(i18n._("Ghostty: Terminal Inspector"));
|
||||
self.window.as(gtk.Window).setDefaultSize(1000, 600);
|
||||
self.window.as(gtk.Window).setIconName(build_config.bundle_id);
|
||||
self.window.as(gtk.Widget).addCssClass("window");
|
||||
self.window.as(gtk.Widget).addCssClass("inspector-window");
|
||||
|
||||
// Initialize our imgui widget
|
||||
try self.imgui_widget.init();
|
||||
errdefer self.imgui_widget.deinit();
|
||||
self.imgui_widget.render_callback = &imguiRender;
|
||||
self.imgui_widget.render_userdata = self;
|
||||
CoreInspector.setup();
|
||||
|
||||
// Signals
|
||||
_ = gtk.Widget.signals.destroy.connect(self.window, *Window, gtkDestroy, self, .{});
|
||||
// Show the window
|
||||
self.window.as(gtk.Window).setChild(self.imgui_widget.gl_area.as(gtk.Widget));
|
||||
self.window.as(gtk.Window).present();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.inspector.locationDidClose();
|
||||
}
|
||||
|
||||
pub fn close(self: *const Window) void {
|
||||
self.window.as(gtk.Window).destroy();
|
||||
}
|
||||
|
||||
fn imguiRender(ud: ?*anyopaque) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||
const surface = &self.inspector.surface.core_surface;
|
||||
const inspector = surface.inspector orelse return;
|
||||
inspector.render();
|
||||
}
|
||||
|
||||
/// "destroy" signal for the window
|
||||
fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.c) void {
|
||||
log.debug("window destroy", .{});
|
||||
self.deinit();
|
||||
}
|
||||
};
|
@@ -1 +0,0 @@
|
||||
pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow;
|
@@ -1,172 +0,0 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
|
||||
// Use a D-Bus method call to open a new window on GTK.
|
||||
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
|
||||
//
|
||||
// `ghostty +new-window` is equivalent to the following command (on a release build):
|
||||
//
|
||||
// ```
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] []
|
||||
// ```
|
||||
//
|
||||
// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build):
|
||||
//
|
||||
// ```
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
|
||||
// ```
|
||||
pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
// Get the appropriate bus name and object path for contacting the
|
||||
// Ghostty instance we're interested in.
|
||||
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
|
||||
.class => |class| result: {
|
||||
// Force the usage of the class specified on the CLI to determine the
|
||||
// bus name and object path.
|
||||
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
|
||||
|
||||
std.mem.replaceScalar(u8, object_path, '.', '/');
|
||||
std.mem.replaceScalar(u8, object_path, '-', '_');
|
||||
|
||||
break :result .{ class, object_path };
|
||||
},
|
||||
.detect => switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" },
|
||||
.ReleaseFast, .ReleaseSmall => .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" },
|
||||
},
|
||||
};
|
||||
defer {
|
||||
switch (target) {
|
||||
.class => alloc.free(object_path),
|
||||
.detect => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
|
||||
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
|
||||
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
const dbus = dbus: {
|
||||
var err_: ?*glib.Error = null;
|
||||
defer if (err_) |err| err.free();
|
||||
|
||||
const dbus_ = gio.busGetSync(.session, null, &err_);
|
||||
if (err_) |err| {
|
||||
try stderr.print(
|
||||
"Unable to establish connection to D-Bus session bus: {s}\n",
|
||||
.{err.f_message orelse "(unknown)"},
|
||||
);
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
break :dbus dbus_ orelse {
|
||||
try stderr.print("gio.busGetSync returned null\n", .{});
|
||||
return error.IPCFailed;
|
||||
};
|
||||
};
|
||||
defer dbus.unref();
|
||||
|
||||
// use a builder to create the D-Bus method call payload
|
||||
const payload = payload: {
|
||||
const builder_type = glib.VariantType.new("(sava{sv})");
|
||||
defer glib.free(builder_type);
|
||||
|
||||
// Initialize our builder to build up our parameters
|
||||
var builder: glib.VariantBuilder = undefined;
|
||||
builder.init(builder_type);
|
||||
errdefer builder.clear();
|
||||
|
||||
// action
|
||||
if (value.arguments == null) {
|
||||
builder.add("s", "new-window");
|
||||
} else {
|
||||
builder.add("s", "new-window-command");
|
||||
}
|
||||
|
||||
// parameters
|
||||
{
|
||||
const av = glib.VariantType.new("av");
|
||||
defer av.free();
|
||||
|
||||
var parameters: glib.VariantBuilder = undefined;
|
||||
parameters.init(av);
|
||||
errdefer parameters.clear();
|
||||
|
||||
if (value.arguments) |arguments| {
|
||||
// If `-e` was specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments
|
||||
// that came after `-e`, which will be interpreted as a command
|
||||
// to run.
|
||||
{
|
||||
const as = glib.VariantType.new("as");
|
||||
defer as.free();
|
||||
|
||||
var command: glib.VariantBuilder = undefined;
|
||||
command.init(as);
|
||||
errdefer command.clear();
|
||||
|
||||
for (arguments) |argument| {
|
||||
command.add("s", argument.ptr);
|
||||
}
|
||||
|
||||
parameters.add("v", command.end());
|
||||
}
|
||||
}
|
||||
|
||||
builder.addValue(parameters.end());
|
||||
}
|
||||
|
||||
{
|
||||
const platform_data = glib.VariantType.new("a{sv}");
|
||||
defer platform_data.free();
|
||||
|
||||
builder.open(platform_data);
|
||||
defer builder.close();
|
||||
|
||||
// we have no platform data
|
||||
}
|
||||
|
||||
break :payload builder.end();
|
||||
};
|
||||
|
||||
{
|
||||
var err_: ?*glib.Error = null;
|
||||
defer if (err_) |err| err.free();
|
||||
|
||||
const result_ = dbus.callSync(
|
||||
bus_name,
|
||||
object_path,
|
||||
"org.gtk.Actions",
|
||||
"Activate",
|
||||
payload,
|
||||
null, // We don't care about the return type, we don't do anything with it.
|
||||
.{}, // no flags
|
||||
-1, // default timeout
|
||||
null, // not cancellable
|
||||
&err_,
|
||||
);
|
||||
defer if (result_) |result| result.unref();
|
||||
|
||||
if (err_) |err| {
|
||||
try stderr.print(
|
||||
"D-Bus method call returned an error err={s}\n",
|
||||
.{err.f_message orelse "(unknown)"},
|
||||
);
|
||||
return error.IPCFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@@ -1,405 +0,0 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const input = @import("../../input.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
|
||||
/// Returns a GTK accelerator string from a trigger.
|
||||
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("<Shift>");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("<Ctrl>");
|
||||
if (trigger.mods.alt) try writer.writeAll("<Alt>");
|
||||
if (trigger.mods.super) try writer.writeAll("<Super>");
|
||||
|
||||
// Write our key
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Returns a XDG-compliant shortcuts string from a trigger.
|
||||
/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/
|
||||
pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("SHIFT+");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("CTRL+");
|
||||
if (trigger.mods.alt) try writer.writeAll("ALT+");
|
||||
if (trigger.mods.super) try writer.writeAll("LOGO+");
|
||||
|
||||
// Write our key
|
||||
// NOTE: While the spec specifies that only libxkbcommon keysyms are
|
||||
// expected, using GTK's keysyms should still work as they are identical
|
||||
// to *X11's* keysyms (which I assume is a subset of libxkbcommon's).
|
||||
// I haven't been able to any evidence to back up that assumption but
|
||||
// this works for now
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool {
|
||||
switch (trigger.key) {
|
||||
.physical => |k| {
|
||||
const keyval = keyvalFromKey(k) orelse return false;
|
||||
try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false));
|
||||
},
|
||||
|
||||
.unicode => |cp| {
|
||||
if (gdk.keyvalName(cp)) |name| {
|
||||
try writer.writeAll(std.mem.span(name));
|
||||
} else {
|
||||
try writer.print("{u}", .{cp});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn translateMods(state: gdk.ModifierType) input.Mods {
|
||||
return .{
|
||||
.shift = state.shift_mask,
|
||||
.ctrl = state.control_mask,
|
||||
.alt = state.alt_mask,
|
||||
.super = state.super_mask,
|
||||
// Lock is dependent on the X settings but we just assume caps lock.
|
||||
.caps_lock = state.lock_mask,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
pub fn keyvalUnicodeUnshifted(
|
||||
widget: *gtk.Widget,
|
||||
event: *gdk.KeyEvent,
|
||||
keycode: u32,
|
||||
) u21 {
|
||||
const display = widget.getDisplay();
|
||||
|
||||
// We need to get the currently active keyboard layout so we know
|
||||
// what group to look at.
|
||||
const layout = event.getLayout();
|
||||
|
||||
// Get all the possible keyboard mappings for this keycode. A keycode is the
|
||||
// physical key pressed.
|
||||
var keys: [*]gdk.KeymapKey = undefined;
|
||||
var keyvals: [*]c_uint = undefined;
|
||||
var n_entries: c_int = 0;
|
||||
if (display.mapKeycode(keycode, &keys, &keyvals, &n_entries) == 0) return 0;
|
||||
|
||||
defer glib.free(keys);
|
||||
defer glib.free(keyvals);
|
||||
|
||||
// debugging:
|
||||
// std.log.debug("layout={}", .{layout});
|
||||
// for (0..@intCast(n_entries)) |i| {
|
||||
// std.log.debug("keymap key={} codepoint={x}", .{
|
||||
// keys[i],
|
||||
// gdk.keyvalToUnicode(keyvals[i]),
|
||||
// });
|
||||
// }
|
||||
|
||||
for (0..@intCast(n_entries)) |i| {
|
||||
if (keys[i].f_group == layout and
|
||||
keys[i].f_level == 0)
|
||||
{
|
||||
return std.math.cast(
|
||||
u21,
|
||||
gdk.keyvalToUnicode(keyvals[i]),
|
||||
) orelse 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Returns the mods to use a key event from a GTK event.
|
||||
/// This requires a lot of context because the GdkEvent
|
||||
/// doesn't contain enough on its own.
|
||||
pub fn eventMods(
|
||||
event: *gdk.Event,
|
||||
physical_key: input.Key,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
action: input.Action,
|
||||
app_winproto: *winproto.App,
|
||||
) input.Mods {
|
||||
const device = event.getDevice();
|
||||
|
||||
var mods = app_winproto.eventMods(device, gtk_mods);
|
||||
mods.num_lock = if (device) |d| d.getNumLockState() != 0 else false;
|
||||
|
||||
// We use the physical key to determine sided modifiers. As
|
||||
// far as I can tell there's no other way to reliably determine
|
||||
// this.
|
||||
//
|
||||
// We also set the main modifier to true if either side is true,
|
||||
// since on both X11/Wayland, GTK doesn't set the main modifier
|
||||
// if only the modifier key is pressed, but our core logic
|
||||
// relies on it.
|
||||
switch (physical_key) {
|
||||
.shift_left => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .left;
|
||||
},
|
||||
|
||||
.shift_right => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .right;
|
||||
},
|
||||
|
||||
.control_left => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .left;
|
||||
},
|
||||
|
||||
.control_right => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .right;
|
||||
},
|
||||
|
||||
.alt_left => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .left;
|
||||
},
|
||||
|
||||
.alt_right => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .right;
|
||||
},
|
||||
|
||||
.meta_left => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .left;
|
||||
},
|
||||
|
||||
.meta_right => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .right;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
/// Returns an input key from a keyval or null if we don't have a mapping.
|
||||
pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
|
||||
for (keymap) |entry| {
|
||||
if (entry[0] == keyval) return entry[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a keyval from an input key or null if we don't have a mapping.
|
||||
pub fn keyvalFromKey(key: input.Key) ?c_uint {
|
||||
switch (key) {
|
||||
inline else => |key_comptime| {
|
||||
return comptime value: {
|
||||
@setEvalBranchQuota(50_000);
|
||||
for (keymap) |entry| {
|
||||
if (entry[1] == key_comptime) break :value entry[0];
|
||||
}
|
||||
|
||||
break :value null;
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test "accelFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("<Shift><Ctrl><Alt><Super>backslash", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
test "xdgShortcutFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
/// A raw entry in the keymap. Our keymap contains mappings between
|
||||
/// GDK keys and our own key enum.
|
||||
const RawEntry = struct { c_uint, input.Key };
|
||||
|
||||
const keymap: []const RawEntry = &.{
|
||||
.{ gdk.KEY_a, .key_a },
|
||||
.{ gdk.KEY_b, .key_b },
|
||||
.{ gdk.KEY_c, .key_c },
|
||||
.{ gdk.KEY_d, .key_d },
|
||||
.{ gdk.KEY_e, .key_e },
|
||||
.{ gdk.KEY_f, .key_f },
|
||||
.{ gdk.KEY_g, .key_g },
|
||||
.{ gdk.KEY_h, .key_h },
|
||||
.{ gdk.KEY_i, .key_i },
|
||||
.{ gdk.KEY_j, .key_j },
|
||||
.{ gdk.KEY_k, .key_k },
|
||||
.{ gdk.KEY_l, .key_l },
|
||||
.{ gdk.KEY_m, .key_m },
|
||||
.{ gdk.KEY_n, .key_n },
|
||||
.{ gdk.KEY_o, .key_o },
|
||||
.{ gdk.KEY_p, .key_p },
|
||||
.{ gdk.KEY_q, .key_q },
|
||||
.{ gdk.KEY_r, .key_r },
|
||||
.{ gdk.KEY_s, .key_s },
|
||||
.{ gdk.KEY_t, .key_t },
|
||||
.{ gdk.KEY_u, .key_u },
|
||||
.{ gdk.KEY_v, .key_v },
|
||||
.{ gdk.KEY_w, .key_w },
|
||||
.{ gdk.KEY_x, .key_x },
|
||||
.{ gdk.KEY_y, .key_y },
|
||||
.{ gdk.KEY_z, .key_z },
|
||||
|
||||
.{ gdk.KEY_0, .digit_0 },
|
||||
.{ gdk.KEY_1, .digit_1 },
|
||||
.{ gdk.KEY_2, .digit_2 },
|
||||
.{ gdk.KEY_3, .digit_3 },
|
||||
.{ gdk.KEY_4, .digit_4 },
|
||||
.{ gdk.KEY_5, .digit_5 },
|
||||
.{ gdk.KEY_6, .digit_6 },
|
||||
.{ gdk.KEY_7, .digit_7 },
|
||||
.{ gdk.KEY_8, .digit_8 },
|
||||
.{ gdk.KEY_9, .digit_9 },
|
||||
|
||||
.{ gdk.KEY_semicolon, .semicolon },
|
||||
.{ gdk.KEY_space, .space },
|
||||
.{ gdk.KEY_apostrophe, .quote },
|
||||
.{ gdk.KEY_comma, .comma },
|
||||
.{ gdk.KEY_grave, .backquote },
|
||||
.{ gdk.KEY_period, .period },
|
||||
.{ gdk.KEY_slash, .slash },
|
||||
.{ gdk.KEY_minus, .minus },
|
||||
.{ gdk.KEY_equal, .equal },
|
||||
.{ gdk.KEY_bracketleft, .bracket_left },
|
||||
.{ gdk.KEY_bracketright, .bracket_right },
|
||||
.{ gdk.KEY_backslash, .backslash },
|
||||
|
||||
.{ gdk.KEY_Up, .arrow_up },
|
||||
.{ gdk.KEY_Down, .arrow_down },
|
||||
.{ gdk.KEY_Right, .arrow_right },
|
||||
.{ gdk.KEY_Left, .arrow_left },
|
||||
.{ gdk.KEY_Home, .home },
|
||||
.{ gdk.KEY_End, .end },
|
||||
.{ gdk.KEY_Insert, .insert },
|
||||
.{ gdk.KEY_Delete, .delete },
|
||||
.{ gdk.KEY_Caps_Lock, .caps_lock },
|
||||
.{ gdk.KEY_Scroll_Lock, .scroll_lock },
|
||||
.{ gdk.KEY_Num_Lock, .num_lock },
|
||||
.{ gdk.KEY_Page_Up, .page_up },
|
||||
.{ gdk.KEY_Page_Down, .page_down },
|
||||
.{ gdk.KEY_Escape, .escape },
|
||||
.{ gdk.KEY_Return, .enter },
|
||||
.{ gdk.KEY_Tab, .tab },
|
||||
.{ gdk.KEY_BackSpace, .backspace },
|
||||
.{ gdk.KEY_Print, .print_screen },
|
||||
.{ gdk.KEY_Pause, .pause },
|
||||
|
||||
.{ gdk.KEY_F1, .f1 },
|
||||
.{ gdk.KEY_F2, .f2 },
|
||||
.{ gdk.KEY_F3, .f3 },
|
||||
.{ gdk.KEY_F4, .f4 },
|
||||
.{ gdk.KEY_F5, .f5 },
|
||||
.{ gdk.KEY_F6, .f6 },
|
||||
.{ gdk.KEY_F7, .f7 },
|
||||
.{ gdk.KEY_F8, .f8 },
|
||||
.{ gdk.KEY_F9, .f9 },
|
||||
.{ gdk.KEY_F10, .f10 },
|
||||
.{ gdk.KEY_F11, .f11 },
|
||||
.{ gdk.KEY_F12, .f12 },
|
||||
.{ gdk.KEY_F13, .f13 },
|
||||
.{ gdk.KEY_F14, .f14 },
|
||||
.{ gdk.KEY_F15, .f15 },
|
||||
.{ gdk.KEY_F16, .f16 },
|
||||
.{ gdk.KEY_F17, .f17 },
|
||||
.{ gdk.KEY_F18, .f18 },
|
||||
.{ gdk.KEY_F19, .f19 },
|
||||
.{ gdk.KEY_F20, .f20 },
|
||||
.{ gdk.KEY_F21, .f21 },
|
||||
.{ gdk.KEY_F22, .f22 },
|
||||
.{ gdk.KEY_F23, .f23 },
|
||||
.{ gdk.KEY_F24, .f24 },
|
||||
.{ gdk.KEY_F25, .f25 },
|
||||
|
||||
.{ gdk.KEY_KP_0, .numpad_0 },
|
||||
.{ gdk.KEY_KP_1, .numpad_1 },
|
||||
.{ gdk.KEY_KP_2, .numpad_2 },
|
||||
.{ gdk.KEY_KP_3, .numpad_3 },
|
||||
.{ gdk.KEY_KP_4, .numpad_4 },
|
||||
.{ gdk.KEY_KP_5, .numpad_5 },
|
||||
.{ gdk.KEY_KP_6, .numpad_6 },
|
||||
.{ gdk.KEY_KP_7, .numpad_7 },
|
||||
.{ gdk.KEY_KP_8, .numpad_8 },
|
||||
.{ gdk.KEY_KP_9, .numpad_9 },
|
||||
.{ gdk.KEY_KP_Decimal, .numpad_decimal },
|
||||
.{ gdk.KEY_KP_Divide, .numpad_divide },
|
||||
.{ gdk.KEY_KP_Multiply, .numpad_multiply },
|
||||
.{ gdk.KEY_KP_Subtract, .numpad_subtract },
|
||||
.{ gdk.KEY_KP_Add, .numpad_add },
|
||||
.{ gdk.KEY_KP_Enter, .numpad_enter },
|
||||
.{ gdk.KEY_KP_Equal, .numpad_equal },
|
||||
|
||||
.{ gdk.KEY_KP_Separator, .numpad_separator },
|
||||
.{ gdk.KEY_KP_Left, .numpad_left },
|
||||
.{ gdk.KEY_KP_Right, .numpad_right },
|
||||
.{ gdk.KEY_KP_Up, .numpad_up },
|
||||
.{ gdk.KEY_KP_Down, .numpad_down },
|
||||
.{ gdk.KEY_KP_Page_Up, .numpad_page_up },
|
||||
.{ gdk.KEY_KP_Page_Down, .numpad_page_down },
|
||||
.{ gdk.KEY_KP_Home, .numpad_home },
|
||||
.{ gdk.KEY_KP_End, .numpad_end },
|
||||
.{ gdk.KEY_KP_Insert, .numpad_insert },
|
||||
.{ gdk.KEY_KP_Delete, .numpad_delete },
|
||||
.{ gdk.KEY_KP_Begin, .numpad_begin },
|
||||
|
||||
.{ gdk.KEY_Copy, .copy },
|
||||
.{ gdk.KEY_Cut, .cut },
|
||||
.{ gdk.KEY_Paste, .paste },
|
||||
|
||||
.{ gdk.KEY_Shift_L, .shift_left },
|
||||
.{ gdk.KEY_Control_L, .control_left },
|
||||
.{ gdk.KEY_Alt_L, .alt_left },
|
||||
.{ gdk.KEY_Super_L, .meta_left },
|
||||
.{ gdk.KEY_Shift_R, .shift_right },
|
||||
.{ gdk.KEY_Control_R, .control_right },
|
||||
.{ gdk.KEY_Alt_R, .alt_right },
|
||||
.{ gdk.KEY_Super_R, .meta_right },
|
||||
|
||||
// TODO: media keys
|
||||
};
|
@@ -1,139 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const gdk = @import("gdk");
|
||||
const gio = @import("gio");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
|
||||
/// Abstract GTK menus to take advantage of machinery for buildtime/comptime
|
||||
/// error checking.
|
||||
pub fn Menu(
|
||||
/// GTK apprt type that the menu is "for". Window and Surface are supported
|
||||
/// right now.
|
||||
comptime T: type,
|
||||
/// Name of the menu. Along with the apprt type, this is used to look up the
|
||||
/// builder ui definitions of the menu.
|
||||
comptime menu_name: []const u8,
|
||||
/// Should the popup have a pointer pointing to the location that it's
|
||||
/// attached to.
|
||||
comptime arrow: bool,
|
||||
) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// parent apprt object
|
||||
parent: *T,
|
||||
|
||||
/// our widget
|
||||
menu_widget: *gtk.PopoverMenu,
|
||||
|
||||
/// initialize the menu
|
||||
pub fn init(self: *Self, parent: *T) void {
|
||||
const object_type = switch (T) {
|
||||
Window => "window",
|
||||
Surface => "surface",
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0);
|
||||
defer builder.deinit();
|
||||
|
||||
const menu_model = builder.getObject(gio.MenuModel, "menu").?;
|
||||
|
||||
const menu_widget = gtk.PopoverMenu.newFromModelFull(menu_model, .{ .nested = true });
|
||||
|
||||
// If this menu has an arrow, don't modify the horizontal alignment
|
||||
// or you get visual anomalies. See PR #6087. Otherwise set the
|
||||
// horizontal alignment to `start` so that the top left corner of
|
||||
// the menu aligns with the point that the menu is popped up at.
|
||||
if (!arrow) menu_widget.as(gtk.Widget).setHalign(.start);
|
||||
|
||||
menu_widget.as(gtk.Popover).setHasArrow(@intFromBool(arrow));
|
||||
|
||||
_ = gtk.Popover.signals.closed.connect(
|
||||
menu_widget,
|
||||
*Self,
|
||||
gtkRefocusTerm,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
self.* = .{
|
||||
.parent = parent,
|
||||
.menu_widget = menu_widget,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setParent(self: *const Self, widget: *gtk.Widget) void {
|
||||
self.menu_widget.as(gtk.Widget).setParent(widget);
|
||||
}
|
||||
|
||||
pub fn asWidget(self: *const Self) *gtk.Widget {
|
||||
return self.menu_widget.as(gtk.Widget);
|
||||
}
|
||||
|
||||
pub fn isVisible(self: *const Self) bool {
|
||||
return self.menu_widget.as(gtk.Widget).getVisible() != 0;
|
||||
}
|
||||
|
||||
/// Refresh the menu. Right now that means enabling/disabling the "Copy"
|
||||
/// menu item based on whether there is an active selection or not, but
|
||||
/// that may change in the future.
|
||||
pub fn refresh(self: *const Self) void {
|
||||
const window: *gtk.Window, const has_selection: bool = switch (T) {
|
||||
Window => window: {
|
||||
const has_selection = if (self.parent.actionSurface()) |core_surface|
|
||||
core_surface.hasSelection()
|
||||
else
|
||||
false;
|
||||
|
||||
break :window .{ self.parent.window.as(gtk.Window), has_selection };
|
||||
},
|
||||
Surface => surface: {
|
||||
const window = self.parent.container.window() orelse return;
|
||||
const has_selection = self.parent.core_surface.hasSelection();
|
||||
break :surface .{ window.window.as(gtk.Window), has_selection };
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
const action_map: *gio.ActionMap = gobject.ext.cast(gio.ActionMap, window) orelse return;
|
||||
const action: *gio.SimpleAction = gobject.ext.cast(
|
||||
gio.SimpleAction,
|
||||
action_map.lookupAction("copy") orelse return,
|
||||
) orelse return;
|
||||
action.setEnabled(@intFromBool(has_selection));
|
||||
}
|
||||
|
||||
/// Pop up the menu at the given coordinates
|
||||
pub fn popupAt(self: *const Self, x: c_int, y: c_int) void {
|
||||
const rect: gdk.Rectangle = .{
|
||||
.f_x = x,
|
||||
.f_y = y,
|
||||
.f_width = 1,
|
||||
.f_height = 1,
|
||||
};
|
||||
const popover = self.menu_widget.as(gtk.Popover);
|
||||
popover.setPointingTo(&rect);
|
||||
self.refresh();
|
||||
popover.popup();
|
||||
}
|
||||
|
||||
/// Refocus tab that lost focus because of the popover menu
|
||||
fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.c) void {
|
||||
const window: *Window = switch (T) {
|
||||
Window => self.parent,
|
||||
Surface => self.parent.container.window() orelse return,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
window.focusCurrentTab();
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
.transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.terminal-window .notebook paned > separator {
|
||||
background-color: rgba(36, 36, 36, 1);
|
||||
background-clip: content-box;
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
.transparent {
|
||||
background-color: transparent;
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
.transparent {
|
||||
background-color: transparent;
|
||||
}
|
@@ -1,116 +0,0 @@
|
||||
label.url-overlay {
|
||||
padding: 4px 8px 4px 8px;
|
||||
outline-style: solid;
|
||||
outline-color: #555555;
|
||||
outline-width: 1px;
|
||||
}
|
||||
|
||||
label.url-overlay:hover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
label.url-overlay.left {
|
||||
border-radius: 0px 6px 0px 0px;
|
||||
}
|
||||
|
||||
label.url-overlay.right {
|
||||
border-radius: 6px 0px 0px 0px;
|
||||
}
|
||||
|
||||
label.url-overlay.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
label.size-overlay {
|
||||
padding: 4px 8px 4px 8px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
outline-style: solid;
|
||||
outline-width: 1px;
|
||||
outline-color: #555555;
|
||||
}
|
||||
|
||||
label.size-overlay.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
window.ssd.no-border-radius {
|
||||
/* Without clearing the border radius, at least on Mutter with
|
||||
* gtk-titlebar=true and gtk-adwaita=false, there is some window artifacting
|
||||
* that this will mitigate.
|
||||
*/
|
||||
border-radius: 0 0;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.terminal-window .notebook paned > separator {
|
||||
background-color: rgba(250, 250, 250, 1);
|
||||
background-clip: content-box;
|
||||
|
||||
/* This works around the oversized drag area for the right side of GtkPaned.
|
||||
*
|
||||
* Upstream Gtk issue:
|
||||
* https://gitlab.gnome.org/GNOME/gtk/-/issues/4484#note_2362002
|
||||
*
|
||||
* Ghostty issue:
|
||||
* https://github.com/ghostty-org/ghostty/issues/3020
|
||||
*
|
||||
* Without this, it's not possible to select the first character on the
|
||||
* right-hand side of a split.
|
||||
*/
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.clipboard-overlay {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.clipboard-content-view {
|
||||
filter: blur(0px);
|
||||
transition: filter 0.3s ease;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.clipboard-content-view.blurred {
|
||||
filter: blur(5px);
|
||||
}
|
||||
|
||||
.command-palette-search {
|
||||
font-size: 1.25rem;
|
||||
padding: 4px;
|
||||
-gtk-icon-size: 20px;
|
||||
}
|
||||
|
||||
.command-palette-search > image:first-child {
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.command-palette-search > image:last-child {
|
||||
margin-left: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
banner.child_exited_normally revealer widget {
|
||||
background-color: rgba(38, 162, 105, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following:
|
||||
/* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */
|
||||
}
|
||||
|
||||
banner.child_exited_abnormally revealer widget {
|
||||
background-color: rgba(192, 28, 40, 0.5);
|
||||
/* after GTK 4.16 is a requirement, switch to the following:
|
||||
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
|
||||
}
|
||||
|
||||
/*
|
||||
* Change the color of an error progressbar
|
||||
*/
|
||||
progressbar.error trough progress {
|
||||
background-color: rgb(192, 28, 40);
|
||||
/* after GTK 4.16 is a requirement, switch to the following: */
|
||||
/* background-color: var(--error-bg-color); */
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
|
||||
menu menu {
|
||||
section {
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
|
||||
menu menu {
|
||||
section {
|
||||
item {
|
||||
label: _("Copy");
|
||||
action: "win.copy";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Paste");
|
||||
action: "win.paste";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Clear");
|
||||
action: "win.clear";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reset");
|
||||
action: "win.reset";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
submenu {
|
||||
label: _("Split");
|
||||
|
||||
item {
|
||||
label: _("Change Title…");
|
||||
action: "win.prompt-title";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
|
||||
submenu {
|
||||
label: _("Tab");
|
||||
|
||||
item {
|
||||
label: _("New Tab");
|
||||
action: "win.new-tab";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "win.close-tab";
|
||||
}
|
||||
}
|
||||
|
||||
submenu {
|
||||
label: _("Window");
|
||||
|
||||
item {
|
||||
label: _("New Window");
|
||||
action: "win.new-window";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Window");
|
||||
action: "win.close";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
submenu {
|
||||
label: _("Config");
|
||||
|
||||
item {
|
||||
label: _("Open Configuration");
|
||||
action: "app.open-config";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reload Configuration");
|
||||
action: "app.reload-config";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,116 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
|
||||
menu menu {
|
||||
section {
|
||||
item {
|
||||
label: _("Copy");
|
||||
action: "win.copy";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Paste");
|
||||
action: "win.paste";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("New Window");
|
||||
action: "win.new-window";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Window");
|
||||
action: "win.close";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("New Tab");
|
||||
action: "win.new-tab";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Close Tab");
|
||||
action: "win.close-tab";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
submenu {
|
||||
label: _("Split");
|
||||
|
||||
item {
|
||||
label: _("Change Title…");
|
||||
action: "win.prompt-title";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Clear");
|
||||
action: "win.clear";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reset");
|
||||
action: "win.reset";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Command Palette");
|
||||
action: "win.toggle-command-palette";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Terminal Inspector");
|
||||
action: "win.toggle-inspector";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Open Configuration");
|
||||
action: "app.open-config";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Reload Configuration");
|
||||
action: "app.reload-config";
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("About Ghostty");
|
||||
action: "win.about";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Quit");
|
||||
action: "app.quit";
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
translation-domain "com.mitchellh.ghostty";
|
||||
|
||||
Adw.MessageDialog clipboard_confirmation_window {
|
||||
heading: _("Authorize Clipboard Access");
|
||||
body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
|
||||
|
||||
responses [
|
||||
cancel: _("Deny") suggested,
|
||||
ok: _("Allow") destructive,
|
||||
]
|
||||
|
||||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
translation-domain "com.mitchellh.ghostty";
|
||||
|
||||
Adw.MessageDialog clipboard_confirmation_window {
|
||||
heading: _("Authorize Clipboard Access");
|
||||
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
|
||||
|
||||
responses [
|
||||
cancel: _("Deny") suggested,
|
||||
ok: _("Allow") destructive,
|
||||
]
|
||||
|
||||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
translation-domain "com.mitchellh.ghostty";
|
||||
|
||||
Adw.MessageDialog clipboard_confirmation_window {
|
||||
heading: _("Warning: Potentially Unsafe Paste");
|
||||
body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
|
||||
|
||||
responses [
|
||||
cancel: _("Cancel") suggested,
|
||||
ok: _("Paste") destructive,
|
||||
]
|
||||
|
||||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.MessageDialog config_errors_dialog {
|
||||
heading: _("Configuration Errors");
|
||||
body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.");
|
||||
|
||||
responses [
|
||||
ignore: _("Ignore"),
|
||||
reload: _("Reload Configuration") suggested,
|
||||
]
|
||||
|
||||
extra-child: ScrolledWindow {
|
||||
min-content-width: 500;
|
||||
min-content-height: 100;
|
||||
|
||||
TextView {
|
||||
editable: false;
|
||||
cursor-visible: false;
|
||||
top-margin: 8;
|
||||
bottom-margin: 8;
|
||||
left-margin: 8;
|
||||
right-margin: 8;
|
||||
|
||||
buffer: TextBuffer error_message {};
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
translation-domain "com.mitchellh.ghostty";
|
||||
|
||||
Adw.AlertDialog clipboard_confirmation_window {
|
||||
heading: _("Authorize Clipboard Access");
|
||||
body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
|
||||
|
||||
responses [
|
||||
cancel: _("Deny") suggested,
|
||||
ok: _("Allow") destructive,
|
||||
]
|
||||
|
||||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
extra-child: ListBox {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list-separate",
|
||||
]
|
||||
|
||||
Overlay {
|
||||
styles [
|
||||
"osd",
|
||||
"clipboard-overlay",
|
||||
]
|
||||
|
||||
ScrolledWindow text_view_scroll {
|
||||
width-request: 500;
|
||||
height-request: 200;
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Adw.SwitchRow remember_choice {
|
||||
title: _("Remember choice for this split");
|
||||
subtitle: _("Reload configuration to show this prompt again");
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
translation-domain "com.mitchellh.ghostty";
|
||||
|
||||
Adw.AlertDialog clipboard_confirmation_window {
|
||||
heading: _("Authorize Clipboard Access");
|
||||
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
|
||||
|
||||
responses [
|
||||
cancel: _("Deny") suggested,
|
||||
ok: _("Allow") destructive,
|
||||
]
|
||||
|
||||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
extra-child: ListBox {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list-separate",
|
||||
]
|
||||
|
||||
Overlay {
|
||||
styles [
|
||||
"osd",
|
||||
"clipboard-overlay",
|
||||
]
|
||||
|
||||
ScrolledWindow text_view_scroll {
|
||||
width-request: 500;
|
||||
height-request: 200;
|
||||
|
||||
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",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Adw.SwitchRow remember_choice {
|
||||
title: _("Remember choice for this split");
|
||||
subtitle: _("Reload configuration to show this prompt again");
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,71 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
translation-domain "com.mitchellh.ghostty";
|
||||
|
||||
Adw.AlertDialog clipboard_confirmation_window {
|
||||
heading: _("Warning: Potentially Unsafe Paste");
|
||||
body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
|
||||
|
||||
responses [
|
||||
cancel: _("Cancel") suggested,
|
||||
ok: _("Paste") destructive,
|
||||
]
|
||||
|
||||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,106 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Gio 2.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.Dialog command-palette {
|
||||
content-width: 700;
|
||||
|
||||
Adw.ToolbarView {
|
||||
top-bar-style: flat;
|
||||
|
||||
[top]
|
||||
Adw.HeaderBar {
|
||||
[title]
|
||||
SearchEntry search {
|
||||
hexpand: true;
|
||||
placeholder-text: _("Execute a command…");
|
||||
|
||||
styles [
|
||||
"command-palette-search",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ScrolledWindow {
|
||||
min-content-height: 300;
|
||||
|
||||
ListView view {
|
||||
show-separators: true;
|
||||
single-click-activate: true;
|
||||
|
||||
model: SingleSelection model {
|
||||
model: FilterListModel {
|
||||
incremental: true;
|
||||
|
||||
filter: AnyFilter {
|
||||
StringFilter {
|
||||
expression: expr item as <$GhosttyCommand>.title;
|
||||
search: bind search.text;
|
||||
}
|
||||
|
||||
StringFilter {
|
||||
expression: expr item as <$GhosttyCommand>.action-key;
|
||||
search: bind search.text;
|
||||
}
|
||||
};
|
||||
|
||||
model: Gio.ListStore source {
|
||||
item-type: typeof<$GhosttyCommand>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
styles [
|
||||
"rich-list",
|
||||
]
|
||||
|
||||
factory: BuilderListItemFactory {
|
||||
template ListItem {
|
||||
child: Box {
|
||||
orientation: horizontal;
|
||||
spacing: 10;
|
||||
tooltip-text: bind template.item as <$GhosttyCommand>.description;
|
||||
|
||||
Box {
|
||||
orientation: vertical;
|
||||
hexpand: true;
|
||||
|
||||
Label {
|
||||
ellipsize: end;
|
||||
halign: start;
|
||||
wrap: false;
|
||||
single-line-mode: true;
|
||||
|
||||
styles [
|
||||
"title",
|
||||
]
|
||||
|
||||
label: bind template.item as <$GhosttyCommand>.title;
|
||||
}
|
||||
|
||||
Label {
|
||||
ellipsize: end;
|
||||
halign: start;
|
||||
wrap: false;
|
||||
single-line-mode: true;
|
||||
|
||||
styles [
|
||||
"subtitle",
|
||||
"monospace",
|
||||
]
|
||||
|
||||
label: bind template.item as <$GhosttyCommand>.action-key;
|
||||
}
|
||||
}
|
||||
|
||||
ShortcutLabel {
|
||||
accelerator: bind template.item as <$GhosttyCommand>.action;
|
||||
valign: center;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.AlertDialog config_errors_dialog {
|
||||
heading: _("Configuration Errors");
|
||||
body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.");
|
||||
|
||||
responses [
|
||||
ignore: _("Ignore"),
|
||||
reload: _("Reload Configuration") suggested,
|
||||
]
|
||||
|
||||
extra-child: ScrolledWindow {
|
||||
min-content-width: 500;
|
||||
min-content-height: 100;
|
||||
|
||||
TextView {
|
||||
editable: false;
|
||||
cursor-visible: false;
|
||||
top-margin: 8;
|
||||
bottom-margin: 8;
|
||||
left-margin: 8;
|
||||
right-margin: 8;
|
||||
|
||||
buffer: TextBuffer error_message {};
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.AlertDialog prompt_title_dialog {
|
||||
heading: _("Change Terminal Title");
|
||||
body: _("Leave blank to restore the default title.");
|
||||
|
||||
responses [
|
||||
cancel: _("Cancel") suggested,
|
||||
ok: _("OK") destructive,
|
||||
]
|
||||
|
||||
focus-widget: title_entry;
|
||||
|
||||
extra-child: Entry title_entry {};
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
# GTK UI files
|
||||
|
||||
This directory is for storing GTK blueprints. GTK blueprints are compiled into
|
||||
GTK resource builder `.ui` files by `blueprint-compiler` at build time and then
|
||||
converted into an embeddable resource by `glib-compile-resources`.
|
||||
|
||||
Blueprint files should be stored in directories that represent the minimum
|
||||
Adwaita version needed to use that resource. Blueprint files should also be
|
||||
formatted using `blueprint-compiler format` as well to ensure consistency
|
||||
(formatting will be checked in CI).
|
||||
|
||||
`blueprint-compiler` version 0.16.0 or newer is required to compile Blueprint
|
||||
files. If your system does not have `blueprint-compiler` or does not have a
|
||||
new enough version you can use the generated source tarballs, which contain
|
||||
precompiled versions of the blueprints.
|
@@ -1,155 +0,0 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
|
||||
const Config = @import("../../config.zig").Config;
|
||||
const input = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
const ApprtWindow = @import("Window.zig");
|
||||
|
||||
pub const noop = @import("winproto/noop.zig");
|
||||
pub const x11 = @import("winproto/x11.zig");
|
||||
pub const wayland = @import("winproto/wayland.zig");
|
||||
|
||||
pub const Protocol = enum {
|
||||
none,
|
||||
wayland,
|
||||
x11,
|
||||
};
|
||||
|
||||
/// App-state for the underlying windowing protocol. There should be one
|
||||
/// instance of this struct per application.
|
||||
pub const App = union(Protocol) {
|
||||
none: noop.App,
|
||||
wayland: if (build_options.wayland) wayland.App else noop.App,
|
||||
x11: if (build_options.x11) x11.App else noop.App,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !App {
|
||||
inline for (@typeInfo(App).@"union".fields) |field| {
|
||||
if (try field.type.init(
|
||||
alloc,
|
||||
gdk_display,
|
||||
app_id,
|
||||
config,
|
||||
)) |v| {
|
||||
return @unionInit(App, field.name, v);
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .none = .{} };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
self: *App,
|
||||
device: ?*gdk.Device,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) input.Mods {
|
||||
return switch (self.*) {
|
||||
inline else => |*v| v.eventMods(device, gtk_mods),
|
||||
} orelse key.translateMods(gtk_mods);
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.supportsQuickTerminal(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Set up necessary support for the quick terminal that must occur
|
||||
/// *before* the window-level winproto object is created.
|
||||
///
|
||||
/// Only has an effect on the Wayland backend, where the gtk4-layer-shell
|
||||
/// library is initialized.
|
||||
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.initQuickTerminal(apprt_window),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-Window state for the underlying windowing protocol.
|
||||
///
|
||||
/// In Wayland, the terminology used is "Surface" and for it, this is
|
||||
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
|
||||
/// heavily to mean something completely different, so we use "Window" here
|
||||
/// to better match what it generally maps to in the Ghostty codebase.
|
||||
pub const Window = union(Protocol) {
|
||||
none: noop.Window,
|
||||
wayland: if (build_options.wayland) wayland.Window else noop.Window,
|
||||
x11: if (build_options.x11) x11.Window else noop.Window,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
return switch (app.*) {
|
||||
inline else => |*v, tag| {
|
||||
inline for (@typeInfo(Window).@"union".fields) |field| {
|
||||
if (comptime std.mem.eql(
|
||||
u8,
|
||||
field.name,
|
||||
@tagName(tag),
|
||||
)) return @unionInit(
|
||||
Window,
|
||||
field.name,
|
||||
try field.type.init(
|
||||
alloc,
|
||||
v,
|
||||
apprt_window,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.resizeEvent(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.syncAppearance(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.clientSideDecorationEnabled(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.addSubprocessEnv(env),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.setUrgent(urgent),
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,75 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../Window.zig");
|
||||
|
||||
const log = std.log.scoped(.winproto_noop);
|
||||
|
||||
pub const App = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *gdk.Display,
|
||||
_: [:0]const u8,
|
||||
_: *const Config,
|
||||
) !?App {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*gdk.Device,
|
||||
_: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
return false;
|
||||
}
|
||||
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *App,
|
||||
_: *ApprtWindow,
|
||||
) !Window {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
_: *Window,
|
||||
_: *const ApprtWindow.DerivedConfig,
|
||||
) !void {}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(_: *Window) !void {}
|
||||
|
||||
/// This returns true if CSD is enabled for this window. This
|
||||
/// should be the actual present state of the window, not the
|
||||
/// desired state.
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
_ = self;
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
|
||||
|
||||
pub fn setUrgent(_: *Window, _: bool) !void {}
|
||||
};
|
@@ -1,511 +0,0 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gdk_wayland = @import("gdk_wayland");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const layer_shell = @import("gtk4-layer-shell");
|
||||
const wayland = @import("wayland");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../Window.zig");
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
const log = std.log.scoped(.winproto_wayland);
|
||||
|
||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||
pub const App = struct {
|
||||
display: *wl.Display,
|
||||
context: *Context,
|
||||
|
||||
const Context = struct {
|
||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
||||
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
||||
///
|
||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
||||
/// creates a quick terminal, and we need to ensure this fails
|
||||
/// gracefully if this situation occurs.
|
||||
///
|
||||
/// FIXME: This is a temporary workaround - we should remove this when
|
||||
/// all of our supported distros drop support for affected old
|
||||
/// gtk4-layer-shell versions.
|
||||
///
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = config;
|
||||
_ = app_id;
|
||||
|
||||
const gdk_wayland_display = gobject.ext.cast(
|
||||
gdk_wayland.WaylandDisplay,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const display: *wl.Display = @ptrCast(@alignCast(
|
||||
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
|
||||
));
|
||||
|
||||
// Create our context for our callbacks so we have a stable pointer.
|
||||
// Note: at the time of writing this comment, we don't really need
|
||||
// a stable pointer, but it's too scary that we'd need one in the future
|
||||
// and not have it and corrupt memory or something so let's just do it.
|
||||
const context = try alloc.create(Context);
|
||||
errdefer alloc.destroy(context);
|
||||
context.* = .{};
|
||||
|
||||
// Get our display registry so we can get all the available interfaces
|
||||
// and bind to what we need.
|
||||
const registry = try display.getRegistry();
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
// Do another round-trip to get the default decoration mode
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.context = context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
alloc.destroy(self.context);
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*gdk.Device,
|
||||
_: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
if (!layer_shell.isSupported()) {
|
||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 4,
|
||||
}) == .lt) {
|
||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
|
||||
const window = apprt_window.window.as(gtk.Window);
|
||||
|
||||
layer_shell.initForWindow(window);
|
||||
layer_shell.setLayer(window, .top);
|
||||
layer_shell.setNamespace(window, "ghostty-quick-terminal");
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) return null;
|
||||
return T;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
||||
|
||||
switch (event) {
|
||||
.global => |v| {
|
||||
log.debug("found global {s}", .{v.interface});
|
||||
|
||||
// We don't actually do anything with this other than checking
|
||||
// for its existence, so we process this separately.
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
"xdg_wm_dialog_v1",
|
||||
) == .eq) {
|
||||
context.xdg_wm_dialog_present = true;
|
||||
return;
|
||||
}
|
||||
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
T.interface.name,
|
||||
) == .eq) {
|
||||
log.debug("matched {}", .{T});
|
||||
|
||||
@field(context, field.name) = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error binding interface {s} error={}",
|
||||
.{ v.interface, err },
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.default_mode => |mode| {
|
||||
context.default_deco_mode = @enumFromInt(mode.mode);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
pub const Window = struct {
|
||||
apprt_window: *ApprtWindow,
|
||||
|
||||
/// The Wayland surface for this window.
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// The context from the app where we can load our Wayland interfaces.
|
||||
app_context: *App.Context,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
decoration: ?*org.KdeKwinServerDecoration = null,
|
||||
|
||||
/// Object that controls the slide-in/slide-out animations of the
|
||||
/// quick terminal. Always null for windows other than the quick terminal.
|
||||
slide: ?*org.KdeKwinSlide = null,
|
||||
|
||||
/// Object that, when present, denotes that the window is currently
|
||||
/// requesting attention from the user.
|
||||
activation_token: ?*xdg.ActivationTokenV1 = null,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const gtk_native = apprt_window.window.as(gtk.Native);
|
||||
const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface;
|
||||
|
||||
// This should never fail, because if we're being called at this point
|
||||
// then we've already asserted that our app state is Wayland.
|
||||
const gdk_wl_surface = gobject.ext.cast(
|
||||
gdk_wayland.WaylandSurface,
|
||||
gdk_surface,
|
||||
) orelse return error.NoWaylandSurface;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(@alignCast(
|
||||
gdk_wl_surface.getWlSurface() orelse return error.NoWaylandSurface,
|
||||
));
|
||||
|
||||
// Get our decoration object so we can control the
|
||||
// CSD vs SSD status of this surface.
|
||||
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
||||
const mgr = app.context.kde_decoration_manager orelse
|
||||
break :deco null;
|
||||
|
||||
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
||||
wl_surface,
|
||||
) catch |err| {
|
||||
log.warn("could not create decoration object={}", .{err});
|
||||
break :deco null;
|
||||
};
|
||||
|
||||
break :deco deco;
|
||||
};
|
||||
|
||||
if (apprt_window.isQuickTerminal()) {
|
||||
_ = gdk.Surface.signals.enter_monitor.connect(
|
||||
gdk_surface,
|
||||
*ApprtWindow,
|
||||
enteredMonitor,
|
||||
apprt_window,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
return .{
|
||||
.apprt_window = apprt_window,
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.decoration = deco,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
if (self.decoration) |deco| deco.release();
|
||||
if (self.slide) |slide| slide.release();
|
||||
}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
self.syncDecoration() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
|
||||
if (self.apprt_window.isQuickTerminal()) {
|
||||
self.syncQuickTerminal() catch |err| {
|
||||
log.warn("failed to sync quick terminal appearance={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self.getDecorationMode()) {
|
||||
.Client => true,
|
||||
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
|
||||
// However, if we do not support SSDs (e.g. GNOME) then we should enable
|
||||
// CSDs even if the user prefers SSDs.
|
||||
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
|
||||
.None => false,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
_ = self;
|
||||
_ = env;
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
|
||||
// If there already is a token, destroy and unset it
|
||||
if (self.activation_token) |token| token.destroy();
|
||||
|
||||
self.activation_token = if (urgent) token: {
|
||||
const token = try activation.getActivationToken();
|
||||
token.setSurface(self.surface);
|
||||
token.setListener(*Window, onActivationTokenEvent, self);
|
||||
token.commit();
|
||||
break :token token;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn syncBlur(self: *Window) !void {
|
||||
const manager = self.app_context.kde_blur_manager orelse return;
|
||||
const blur = self.apprt_window.config.background_blur;
|
||||
|
||||
if (self.blur_token) |tok| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blur.enabled()) {
|
||||
manager.unset(self.surface);
|
||||
tok.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blur.enabled()) {
|
||||
const tok = try manager.create(self.surface);
|
||||
tok.commit();
|
||||
self.blur_token = tok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecoration(self: *Window) !void {
|
||||
const deco = self.decoration orelse return;
|
||||
|
||||
// The protocol requests uint instead of enum so we have
|
||||
// to convert it.
|
||||
deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
|
||||
}
|
||||
|
||||
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
||||
return switch (self.apprt_window.config.window_decoration) {
|
||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
||||
.client => .Client,
|
||||
.server => .Server,
|
||||
.none => .None,
|
||||
};
|
||||
}
|
||||
|
||||
fn syncQuickTerminal(self: *Window) !void {
|
||||
const window = self.apprt_window.window.as(gtk.Window);
|
||||
const config = &self.apprt_window.config;
|
||||
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.quick_terminal_keyboard_interactivity) {
|
||||
.none => .none,
|
||||
.@"on-demand" => on_demand: {
|
||||
if (layer_shell.getProtocolVersion() < 4) {
|
||||
log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{});
|
||||
break :on_demand .exclusive;
|
||||
}
|
||||
break :on_demand .on_demand;
|
||||
},
|
||||
.exclusive => .exclusive,
|
||||
},
|
||||
);
|
||||
|
||||
const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) {
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.center => null,
|
||||
};
|
||||
|
||||
for (std.meta.tags(layer_shell.ShellEdge)) |edge| {
|
||||
if (anchored_edge) |anchored| {
|
||||
if (edge == anchored) {
|
||||
layer_shell.setMargin(window, edge, 0);
|
||||
layer_shell.setAnchor(window, edge, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary margin - could be made customizable?
|
||||
layer_shell.setMargin(window, edge, 20);
|
||||
layer_shell.setAnchor(window, edge, false);
|
||||
}
|
||||
|
||||
if (self.slide) |slide| slide.release();
|
||||
|
||||
self.slide = if (anchored_edge) |anchored| slide: {
|
||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
||||
|
||||
const slide = mgr.create(self.surface) catch |err| {
|
||||
log.warn("could not create slide object={}", .{err});
|
||||
break :slide null;
|
||||
};
|
||||
|
||||
const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
};
|
||||
|
||||
slide.setLocation(@intCast(@intFromEnum(slide_location)));
|
||||
slide.commit();
|
||||
break :slide slide;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the size of the quick terminal based on monitor dimensions.
|
||||
fn enteredMonitor(
|
||||
_: *gdk.Surface,
|
||||
monitor: *gdk.Monitor,
|
||||
apprt_window: *ApprtWindow,
|
||||
) callconv(.c) void {
|
||||
const window = apprt_window.window.as(gtk.Window);
|
||||
const config = &apprt_window.config;
|
||||
|
||||
var monitor_size: gdk.Rectangle = undefined;
|
||||
monitor.getGeometry(&monitor_size);
|
||||
|
||||
const dims = config.quick_terminal_size.calculate(
|
||||
config.quick_terminal_position,
|
||||
.{
|
||||
.width = @intCast(monitor_size.f_width),
|
||||
.height = @intCast(monitor_size.f_height),
|
||||
},
|
||||
);
|
||||
|
||||
window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
|
||||
}
|
||||
|
||||
fn onActivationTokenEvent(
|
||||
token: *xdg.ActivationTokenV1,
|
||||
event: xdg.ActivationTokenV1.Event,
|
||||
self: *Window,
|
||||
) void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
const current_token = self.activation_token orelse return;
|
||||
|
||||
if (token.getId() != current_token.getId()) {
|
||||
log.warn("received event for unknown activation token; ignoring", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
.done => |done| {
|
||||
activation.activate(done.token, self.surface);
|
||||
token.destroy();
|
||||
self.activation_token = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,507 +0,0 @@
|
||||
//! X11 window protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gdk_x11 = @import("gdk_x11");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const xlib = @import("xlib");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("X11/Xlib.h");
|
||||
@cInclude("X11/Xatom.h");
|
||||
@cInclude("X11/XKBlib.h");
|
||||
});
|
||||
|
||||
const input = @import("../../../input.zig");
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const ApprtWindow = @import("../Window.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
pub const App = struct {
|
||||
display: *xlib.Display,
|
||||
base_event_code: c_int,
|
||||
atoms: Atoms,
|
||||
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
const gdk_x11_display = gobject.ext.cast(
|
||||
gdk_x11.X11Display,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const xlib_display = gdk_x11_display.getXdisplay();
|
||||
|
||||
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
// This is how the instance name field for WM_CLASS is derived when
|
||||
// calling gdk_x11_display_set_program_class; there does not seem to be
|
||||
// a way to set it directly. It does not look like this is being set by
|
||||
// our other app initialization routines currently, but since we're
|
||||
// currently deriving its value from x11-instance-name effectively, I
|
||||
// feel like gating it behind an X11 check is better intent.
|
||||
//
|
||||
// This makes the property show up like so when using xprop:
|
||||
//
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
//
|
||||
// Append "-debug" on both when using the debug build.
|
||||
glib.setPrgname(x11_program_name);
|
||||
gdk_x11.X11Display.setProgramClass(gdk_display, app_id);
|
||||
|
||||
// XKB
|
||||
log.debug("Xkb.init: initializing Xkb", .{});
|
||||
log.debug("Xkb.init: running XkbQueryExtension", .{});
|
||||
var opcode: c_int = 0;
|
||||
var base_event_code: c_int = 0;
|
||||
var base_error_code: c_int = 0;
|
||||
var major = c.XkbMajorVersion;
|
||||
var minor = c.XkbMinorVersion;
|
||||
if (c.XkbQueryExtension(
|
||||
@ptrCast(@alignCast(xlib_display)),
|
||||
&opcode,
|
||||
&base_event_code,
|
||||
&base_error_code,
|
||||
&major,
|
||||
&minor,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
|
||||
if (c.XkbSelectEventDetails(
|
||||
@ptrCast(@alignCast(xlib_display)),
|
||||
c.XkbUseCoreKbd,
|
||||
c.XkbStateNotify,
|
||||
c.XkbModifierStateMask,
|
||||
c.XkbModifierStateMask,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = xlib_display,
|
||||
.base_event_code = base_event_code,
|
||||
.atoms = .init(gdk_x11_display),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event, and returns the
|
||||
/// keyboard state based on if it finds any. This is necessary as the
|
||||
/// standard GTK X11 API (and X11 in general) does not include the current
|
||||
/// key pressed in any modifier state snapshot for that event (e.g. if the
|
||||
/// pressed key is a modifier, that is not necessarily reflected in the
|
||||
/// modifiers).
|
||||
///
|
||||
/// Returns null if there is no event. In this case, the caller should fall
|
||||
/// back to the standard GDK modifier state (this likely means the key
|
||||
/// event did not result in a modifier change).
|
||||
pub fn eventMods(
|
||||
self: App,
|
||||
device: ?*gdk.Device,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
_ = device;
|
||||
_ = gtk_mods;
|
||||
|
||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
||||
if (c.XEventsQueued(
|
||||
@ptrCast(@alignCast(self.display)),
|
||||
c.QueuedAfterReading,
|
||||
) == 0) return null;
|
||||
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = c.XPeekEvent(@ptrCast(@alignCast(self.display)), &nextEvent);
|
||||
if (nextEvent.type != self.base_event_code) return null;
|
||||
|
||||
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
|
||||
if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
|
||||
|
||||
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
|
||||
// Check the state according to XKB masks.
|
||||
const lookup_mods = xkb_state_notify_event.lookup_mods;
|
||||
var mods: input.Mods = .{};
|
||||
|
||||
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
|
||||
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
|
||||
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
|
||||
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
|
||||
if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
|
||||
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
log.warn("quick terminal is not yet supported on X11", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
app: *App,
|
||||
config: *const ApprtWindow.DerivedConfig,
|
||||
gtk_window: *adw.ApplicationWindow,
|
||||
x11_surface: *gdk_x11.X11Surface,
|
||||
|
||||
blur_region: Region = .{},
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const surface = apprt_window.window.as(
|
||||
gtk.Native,
|
||||
).getSurface() orelse return error.NotX11Surface;
|
||||
|
||||
const x11_surface = gobject.ext.cast(
|
||||
gdk_x11.X11Surface,
|
||||
surface,
|
||||
) orelse return error.NotX11Surface;
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.config = &apprt_window.config,
|
||||
.gtk_window = apprt_window.window,
|
||||
.x11_surface = x11_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
// The user could have toggled between CSDs and SSDs,
|
||||
// therefore we need to recalculate the blur region offset.
|
||||
self.blur_region = blur: {
|
||||
// NOTE(pluiedev): CSDs are a f--king mistake.
|
||||
// Please, GNOME, stop this nonsense of making a window ~30% bigger
|
||||
// internally than how they really are just for your shadows and
|
||||
// rounded corners and all that fluff. Please. I beg of you.
|
||||
var x: f64 = 0;
|
||||
var y: f64 = 0;
|
||||
|
||||
self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor());
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
};
|
||||
};
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to synchronize blur={}", .{err});
|
||||
};
|
||||
self.syncDecorations() catch |err| {
|
||||
log.err("failed to synchronize decorations={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self.config.window_decoration) {
|
||||
.auto, .client => true,
|
||||
.server, .none => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn syncBlur(self: *Window) !void {
|
||||
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
|
||||
// which means that the blur region will grow slightly outside of the
|
||||
// window borders. Unfortunately, actually calculating the rounded
|
||||
// region can be quite complex without having access to existing APIs
|
||||
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
|
||||
// and I think it's not really noticeable enough to justify the effort.
|
||||
// (Wayland also has this visual artifact anyway...)
|
||||
|
||||
const gtk_widget = self.gtk_window.as(gtk.Widget);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = self.gtk_window.as(gtk.Widget).getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
|
||||
const blur = self.config.background_blur;
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur.enabled()) {
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
} else {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecorations(self: *Window) !void {
|
||||
var hints: MotifWMHints = .{};
|
||||
|
||||
self.getWindowProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{},
|
||||
&hints,
|
||||
) catch |err| switch (err) {
|
||||
// motif_wm_hints is already initialized, so this is fine
|
||||
error.PropertyNotFound => {},
|
||||
|
||||
error.RequestFailed,
|
||||
error.PropertyTypeMismatch,
|
||||
error.PropertyFormatMismatch,
|
||||
=> return err,
|
||||
};
|
||||
|
||||
hints.flags.decorations = true;
|
||||
hints.decorations.all = switch (self.config.window_decoration) {
|
||||
.server => true,
|
||||
.auto, .client, .none => false,
|
||||
};
|
||||
|
||||
try self.changeProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&hints,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const window_id = try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"{}",
|
||||
.{self.x11_surface.getXid()},
|
||||
);
|
||||
|
||||
try env.put("WINDOWID", window_id);
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
self.x11_surface.setUrgencyHint(@intFromBool(urgent));
|
||||
}
|
||||
|
||||
fn getWindowProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
name: c.Atom,
|
||||
typ: c.Atom,
|
||||
comptime format: PropertyFormat,
|
||||
options: struct {
|
||||
offset: c_long = 0,
|
||||
length: c_long = std.math.maxInt(c_long),
|
||||
delete: bool = false,
|
||||
},
|
||||
result: *T,
|
||||
) GetWindowPropertyError!void {
|
||||
// FIXME: Maybe we should switch to libxcb one day.
|
||||
// Sounds like a much better idea than whatever this is
|
||||
var actual_type_return: c.Atom = undefined;
|
||||
var actual_format_return: c_int = undefined;
|
||||
var nitems_return: c_ulong = undefined;
|
||||
var bytes_after_return: c_ulong = undefined;
|
||||
var prop_return: ?format.bufferType() = null;
|
||||
|
||||
const code = c.XGetWindowProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
options.offset,
|
||||
options.length,
|
||||
@intFromBool(options.delete),
|
||||
typ,
|
||||
&actual_type_return,
|
||||
&actual_format_return,
|
||||
&nitems_return,
|
||||
&bytes_after_return,
|
||||
@ptrCast(&prop_return),
|
||||
);
|
||||
if (code != c.Success) return error.RequestFailed;
|
||||
|
||||
if (actual_type_return == c.None) return error.PropertyNotFound;
|
||||
if (typ != actual_type_return) return error.PropertyTypeMismatch;
|
||||
if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
|
||||
|
||||
const data_ptr: *T = @ptrCast(prop_return);
|
||||
result.* = data_ptr.*;
|
||||
_ = c.XFree(prop_return);
|
||||
}
|
||||
|
||||
fn changeProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
name: c.Atom,
|
||||
typ: c.Atom,
|
||||
comptime format: PropertyFormat,
|
||||
options: struct {
|
||||
mode: PropertyChangeMode,
|
||||
},
|
||||
value: *T,
|
||||
) X11Error!void {
|
||||
const data: format.bufferType() = @ptrCast(value);
|
||||
|
||||
const status = c.XChangeProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
typ,
|
||||
@intFromEnum(format),
|
||||
@intFromEnum(options.mode),
|
||||
data,
|
||||
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
|
||||
);
|
||||
|
||||
// For some godforsaken reason Xlib alternates between
|
||||
// error values (0 = success) and booleans (1 = success), and they look exactly
|
||||
// the same in the signature (just `int`, since Xlib is written in C89)...
|
||||
if (status == 0) return error.RequestFailed;
|
||||
}
|
||||
|
||||
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
|
||||
const status = c.XDeleteProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
);
|
||||
if (status == 0) return error.RequestFailed;
|
||||
}
|
||||
};
|
||||
|
||||
const X11Error = error{
|
||||
RequestFailed,
|
||||
};
|
||||
|
||||
const GetWindowPropertyError = X11Error || error{
|
||||
PropertyNotFound,
|
||||
PropertyTypeMismatch,
|
||||
PropertyFormatMismatch,
|
||||
};
|
||||
|
||||
const Atoms = struct {
|
||||
kde_blur: c.Atom,
|
||||
motif_wm_hints: c.Atom,
|
||||
|
||||
fn init(display: *gdk_x11.X11Display) Atoms {
|
||||
return .{
|
||||
.kde_blur = gdk_x11.x11GetXatomByNameForDisplay(
|
||||
display,
|
||||
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||
),
|
||||
.motif_wm_hints = gdk_x11.x11GetXatomByNameForDisplay(
|
||||
display,
|
||||
"_MOTIF_WM_HINTS",
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const PropertyChangeMode = enum(c_int) {
|
||||
replace = c.PropModeReplace,
|
||||
prepend = c.PropModePrepend,
|
||||
append = c.PropModeAppend,
|
||||
};
|
||||
|
||||
const PropertyFormat = enum(c_int) {
|
||||
_8 = 8,
|
||||
_16 = 16,
|
||||
_32 = 32,
|
||||
|
||||
fn elemType(comptime self: PropertyFormat) type {
|
||||
return switch (self) {
|
||||
._8 => c_char,
|
||||
._16 => c_int,
|
||||
._32 => c_long,
|
||||
};
|
||||
}
|
||||
|
||||
fn bufferType(comptime self: PropertyFormat) type {
|
||||
// The buffer type has to be a multi-pointer to bytes
|
||||
// *aligned to the element type* (very important,
|
||||
// otherwise you'll read garbage!)
|
||||
//
|
||||
// I know this is really ugly. X11 is ugly. I consider it apropos.
|
||||
return [*]align(@alignOf(self.elemType())) u8;
|
||||
}
|
||||
};
|
||||
|
||||
const Region = extern struct {
|
||||
x: c_long = 0,
|
||||
y: c_long = 0,
|
||||
width: c_long = 0,
|
||||
height: c_long = 0,
|
||||
};
|
||||
|
||||
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
|
||||
const MotifWMHints = extern struct {
|
||||
flags: packed struct(c_ulong) {
|
||||
_pad: u1 = 0,
|
||||
decorations: bool = false,
|
||||
|
||||
// We don't really care about the other flags
|
||||
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
|
||||
} = .{},
|
||||
functions: c_ulong = 0,
|
||||
decorations: packed struct(c_ulong) {
|
||||
all: bool = false,
|
||||
|
||||
// We don't really care about the other flags
|
||||
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
|
||||
} = .{},
|
||||
input_mode: c_long = 0,
|
||||
status: c_ulong = 0,
|
||||
};
|
@@ -39,13 +39,13 @@ pub const Clipboard = enum(Backing) {
|
||||
// Our backing isn't is as small as we can in Zig, but a full
|
||||
// C int if we're binding to C APIs.
|
||||
const Backing = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => c_int,
|
||||
.@"gtk-ng" => c_int,
|
||||
else => u2,
|
||||
};
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
Clipboard,
|
||||
.{ .name = "GhosttyApprtClipboard" },
|
||||
),
|
||||
@@ -74,7 +74,7 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
ClipboardRequest,
|
||||
.{ .name = "GhosttyClipboardRequest" },
|
||||
),
|
||||
|
@@ -111,7 +111,6 @@ pub const Message = union(enum) {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk,
|
||||
.@"gtk-ng",
|
||||
=> @import("gobject").ext.defineBoxed(
|
||||
ChildExited,
|
||||
|
@@ -20,11 +20,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
|
||||
// Get the resources we're going to inject into the source tarball.
|
||||
const alloc = b.allocator;
|
||||
var resources: std.ArrayListUnmanaged(Resource) = .empty;
|
||||
{
|
||||
const gtk = SharedDeps.gtkDistResources(b);
|
||||
try resources.append(alloc, gtk.resources_c);
|
||||
try resources.append(alloc, gtk.resources_h);
|
||||
}
|
||||
{
|
||||
const gtk = SharedDeps.gtkNgDistResources(b);
|
||||
try resources.append(alloc, gtk.resources_c);
|
||||
|
@@ -3,7 +3,7 @@ const GhosttyI18n = @This();
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Config = @import("Config.zig");
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
const gresource = @import("../apprt/gtk-ng/build/gresource.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
const domain = "com.mitchellh.ghostty";
|
||||
@@ -78,9 +78,9 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
|
||||
// Not cacheable due to the gresource files
|
||||
xgettext.has_side_effects = true;
|
||||
|
||||
inline for (gresource.blueprint_files) |blp| {
|
||||
inline for (gresource.blueprints) |blp| {
|
||||
const path = std.fmt.comptimePrint(
|
||||
"src/apprt/gtk/ui/{[major]}.{[minor]}/{[name]s}.blp",
|
||||
"src/apprt/gtk-ng/ui/{[major]}.{[minor]}/{[name]s}.blp",
|
||||
blp,
|
||||
);
|
||||
// The arguments to xgettext must be the relative path in the build root
|
||||
@@ -105,7 +105,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
|
||||
}
|
||||
|
||||
var gtk_dir = try b.build_root.handle.openDir(
|
||||
"src/apprt/gtk",
|
||||
"src/apprt/gtk-ng",
|
||||
.{ .iterate = true },
|
||||
);
|
||||
defer gtk_dir.close();
|
||||
@@ -138,7 +138,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
|
||||
);
|
||||
|
||||
for (gtk_files.items) |item| {
|
||||
const path = b.pathJoin(&.{ "src/apprt/gtk", item });
|
||||
const path = b.pathJoin(&.{ "src/apprt/gtk-ng", item });
|
||||
// The arguments to xgettext must be the relative path in the build root
|
||||
// or the resulting files will contain the absolute path. This will
|
||||
// cause a lot of churn because not everyone has the Ghostty code
|
||||
|
@@ -550,7 +550,6 @@ pub fn add(
|
||||
|
||||
switch (self.config.app_runtime) {
|
||||
.none => {},
|
||||
.gtk => try self.addGTK(step),
|
||||
.@"gtk-ng" => try self.addGtkNg(step),
|
||||
}
|
||||
}
|
||||
@@ -789,234 +788,6 @@ pub fn gtkNgDistResources(
|
||||
};
|
||||
}
|
||||
|
||||
/// Setup the dependencies for the GTK apprt build. The GTK apprt
|
||||
/// is particularly involved compared to others so we pull this out
|
||||
/// into a dedicated function.
|
||||
fn addGTK(
|
||||
self: *const SharedDeps,
|
||||
step: *std.Build.Step.Compile,
|
||||
) !void {
|
||||
const b = step.step.owner;
|
||||
const target = step.root_module.resolved_target.?;
|
||||
const optimize = step.root_module.optimize.?;
|
||||
|
||||
const gobject_ = b.lazyDependency("gobject", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
if (gobject_) |gobject| {
|
||||
const gobject_imports = .{
|
||||
.{ "adw", "adw1" },
|
||||
.{ "gdk", "gdk4" },
|
||||
.{ "gio", "gio2" },
|
||||
.{ "glib", "glib2" },
|
||||
.{ "gobject", "gobject2" },
|
||||
.{ "gtk", "gtk4" },
|
||||
.{ "xlib", "xlib2" },
|
||||
};
|
||||
inline for (gobject_imports) |import| {
|
||||
const name, const module = import;
|
||||
step.root_module.addImport(name, gobject.module(module));
|
||||
}
|
||||
}
|
||||
|
||||
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
|
||||
|
||||
if (self.config.x11) {
|
||||
step.linkSystemLibrary2("X11", dynamic_link_opts);
|
||||
if (gobject_) |gobject| {
|
||||
step.root_module.addImport(
|
||||
"gdk_x11",
|
||||
gobject.module("gdkx114"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.config.wayland) wayland: {
|
||||
// These need to be all be called to note that we need them.
|
||||
const wayland_dep_ = b.lazyDependency("wayland", .{});
|
||||
const wayland_protocols_dep_ = b.lazyDependency(
|
||||
"wayland_protocols",
|
||||
.{},
|
||||
);
|
||||
const plasma_wayland_protocols_dep_ = b.lazyDependency(
|
||||
"plasma_wayland_protocols",
|
||||
.{},
|
||||
);
|
||||
|
||||
// Unwrap or return, there are no more dependencies below.
|
||||
const wayland_dep = wayland_dep_ orelse break :wayland;
|
||||
const wayland_protocols_dep = wayland_protocols_dep_ orelse break :wayland;
|
||||
const plasma_wayland_protocols_dep = plasma_wayland_protocols_dep_ orelse break :wayland;
|
||||
|
||||
// Note that zig_wayland cannot be lazy because lazy dependencies
|
||||
// can't be imported since they don't exist and imports are
|
||||
// resolved at compile time of the build.
|
||||
const zig_wayland_dep = b.dependency("zig_wayland", .{});
|
||||
const Scanner = @import("zig_wayland").Scanner;
|
||||
const scanner = Scanner.create(zig_wayland_dep.builder, .{
|
||||
.wayland_xml = wayland_dep.path("protocol/wayland.xml"),
|
||||
.wayland_protocols = wayland_protocols_dep.path(""),
|
||||
});
|
||||
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/blur.xml"),
|
||||
);
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"),
|
||||
);
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/slide.xml"),
|
||||
);
|
||||
scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml");
|
||||
|
||||
scanner.generate("wl_compositor", 1);
|
||||
scanner.generate("org_kde_kwin_blur_manager", 1);
|
||||
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
|
||||
scanner.generate("org_kde_kwin_slide_manager", 1);
|
||||
scanner.generate("xdg_activation_v1", 1);
|
||||
|
||||
step.root_module.addImport("wayland", b.createModule(.{
|
||||
.root_source_file = scanner.result,
|
||||
}));
|
||||
if (gobject_) |gobject| step.root_module.addImport(
|
||||
"gdk_wayland",
|
||||
gobject.module("gdkwayland4"),
|
||||
);
|
||||
|
||||
if (b.lazyDependency("gtk4_layer_shell", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |gtk4_layer_shell| {
|
||||
const layer_shell_module = gtk4_layer_shell.module("gtk4-layer-shell");
|
||||
if (gobject_) |gobject| layer_shell_module.addImport(
|
||||
"gtk",
|
||||
gobject.module("gtk4"),
|
||||
);
|
||||
step.root_module.addImport(
|
||||
"gtk4-layer-shell",
|
||||
layer_shell_module,
|
||||
);
|
||||
|
||||
// IMPORTANT: gtk4-layer-shell must be linked BEFORE
|
||||
// wayland-client, as it relies on shimming libwayland's APIs.
|
||||
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
|
||||
step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
|
||||
} else {
|
||||
// gtk4-layer-shell *must* be dynamically linked,
|
||||
// so we don't add it as a static library
|
||||
const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
|
||||
b.installArtifact(shared_lib);
|
||||
step.linkLibrary(shared_lib);
|
||||
}
|
||||
}
|
||||
|
||||
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
|
||||
}
|
||||
|
||||
{
|
||||
// Get our gresource c/h files and add them to our build.
|
||||
const dist = gtkDistResources(b);
|
||||
step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
|
||||
step.addIncludePath(dist.resources_h.path(b).dirname());
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the resources that can be prebuilt for our dist build.
|
||||
pub fn gtkDistResources(
|
||||
b: *std.Build,
|
||||
) struct {
|
||||
resources_c: DistResource,
|
||||
resources_h: DistResource,
|
||||
} {
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
|
||||
const gresource_xml = gresource_xml: {
|
||||
const xml_exe = b.addExecutable(.{
|
||||
.name = "generate_gresource_xml",
|
||||
.root_source_file = b.path("src/apprt/gtk/gresource.zig"),
|
||||
.target = b.graph.host,
|
||||
});
|
||||
const xml_run = b.addRunArtifact(xml_exe);
|
||||
|
||||
const blueprint_exe = b.addExecutable(.{
|
||||
.name = "gtk_blueprint_compiler",
|
||||
.root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"),
|
||||
.target = b.graph.host,
|
||||
});
|
||||
blueprint_exe.linkLibC();
|
||||
blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||
blueprint_exe.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
|
||||
|
||||
for (gresource.blueprint_files) |blueprint_file| {
|
||||
const blueprint_run = b.addRunArtifact(blueprint_exe);
|
||||
blueprint_run.addArgs(&.{
|
||||
b.fmt("{d}", .{blueprint_file.major}),
|
||||
b.fmt("{d}", .{blueprint_file.minor}),
|
||||
});
|
||||
const ui_file = blueprint_run.addOutputFileArg(b.fmt(
|
||||
"{d}.{d}/{s}.ui",
|
||||
.{
|
||||
blueprint_file.major,
|
||||
blueprint_file.minor,
|
||||
blueprint_file.name,
|
||||
},
|
||||
));
|
||||
blueprint_run.addFileArg(b.path(b.fmt(
|
||||
"src/apprt/gtk/ui/{d}.{d}/{s}.blp",
|
||||
.{
|
||||
blueprint_file.major,
|
||||
blueprint_file.minor,
|
||||
blueprint_file.name,
|
||||
},
|
||||
)));
|
||||
|
||||
xml_run.addFileArg(ui_file);
|
||||
}
|
||||
|
||||
break :gresource_xml xml_run.captureStdOut();
|
||||
};
|
||||
|
||||
const generate_c = b.addSystemCommand(&.{
|
||||
"glib-compile-resources",
|
||||
"--c-name",
|
||||
"ghostty",
|
||||
"--generate-source",
|
||||
"--target",
|
||||
});
|
||||
const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
|
||||
generate_c.addFileArg(gresource_xml);
|
||||
for (gresource.dependencies) |file| {
|
||||
generate_c.addFileInput(b.path(file));
|
||||
}
|
||||
|
||||
const generate_h = b.addSystemCommand(&.{
|
||||
"glib-compile-resources",
|
||||
"--c-name",
|
||||
"ghostty",
|
||||
"--generate-header",
|
||||
"--target",
|
||||
});
|
||||
const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
|
||||
generate_h.addFileArg(gresource_xml);
|
||||
for (gresource.dependencies) |file| {
|
||||
generate_h.addFileInput(b.path(file));
|
||||
}
|
||||
|
||||
return .{
|
||||
.resources_c = .{
|
||||
.dist = "src/apprt/gtk/ghostty_resources.c",
|
||||
.generated = resources_c,
|
||||
},
|
||||
.resources_h = .{
|
||||
.dist = "src/apprt/gtk/ghostty_resources.h",
|
||||
.generated = resources_h,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
|
@@ -7,8 +7,8 @@ const internal_os = @import("../os/main.zig");
|
||||
const xev = @import("../global.zig").xev;
|
||||
const renderer = @import("../renderer.zig");
|
||||
|
||||
const gtk_version = @import("../apprt/gtk/gtk_version.zig");
|
||||
const adw_version = @import("../apprt/gtk/adw_version.zig");
|
||||
const gtk_version = @import("../apprt/gtk-ng/gtk_version.zig");
|
||||
const adw_version = @import("../apprt/gtk-ng/adw_version.zig");
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
try stdout.print(" - font engine : {}\n", .{build_config.font_backend});
|
||||
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
|
||||
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
|
||||
if (comptime build_config.app_runtime == .gtk) {
|
||||
if (comptime build_config.app_runtime == .@"gtk-ng") {
|
||||
if (comptime builtin.os.tag == .linux) {
|
||||
const kernel_info = internal_os.getKernelInfo(alloc);
|
||||
defer if (kernel_info) |k| alloc.free(k);
|
||||
|
@@ -7149,7 +7149,7 @@ pub const GtkTitlebarStyle = enum(c_int) {
|
||||
tabs,
|
||||
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
GtkTitlebarStyle,
|
||||
.{ .name = "GhosttyGtkTitlebarStyle" },
|
||||
),
|
||||
@@ -7717,7 +7717,7 @@ pub const WindowDecoration = enum(c_int) {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
WindowDecoration,
|
||||
.{ .name = "GhosttyConfigWindowDecoration" },
|
||||
),
|
||||
|
@@ -1266,7 +1266,7 @@ pub fn SplitTree(comptime V: type) type {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
Self,
|
||||
.{
|
||||
// To get the type name we get the non-qualified type name
|
||||
|
@@ -59,7 +59,7 @@ pub const DesiredSize = struct {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
DesiredSize,
|
||||
.{ .name = "GhosttyFontDesiredSize" },
|
||||
),
|
||||
|
@@ -744,7 +744,7 @@ pub const Action = union(enum) {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||
Action,
|
||||
.{ .name = "GhosttyBindingAction" },
|
||||
),
|
||||
|
@@ -165,7 +165,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
// GTK uses global OpenGL context so we load from null.
|
||||
apprt.gtk,
|
||||
apprt.gtk_ng,
|
||||
=> try prepareContext(null),
|
||||
|
||||
@@ -201,7 +200,7 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
apprt.gtk, apprt.gtk_ng => {
|
||||
apprt.gtk_ng => {
|
||||
// GTK doesn't support threaded OpenGL operations as far as I can
|
||||
// tell, so we use the renderer thread to setup all the state
|
||||
// but then do the actual draws and texture syncs and all that
|
||||
@@ -223,7 +222,7 @@ pub fn threadExit(self: *const OpenGL) void {
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
apprt.gtk, apprt.gtk_ng => {
|
||||
apprt.gtk_ng => {
|
||||
// We don't need to do any unloading for GTK because we may
|
||||
// be sharing the global bindings with other windows.
|
||||
},
|
||||
@@ -238,7 +237,7 @@ pub fn displayRealized(self: *const OpenGL) void {
|
||||
_ = self;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
apprt.gtk, apprt.gtk_ng => prepareContext(null) catch |err| {
|
||||
apprt.gtk_ng => prepareContext(null) catch |err| {
|
||||
log.warn(
|
||||
"Error preparing GL context in displayRealized, err={}",
|
||||
.{err},
|
||||
|
@@ -49,7 +49,7 @@ pub const MouseShape = enum(c_int) {
|
||||
|
||||
/// Make this a valid gobject if we're in a GTK environment.
|
||||
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
.@"gtk-ng" => @import("gobject").ext.defineEnum(
|
||||
MouseShape,
|
||||
.{ .name = "GhosttyMouseShape" },
|
||||
),
|
||||
|
Reference in New Issue
Block a user