mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into main
This commit is contained in:
@ -70,8 +70,10 @@ parts:
|
|||||||
plugin: nil
|
plugin: nil
|
||||||
build-attributes: [enable-patchelf]
|
build-attributes: [enable-patchelf]
|
||||||
build-packages:
|
build-packages:
|
||||||
|
- blueprint-compiler
|
||||||
- libgtk-4-dev
|
- libgtk-4-dev
|
||||||
- libadwaita-1-dev
|
- libadwaita-1-dev
|
||||||
|
- libxml2-utils
|
||||||
- git
|
- git
|
||||||
- patchelf
|
- patchelf
|
||||||
override-build: |
|
override-build: |
|
||||||
|
@ -58,12 +58,6 @@ single_instance: bool,
|
|||||||
/// The "none" cursor. We use one that is shared across the entire app.
|
/// The "none" cursor. We use one that is shared across the entire app.
|
||||||
cursor_none: ?*c.GdkCursor,
|
cursor_none: ?*c.GdkCursor,
|
||||||
|
|
||||||
/// The shared application menu.
|
|
||||||
menu: ?*c.GMenu = null,
|
|
||||||
|
|
||||||
/// The shared context menu.
|
|
||||||
context_menu: ?*c.GMenu = null,
|
|
||||||
|
|
||||||
/// The configuration errors window, if it is currently open.
|
/// The configuration errors window, if it is currently open.
|
||||||
config_errors_window: ?*ConfigErrorsWindow = null,
|
config_errors_window: ?*ConfigErrorsWindow = null,
|
||||||
|
|
||||||
@ -448,8 +442,6 @@ pub fn terminate(self: *App) void {
|
|||||||
c.g_object_unref(self.app);
|
c.g_object_unref(self.app);
|
||||||
|
|
||||||
if (self.cursor_none) |cursor| c.g_object_unref(cursor);
|
if (self.cursor_none) |cursor| c.g_object_unref(cursor);
|
||||||
if (self.menu) |menu| c.g_object_unref(menu);
|
|
||||||
if (self.context_menu) |context_menu| c.g_object_unref(context_menu);
|
|
||||||
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
|
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
|
||||||
|
|
||||||
for (self.custom_css_providers.items) |provider| {
|
for (self.custom_css_providers.items) |provider| {
|
||||||
@ -478,7 +470,6 @@ pub fn performAction(
|
|||||||
}),
|
}),
|
||||||
.toggle_maximize => self.toggleMaximize(target),
|
.toggle_maximize => self.toggleMaximize(target),
|
||||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||||
|
|
||||||
.new_tab => try self.newTab(target),
|
.new_tab => try self.newTab(target),
|
||||||
.close_tab => try self.closeTab(target),
|
.close_tab => try self.closeTab(target),
|
||||||
.goto_tab => return self.gotoTab(target, value),
|
.goto_tab => return self.gotoTab(target, value),
|
||||||
@ -504,6 +495,7 @@ pub fn performAction(
|
|||||||
.toggle_split_zoom => self.toggleSplitZoom(target),
|
.toggle_split_zoom => self.toggleSplitZoom(target),
|
||||||
.toggle_window_decorations => self.toggleWindowDecorations(target),
|
.toggle_window_decorations => self.toggleWindowDecorations(target),
|
||||||
.quit_timer => self.quitTimer(value),
|
.quit_timer => self.quitTimer(value),
|
||||||
|
.prompt_title => try self.promptTitle(target),
|
||||||
|
|
||||||
// Unimplemented
|
// Unimplemented
|
||||||
.close_all_windows,
|
.close_all_windows,
|
||||||
@ -515,7 +507,6 @@ pub fn performAction(
|
|||||||
.render_inspector,
|
.render_inspector,
|
||||||
.renderer_health,
|
.renderer_health,
|
||||||
.color_change,
|
.color_change,
|
||||||
.prompt_title,
|
|
||||||
=> {
|
=> {
|
||||||
log.warn("unimplemented action={}", .{action});
|
log.warn("unimplemented action={}", .{action});
|
||||||
return false;
|
return false;
|
||||||
@ -779,6 +770,15 @@ fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn promptTitle(_: *App, target: apprt.Target) !void {
|
||||||
|
switch (target) {
|
||||||
|
.app => {},
|
||||||
|
.surface => |v| {
|
||||||
|
try v.rt_surface.promptTitle();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn setTitle(
|
fn setTitle(
|
||||||
_: *App,
|
_: *App,
|
||||||
target: apprt.Target,
|
target: apprt.Target,
|
||||||
@ -786,7 +786,7 @@ fn setTitle(
|
|||||||
) !void {
|
) !void {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
.app => {},
|
.app => {},
|
||||||
.surface => |v| try v.rt_surface.setTitle(title.title),
|
.surface => |v| try v.rt_surface.setTitle(title.title, .terminal),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -914,6 +914,9 @@ fn configChange(
|
|||||||
) void {
|
) void {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
.surface => |surface| surface: {
|
.surface => |surface| surface: {
|
||||||
|
surface.rt_surface.updateConfig(new_config) catch |err| {
|
||||||
|
log.err("unable to update surface config: {}", .{err});
|
||||||
|
};
|
||||||
const window = surface.rt_surface.container.window() orelse break :surface;
|
const window = surface.rt_surface.container.window() orelse break :surface;
|
||||||
window.updateConfig(new_config) catch |err| {
|
window.updateConfig(new_config) catch |err| {
|
||||||
log.warn("error updating config for window err={}", .{err});
|
log.warn("error updating config for window err={}", .{err});
|
||||||
@ -1012,17 +1015,20 @@ fn syncActionAccelerators(self: *App) !void {
|
|||||||
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
|
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
|
||||||
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
|
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
|
||||||
try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
|
try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
|
||||||
try self.syncActionAccelerator("win.toggle_inspector", .{ .inspector = .toggle });
|
try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
|
||||||
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
|
try self.syncActionAccelerator("win.close", .{ .close_window = {} });
|
||||||
try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
|
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
|
||||||
try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
|
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
|
||||||
try self.syncActionAccelerator("win.split_right", .{ .new_split = .right });
|
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
|
||||||
try self.syncActionAccelerator("win.split_down", .{ .new_split = .down });
|
try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
|
||||||
try self.syncActionAccelerator("win.split_left", .{ .new_split = .left });
|
try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
|
||||||
try self.syncActionAccelerator("win.split_up", .{ .new_split = .up });
|
try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
|
||||||
|
try self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
|
||||||
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
|
try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
|
||||||
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
|
try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
|
||||||
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
|
try self.syncActionAccelerator("win.reset", .{ .reset = {} });
|
||||||
|
try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
|
||||||
|
try self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn syncActionAccelerator(
|
fn syncActionAccelerator(
|
||||||
@ -1254,10 +1260,8 @@ pub fn run(self: *App) !void {
|
|||||||
// and asynchronously request the initial color scheme
|
// and asynchronously request the initial color scheme
|
||||||
self.initDbus();
|
self.initDbus();
|
||||||
|
|
||||||
// Setup our menu items
|
// Setup our actions
|
||||||
self.initActions();
|
self.initActions();
|
||||||
self.initMenu();
|
|
||||||
self.initContextMenu();
|
|
||||||
|
|
||||||
// On startup, we want to check for configuration errors right away
|
// On startup, we want to check for configuration errors right away
|
||||||
// so we can show our error window. We also need to setup other initial
|
// so we can show our error window. We also need to setup other initial
|
||||||
@ -1775,87 +1779,6 @@ fn initActions(self: *App) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes and populates the provided GMenu with sections and actions.
|
|
||||||
/// This function is used to set up the application's menu structure, either for
|
|
||||||
/// the main menu button or as a context menu when window decorations are disabled.
|
|
||||||
fn initMenuContent(menu: *c.GMenu) void {
|
|
||||||
{
|
|
||||||
const section = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(section);
|
|
||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
c.g_menu_append(section, "New Window", "win.new_window");
|
|
||||||
c.g_menu_append(section, "New Tab", "win.new_tab");
|
|
||||||
c.g_menu_append(section, "Close Tab", "win.close_tab");
|
|
||||||
c.g_menu_append(section, "Split Right", "win.split_right");
|
|
||||||
c.g_menu_append(section, "Split Down", "win.split_down");
|
|
||||||
c.g_menu_append(section, "Close Window", "win.close");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const section = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(section);
|
|
||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
|
||||||
c.g_menu_append(section, "Open Configuration", "app.open-config");
|
|
||||||
c.g_menu_append(section, "Reload Configuration", "app.reload-config");
|
|
||||||
c.g_menu_append(section, "About Ghostty", "win.about");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This sets the self.menu property to the application menu that can be
|
|
||||||
/// shared by all application windows.
|
|
||||||
fn initMenu(self: *App) void {
|
|
||||||
const menu = c.g_menu_new();
|
|
||||||
errdefer c.g_object_unref(menu);
|
|
||||||
initMenuContent(@ptrCast(menu));
|
|
||||||
self.menu = menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initContextMenu(self: *App) void {
|
|
||||||
const menu = c.g_menu_new();
|
|
||||||
errdefer c.g_object_unref(menu);
|
|
||||||
|
|
||||||
{
|
|
||||||
const section = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(section);
|
|
||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
c.g_menu_append(section, "Copy", "win.copy");
|
|
||||||
c.g_menu_append(section, "Paste", "win.paste");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const section = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(section);
|
|
||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
c.g_menu_append(section, "Split Right", "win.split_right");
|
|
||||||
c.g_menu_append(section, "Split Down", "win.split_down");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const section = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(section);
|
|
||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
c.g_menu_append(section, "Reset", "win.reset");
|
|
||||||
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
|
||||||
}
|
|
||||||
|
|
||||||
const section = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(section);
|
|
||||||
const submenu = c.g_menu_new();
|
|
||||||
defer c.g_object_unref(submenu);
|
|
||||||
|
|
||||||
initMenuContent(@ptrCast(submenu));
|
|
||||||
c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
|
|
||||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
|
||||||
|
|
||||||
self.context_menu = menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
|
|
||||||
const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
|
|
||||||
c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isValidAppId(app_id: [:0]const u8) bool {
|
fn isValidAppId(app_id: [:0]const u8) bool {
|
||||||
if (app_id.len > 255 or app_id.len == 0) return false;
|
if (app_id.len > 255 or app_id.len == 0) return false;
|
||||||
if (app_id[0] == '.') return false;
|
if (app_id[0] == '.') return false;
|
||||||
|
@ -25,7 +25,7 @@ pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder
|
|||||||
// GResource.
|
// GResource.
|
||||||
const gresource = @import("gresource.zig");
|
const gresource = @import("gresource.zig");
|
||||||
for (gresource.blueprint_files) |blueprint_file| {
|
for (gresource.blueprint_files) |blueprint_file| {
|
||||||
if (std.mem.eql(u8, blueprint_file, name)) break;
|
if (std.mem.eql(u8, blueprint_file.name, name)) break;
|
||||||
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
||||||
},
|
},
|
||||||
.ui => {
|
.ui => {
|
||||||
@ -56,7 +56,7 @@ pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) voi
|
|||||||
class.setTemplateFromResource(self.resource_name);
|
class.setTemplateFromResource(self.resource_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object {
|
pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T {
|
||||||
const builder = builder: {
|
const builder = builder: {
|
||||||
if (self.builder) |builder| break :builder builder;
|
if (self.builder) |builder| break :builder builder;
|
||||||
const builder = gtk.Builder.newFromResource(self.resource_name);
|
const builder = gtk.Builder.newFromResource(self.resource_name);
|
||||||
@ -64,7 +64,7 @@ pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object {
|
|||||||
break :builder builder;
|
break :builder builder;
|
||||||
};
|
};
|
||||||
|
|
||||||
return builder.getObject(name);
|
return gobject.ext.cast(T, builder.getObject(name) orelse return null);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Builder) void {
|
pub fn deinit(self: *const Builder) void {
|
||||||
|
@ -1,24 +1,45 @@
|
|||||||
const ResizeOverlay = @This();
|
const ResizeOverlay = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const c = @import("c.zig").c;
|
|
||||||
|
const glib = @import("glib");
|
||||||
|
const gtk = @import("gtk");
|
||||||
|
|
||||||
const configpkg = @import("../../config.zig");
|
const configpkg = @import("../../config.zig");
|
||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk);
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
/// Back reference to the surface we belong to
|
/// local copy of configuration data
|
||||||
surface: ?*Surface = null,
|
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
|
/// If non-null this is the widget on the overlay that shows the size of the
|
||||||
/// surface when it is resized.
|
/// surface when it is resized.
|
||||||
widget: ?*c.GtkWidget = null,
|
label: ?*gtk.Label = null,
|
||||||
|
|
||||||
/// If non-null this is a timer for dismissing the resize overlay.
|
/// If non-null this is a timer for dismissing the resize overlay.
|
||||||
timer: ?c.guint = null,
|
timer: ?c_uint = null,
|
||||||
|
|
||||||
/// If non-null this is a timer for dismissing the resize overlay.
|
/// If non-null this is a timer for dismissing the resize overlay.
|
||||||
idler: ?c.guint = null,
|
idler: ?c_uint = null,
|
||||||
|
|
||||||
/// If true, the next resize event will be the first one.
|
/// If true, the next resize event will be the first one.
|
||||||
first: bool = true,
|
first: bool = true,
|
||||||
@ -26,24 +47,29 @@ first: bool = true,
|
|||||||
/// Initialize the ResizeOverlay. This doesn't do anything more than save a
|
/// 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
|
/// pointer to the surface that we are a part of as all of the widget creation
|
||||||
/// is done later.
|
/// is done later.
|
||||||
pub fn init(surface: *Surface) ResizeOverlay {
|
pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
|
||||||
return .{
|
self.* = .{
|
||||||
.surface = surface,
|
.surface = surface,
|
||||||
|
.config = DerivedConfig.init(config),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
|
||||||
|
self.config = DerivedConfig.init(config);
|
||||||
|
}
|
||||||
|
|
||||||
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
|
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
|
||||||
/// may not have fired yet.
|
/// may not have fired yet.
|
||||||
pub fn deinit(self: *ResizeOverlay) void {
|
pub fn deinit(self: *ResizeOverlay) void {
|
||||||
if (self.idler) |idler| {
|
if (self.idler) |idler| {
|
||||||
if (c.g_source_remove(idler) == c.FALSE) {
|
if (glib.Source.remove(idler) == 0) {
|
||||||
log.warn("unable to remove resize overlay idler", .{});
|
log.warn("unable to remove resize overlay idler", .{});
|
||||||
}
|
}
|
||||||
self.idler = null;
|
self.idler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.timer) |timer| {
|
if (self.timer) |timer| {
|
||||||
if (c.g_source_remove(timer) == c.FALSE) {
|
if (glib.Source.remove(timer) == 0) {
|
||||||
log.warn("unable to remove resize overlay timer", .{});
|
log.warn("unable to remove resize overlay timer", .{});
|
||||||
}
|
}
|
||||||
self.timer = null;
|
self.timer = null;
|
||||||
@ -56,12 +82,7 @@ pub fn deinit(self: *ResizeOverlay) void {
|
|||||||
///
|
///
|
||||||
/// If we're not configured to show the overlay, do nothing.
|
/// If we're not configured to show the overlay, do nothing.
|
||||||
pub fn maybeShow(self: *ResizeOverlay) void {
|
pub fn maybeShow(self: *ResizeOverlay) void {
|
||||||
const surface = self.surface orelse {
|
switch (self.config.resize_overlay) {
|
||||||
log.err("resize overlay configured without a surface", .{});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (surface.app.config.@"resize-overlay") {
|
|
||||||
.never => return,
|
.never => return,
|
||||||
.always => {},
|
.always => {},
|
||||||
.@"after-first" => if (self.first) {
|
.@"after-first" => if (self.first) {
|
||||||
@ -78,23 +99,18 @@ pub fn maybeShow(self: *ResizeOverlay) void {
|
|||||||
// results in a lot of warnings from GTK and _horrible_ flickering of the
|
// results in a lot of warnings from GTK and _horrible_ flickering of the
|
||||||
// resize overlay.
|
// resize overlay.
|
||||||
if (self.idler != null) return;
|
if (self.idler != null) return;
|
||||||
self.idler = c.g_idle_add(gtkUpdate, @ptrCast(self));
|
self.idler = glib.idleAdd(gtkUpdate, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actually update the overlay widget. This should only be called from a GTK
|
/// Actually update the overlay widget. This should only be called from a GTK
|
||||||
/// idle handler.
|
/// idle handler.
|
||||||
fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c.gboolean {
|
fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c_int {
|
||||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud));
|
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||||
|
|
||||||
// No matter what our idler is complete with this callback
|
// No matter what our idler is complete with this callback
|
||||||
self.idler = null;
|
self.idler = null;
|
||||||
|
|
||||||
const surface = self.surface orelse {
|
const grid_size = self.surface.core_surface.size.grid();
|
||||||
log.err("resize overlay configured without a surface", .{});
|
|
||||||
return c.FALSE;
|
|
||||||
};
|
|
||||||
|
|
||||||
const grid_size = surface.core_surface.size.grid();
|
|
||||||
var buf: [32]u8 = undefined;
|
var buf: [32]u8 = undefined;
|
||||||
const text = std.fmt.bufPrintZ(
|
const text = std.fmt.bufPrintZ(
|
||||||
&buf,
|
&buf,
|
||||||
@ -105,88 +121,86 @@ fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c.gboolean {
|
|||||||
},
|
},
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err("unable to format text: {}", .{err});
|
log.err("unable to format text: {}", .{err});
|
||||||
return c.FALSE;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (self.widget) |widget| {
|
if (self.label) |label| {
|
||||||
// The resize overlay widget already exists, just update it.
|
// The resize overlay widget already exists, just update it.
|
||||||
c.gtk_label_set_text(@ptrCast(widget), text.ptr);
|
label.setText(text.ptr);
|
||||||
setPosition(widget, &surface.app.config);
|
setPosition(label, &self.config);
|
||||||
show(widget);
|
show(label);
|
||||||
} else {
|
} else {
|
||||||
// Create the resize overlay widget.
|
// Create the resize overlay widget.
|
||||||
const widget = c.gtk_label_new(text.ptr);
|
const label = gtk.Label.new(text.ptr);
|
||||||
|
label.setJustify(gtk.Justification.center);
|
||||||
|
label.setSelectable(0);
|
||||||
|
setPosition(label, &self.config);
|
||||||
|
|
||||||
c.gtk_widget_add_css_class(widget, "view");
|
const widget = label.as(gtk.Widget);
|
||||||
c.gtk_widget_add_css_class(widget, "size-overlay");
|
widget.addCssClass("view");
|
||||||
c.gtk_widget_set_focusable(widget, c.FALSE);
|
widget.addCssClass("size-overlay");
|
||||||
c.gtk_widget_set_can_target(widget, c.FALSE);
|
widget.setFocusable(0);
|
||||||
c.gtk_label_set_justify(@ptrCast(widget), c.GTK_JUSTIFY_CENTER);
|
widget.setCanTarget(0);
|
||||||
c.gtk_label_set_selectable(@ptrCast(widget), c.FALSE);
|
|
||||||
setPosition(widget, &surface.app.config);
|
|
||||||
|
|
||||||
c.gtk_overlay_add_overlay(surface.overlay, widget);
|
const overlay: *gtk.Overlay = @ptrCast(@alignCast(self.surface.overlay));
|
||||||
|
overlay.addOverlay(widget);
|
||||||
|
|
||||||
self.widget = widget;
|
self.label = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.timer) |timer| {
|
if (self.timer) |timer| {
|
||||||
if (c.g_source_remove(timer) == c.FALSE) {
|
if (glib.Source.remove(timer) == 0) {
|
||||||
log.warn("unable to remove size overlay timer", .{});
|
log.warn("unable to remove size overlay timer", .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.timer = c.g_timeout_add(
|
|
||||||
surface.app.config.@"resize-overlay-duration".asMilliseconds(),
|
self.timer = glib.timeoutAdd(
|
||||||
|
self.surface.app.config.@"resize-overlay-duration".asMilliseconds(),
|
||||||
gtkTimerExpired,
|
gtkTimerExpired,
|
||||||
@ptrCast(self),
|
self,
|
||||||
);
|
);
|
||||||
|
|
||||||
return c.FALSE;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should only be called from a GTK idle handler or timer.
|
// This should only be called from a GTK idle handler or timer.
|
||||||
fn show(widget: *c.GtkWidget) void {
|
fn show(label: *gtk.Label) void {
|
||||||
// The CSS class is used only by libadwaita.
|
const widget = label.as(gtk.Widget);
|
||||||
c.gtk_widget_remove_css_class(@ptrCast(widget), "hidden");
|
widget.removeCssClass("hidden");
|
||||||
// Set the visibility for non-libadwaita usage.
|
|
||||||
c.gtk_widget_set_visible(@ptrCast(widget), 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should only be called from a GTK idle handler or timer.
|
// This should only be called from a GTK idle handler or timer.
|
||||||
fn hide(widget: *c.GtkWidget) void {
|
fn hide(label: *gtk.Label) void {
|
||||||
// The CSS class is used only by libadwaita.
|
const widget = label.as(gtk.Widget);
|
||||||
c.gtk_widget_add_css_class(widget, "hidden");
|
widget.addCssClass("hidden");
|
||||||
// Set the visibility for non-libadwaita usage.
|
|
||||||
c.gtk_widget_set_visible(widget, c.FALSE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the position of the resize overlay widget. It might seem excessive to
|
/// 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.
|
/// do this often, but it should make hot config reloading of the position work.
|
||||||
/// This should only be called from a GTK idle handler.
|
/// This should only be called from a GTK idle handler.
|
||||||
fn setPosition(widget: *c.GtkWidget, config: *configpkg.Config) void {
|
fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
|
||||||
c.gtk_widget_set_halign(
|
const widget = label.as(gtk.Widget);
|
||||||
@ptrCast(widget),
|
widget.setHalign(
|
||||||
switch (config.@"resize-overlay-position") {
|
switch (config.resize_overlay_position) {
|
||||||
.center, .@"top-center", .@"bottom-center" => c.GTK_ALIGN_CENTER,
|
.center, .@"top-center", .@"bottom-center" => gtk.Align.center,
|
||||||
.@"top-left", .@"bottom-left" => c.GTK_ALIGN_START,
|
.@"top-left", .@"bottom-left" => gtk.Align.start,
|
||||||
.@"top-right", .@"bottom-right" => c.GTK_ALIGN_END,
|
.@"top-right", .@"bottom-right" => gtk.Align.end,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
c.gtk_widget_set_valign(
|
widget.setValign(
|
||||||
@ptrCast(widget),
|
switch (config.resize_overlay_position) {
|
||||||
switch (config.@"resize-overlay-position") {
|
.center => gtk.Align.center,
|
||||||
.center => c.GTK_ALIGN_CENTER,
|
.@"top-left", .@"top-center", .@"top-right" => gtk.Align.start,
|
||||||
.@"top-left", .@"top-center", .@"top-right" => c.GTK_ALIGN_START,
|
.@"bottom-left", .@"bottom-center", .@"bottom-right" => gtk.Align.end,
|
||||||
.@"bottom-left", .@"bottom-center", .@"bottom-right" => c.GTK_ALIGN_END,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this fires, it means that the delay period has expired and the resize
|
/// If this fires, it means that the delay period has expired and the resize
|
||||||
/// overlay widget should be hidden.
|
/// overlay widget should be hidden.
|
||||||
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean {
|
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.C) c_int {
|
||||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud));
|
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||||
self.timer = null;
|
self.timer = null;
|
||||||
if (self.widget) |widget| hide(widget);
|
if (self.label) |label| hide(label);
|
||||||
return c.FALSE;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
const Surface = @This();
|
const Surface = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const adw = @import("adw");
|
||||||
|
const gtk = @import("gtk");
|
||||||
|
const gio = @import("gio");
|
||||||
|
const gobject = @import("gobject");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const build_config = @import("../../build_config.zig");
|
const build_config = @import("../../build_config.zig");
|
||||||
const build_options = @import("build_options");
|
const build_options = @import("build_options");
|
||||||
@ -20,11 +24,14 @@ const App = @import("App.zig");
|
|||||||
const Split = @import("Split.zig");
|
const Split = @import("Split.zig");
|
||||||
const Tab = @import("Tab.zig");
|
const Tab = @import("Tab.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
|
const Menu = @import("menu.zig").Menu;
|
||||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||||
const ResizeOverlay = @import("ResizeOverlay.zig");
|
const ResizeOverlay = @import("ResizeOverlay.zig");
|
||||||
const inspector = @import("inspector.zig");
|
const inspector = @import("inspector.zig");
|
||||||
const gtk_key = @import("key.zig");
|
const gtk_key = @import("key.zig");
|
||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
|
const Builder = @import("Builder.zig");
|
||||||
|
const adwaita = @import("adwaita.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_surface);
|
const log = std.log.scoped(.gtk_surface);
|
||||||
|
|
||||||
@ -266,8 +273,8 @@ pub const URLWidget = struct {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Show it
|
// Show it
|
||||||
c.gtk_overlay_add_overlay(@ptrCast(surface.overlay), left);
|
c.gtk_overlay_add_overlay(surface.overlay, left);
|
||||||
c.gtk_overlay_add_overlay(@ptrCast(surface.overlay), right);
|
c.gtk_overlay_add_overlay(surface.overlay, right);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.left = left,
|
.left = left,
|
||||||
@ -276,8 +283,8 @@ pub const URLWidget = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *URLWidget, overlay: *c.GtkOverlay) void {
|
pub fn deinit(self: *URLWidget, overlay: *c.GtkOverlay) void {
|
||||||
c.gtk_overlay_remove_overlay(@ptrCast(overlay), @ptrCast(self.left));
|
c.gtk_overlay_remove_overlay(overlay, @ptrCast(self.left));
|
||||||
c.gtk_overlay_remove_overlay(@ptrCast(overlay), @ptrCast(self.right));
|
c.gtk_overlay_remove_overlay(overlay, @ptrCast(self.right));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setText(self: *const URLWidget, str: [:0]const u8) void {
|
pub fn setText(self: *const URLWidget, str: [:0]const u8) void {
|
||||||
@ -329,7 +336,7 @@ gl_area: *c.GtkGLArea,
|
|||||||
url_widget: ?URLWidget = null,
|
url_widget: ?URLWidget = null,
|
||||||
|
|
||||||
/// The overlay that shows resizing information.
|
/// The overlay that shows resizing information.
|
||||||
resize_overlay: ResizeOverlay = .{},
|
resize_overlay: ResizeOverlay = undefined,
|
||||||
|
|
||||||
/// Whether or not the current surface is zoomed in (see `toggle_split_zoom`).
|
/// Whether or not the current surface is zoomed in (see `toggle_split_zoom`).
|
||||||
zoomed_in: bool = false,
|
zoomed_in: bool = false,
|
||||||
@ -346,6 +353,12 @@ cursor: ?*c.GdkCursor = null,
|
|||||||
/// pass it to GTK.
|
/// pass it to GTK.
|
||||||
title_text: ?[:0]const u8 = null,
|
title_text: ?[:0]const u8 = null,
|
||||||
|
|
||||||
|
/// The title of the surface as reported by the terminal. If it is null, the
|
||||||
|
/// title reported by the terminal is currently being used. If the title was
|
||||||
|
/// manually overridden by the user, this will be set to a non-null value
|
||||||
|
/// representing the default terminal title.
|
||||||
|
title_from_terminal: ?[:0]const u8 = null,
|
||||||
|
|
||||||
/// Our current working directory. We use this value for setting tooltips in
|
/// Our current working directory. We use this value for setting tooltips in
|
||||||
/// the headerbar subtitle if we have focus. When set, the text in this buf
|
/// the headerbar subtitle if we have focus. When set, the text in this buf
|
||||||
/// will be null-terminated because we need to pass it to GTK.
|
/// will be null-terminated because we need to pass it to GTK.
|
||||||
@ -378,6 +391,9 @@ im_len: u7 = 0,
|
|||||||
/// details on what this is.
|
/// details on what this is.
|
||||||
cgroup_path: ?[]const u8 = null,
|
cgroup_path: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Our context menu.
|
||||||
|
context_menu: Menu(Surface, "context_menu", false),
|
||||||
|
|
||||||
/// The state of the key event while we're doing IM composition.
|
/// The state of the key event while we're doing IM composition.
|
||||||
/// See gtkKeyPressed for detailed descriptions.
|
/// See gtkKeyPressed for detailed descriptions.
|
||||||
pub const IMKeyEvent = enum {
|
pub const IMKeyEvent = enum {
|
||||||
@ -567,7 +583,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
|||||||
.container = .{ .none = {} },
|
.container = .{ .none = {} },
|
||||||
.overlay = @ptrCast(overlay),
|
.overlay = @ptrCast(overlay),
|
||||||
.gl_area = @ptrCast(gl_area),
|
.gl_area = @ptrCast(gl_area),
|
||||||
.resize_overlay = ResizeOverlay.init(self),
|
.resize_overlay = undefined,
|
||||||
.title_text = null,
|
.title_text = null,
|
||||||
.core_surface = undefined,
|
.core_surface = undefined,
|
||||||
.font_size = font_size,
|
.font_size = font_size,
|
||||||
@ -576,9 +592,17 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
|||||||
.cursor_pos = .{ .x = -1, .y = -1 },
|
.cursor_pos = .{ .x = -1, .y = -1 },
|
||||||
.im_context = im_context,
|
.im_context = im_context,
|
||||||
.cgroup_path = cgroup_path,
|
.cgroup_path = cgroup_path,
|
||||||
|
.context_menu = undefined,
|
||||||
};
|
};
|
||||||
errdefer self.* = undefined;
|
errdefer self.* = undefined;
|
||||||
|
|
||||||
|
// initialize the context menu
|
||||||
|
self.context_menu.init(self);
|
||||||
|
self.context_menu.setParent(@ptrCast(@alignCast(overlay)));
|
||||||
|
|
||||||
|
// initialize the resize overlay
|
||||||
|
self.resize_overlay.init(self, &app.config);
|
||||||
|
|
||||||
// Set our default mouse shape
|
// Set our default mouse shape
|
||||||
try self.setMouseShape(.text);
|
try self.setMouseShape(.text);
|
||||||
|
|
||||||
@ -654,6 +678,7 @@ fn realize(self: *Surface) !void {
|
|||||||
pub fn deinit(self: *Surface) void {
|
pub fn deinit(self: *Surface) void {
|
||||||
self.init_config.deinit(self.app.core_app.alloc);
|
self.init_config.deinit(self.app.core_app.alloc);
|
||||||
if (self.title_text) |title| self.app.core_app.alloc.free(title);
|
if (self.title_text) |title| self.app.core_app.alloc.free(title);
|
||||||
|
if (self.title_from_terminal) |title| self.app.core_app.alloc.free(title);
|
||||||
if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd);
|
if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd);
|
||||||
|
|
||||||
// We don't allocate anything if we aren't realized.
|
// We don't allocate anything if we aren't realized.
|
||||||
@ -682,6 +707,11 @@ pub fn deinit(self: *Surface) void {
|
|||||||
self.resize_overlay.deinit();
|
self.resize_overlay.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update our local copy of any configuration that we use.
|
||||||
|
pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void {
|
||||||
|
self.resize_overlay.updateConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
// unref removes the long-held reference to the gl_area and kicks off the
|
// unref removes the long-held reference to the gl_area and kicks off the
|
||||||
// deinit/destroy process for this surface.
|
// deinit/destroy process for this surface.
|
||||||
pub fn unref(self: *Surface) void {
|
pub fn unref(self: *Surface) void {
|
||||||
@ -817,28 +847,44 @@ pub fn shouldClose(self: *const Surface) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
||||||
// Future: detect GTK version 4.12+ and use gdk_surface_get_scale so we
|
const gtk_scale: f32 = scale: {
|
||||||
// can support fractional scaling.
|
const widget: *gtk.Widget = @ptrCast(@alignCast(self.gl_area));
|
||||||
const gtk_scale: f32 = @floatFromInt(c.gtk_widget_get_scale_factor(@ptrCast(self.gl_area)));
|
// Future: detect GTK version 4.12+ and use gdk_surface_get_scale so we
|
||||||
|
// can support fractional scaling.
|
||||||
|
const scale = widget.getScaleFactor();
|
||||||
|
if (scale <= 0) {
|
||||||
|
log.warn("gtk_widget_get_scale_factor returned a non-positive number: {}", .{scale});
|
||||||
|
break :scale 1.0;
|
||||||
|
}
|
||||||
|
break :scale @floatFromInt(scale);
|
||||||
|
};
|
||||||
|
|
||||||
// Also scale using font-specific DPI, which is often exposed to the user
|
// Also scale using font-specific DPI, which is often exposed to the user
|
||||||
// via DE accessibility settings (see https://docs.gtk.org/gtk4/class.Settings.html).
|
// via DE accessibility settings (see https://docs.gtk.org/gtk4/class.Settings.html).
|
||||||
const xft_dpi_scale = xft_scale: {
|
const xft_dpi_scale = xft_scale: {
|
||||||
// gtk-xft-dpi is font DPI multiplied by 1024. See
|
// gtk-xft-dpi is font DPI multiplied by 1024. See
|
||||||
// https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
|
// https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
|
||||||
const settings = c.gtk_settings_get_default();
|
const settings = gtk.Settings.getDefault() orelse break :xft_scale 1.0;
|
||||||
|
var value = std.mem.zeroes(gobject.Value);
|
||||||
|
defer value.unset();
|
||||||
|
_ = value.init(gobject.ext.typeFor(c_int));
|
||||||
|
settings.as(gobject.Object).getProperty("gtk-xft-dpi", &value);
|
||||||
|
const gtk_xft_dpi = value.getInt();
|
||||||
|
|
||||||
var value: c.GValue = std.mem.zeroes(c.GValue);
|
// Use a value of 1.0 for the XFT DPI scale if the setting is <= 0
|
||||||
defer c.g_value_unset(&value);
|
// See:
|
||||||
_ = c.g_value_init(&value, c.G_TYPE_INT);
|
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421
|
||||||
c.g_object_get_property(@ptrCast(@alignCast(settings)), "gtk-xft-dpi", &value);
|
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead
|
||||||
const gtk_xft_dpi = c.g_value_get_int(&value);
|
if (gtk_xft_dpi <= 0) {
|
||||||
|
log.warn("gtk-xft-dpi was not set, using default value", .{});
|
||||||
|
break :xft_scale 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
// As noted above gtk-xft-dpi is multiplied by 1024, so we divide by
|
// As noted above gtk-xft-dpi is multiplied by 1024, so we divide by
|
||||||
// 1024, then divide by the default value (96) to derive a scale. Note
|
// 1024, then divide by the default value (96) to derive a scale. Note
|
||||||
// gtk-xft-dpi can be fractional, so we use floating point math here.
|
// gtk-xft-dpi can be fractional, so we use floating point math here.
|
||||||
const xft_dpi: f32 = @as(f32, @floatFromInt(gtk_xft_dpi)) / 1024;
|
const xft_dpi: f32 = @as(f32, @floatFromInt(gtk_xft_dpi)) / 1024.0;
|
||||||
break :xft_scale xft_dpi / 96;
|
break :xft_scale xft_dpi / 96.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const scale = gtk_scale * xft_dpi_scale;
|
const scale = gtk_scale * xft_dpi_scale;
|
||||||
@ -913,7 +959,7 @@ fn updateTitleLabels(self: *Surface) void {
|
|||||||
|
|
||||||
// If we have a tab and are the focused child, then we have to update the tab
|
// If we have a tab and are the focused child, then we have to update the tab
|
||||||
if (self.container.tab()) |tab| {
|
if (self.container.tab()) |tab| {
|
||||||
if (tab.focus_child == self) tab.setLabelText(title);
|
if (tab.focus_child == self) tab.setTitleText(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a window and are focused, then we have to update the window title.
|
// If we have a window and are focused, then we have to update the window title.
|
||||||
@ -931,8 +977,9 @@ fn updateTitleLabels(self: *Surface) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const zoom_title_prefix = "🔍 ";
|
const zoom_title_prefix = "🔍 ";
|
||||||
|
pub const SetTitleSource = enum { user, terminal };
|
||||||
|
|
||||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !void {
|
||||||
const alloc = self.app.core_app.alloc;
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
// Always allocate with the "🔍 " at the beginning and slice accordingly
|
// Always allocate with the "🔍 " at the beginning and slice accordingly
|
||||||
@ -945,6 +992,14 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
|||||||
};
|
};
|
||||||
errdefer alloc.free(copy);
|
errdefer alloc.free(copy);
|
||||||
|
|
||||||
|
// The user has overridden the title
|
||||||
|
// We only want to update the terminal provided title so that it can be restored to the most recent state.
|
||||||
|
if (self.title_from_terminal != null and source == .terminal) {
|
||||||
|
alloc.free(self.title_from_terminal.?);
|
||||||
|
self.title_from_terminal = copy;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (self.title_text) |old| alloc.free(old);
|
if (self.title_text) |old| alloc.free(old);
|
||||||
self.title_text = copy;
|
self.title_text = copy;
|
||||||
|
|
||||||
@ -969,15 +1024,41 @@ fn updateTitleTimerExpired(ctx: ?*anyopaque) callconv(.C) c.gboolean {
|
|||||||
|
|
||||||
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||||
if (self.title_text) |title_text| {
|
if (self.title_text) |title_text| {
|
||||||
return if (self.zoomed_in)
|
return self.resolveTitle(title_text);
|
||||||
title_text
|
|
||||||
else
|
|
||||||
title_text[zoom_title_prefix.len..];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getTerminalTitle(self: *Surface) ?[:0]const u8 {
|
||||||
|
if (self.title_from_terminal) |title_text| {
|
||||||
|
return self.resolveTitle(title_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolveTitle(self: *Surface, title: [:0]const u8) [:0]const u8 {
|
||||||
|
return if (self.zoomed_in)
|
||||||
|
title
|
||||||
|
else
|
||||||
|
title[zoom_title_prefix.len..];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn promptTitle(self: *Surface) !void {
|
||||||
|
if (!adwaita.versionAtLeast(1, 5, 0)) return;
|
||||||
|
const window = self.container.window() orelse return;
|
||||||
|
|
||||||
|
var builder = Builder.init("prompt-title-dialog", .blp);
|
||||||
|
defer builder.deinit();
|
||||||
|
|
||||||
|
const entry = builder.getObject(gtk.Entry, "title_entry").?;
|
||||||
|
entry.getBuffer().setText(self.getTitle() orelse "", -1);
|
||||||
|
|
||||||
|
const dialog = builder.getObject(adw.AlertDialog, "prompt_title_dialog").?;
|
||||||
|
dialog.choose(@ptrCast(window.window), null, gtkPromptTitleResponse, self);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the current working directory of the surface.
|
/// Set the current working directory of the surface.
|
||||||
///
|
///
|
||||||
/// In addition, update the tab's tooltip text, and if we are the focused child,
|
/// In addition, update the tab's tooltip text, and if we are the focused child,
|
||||||
@ -1224,6 +1305,7 @@ fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboa
|
|||||||
.selection, .primary => c.gtk_widget_get_primary_clipboard(widget),
|
.selection, .primary => c.gtk_widget_get_primary_clipboard(widget),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
|
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
|
||||||
return self.cursor_pos;
|
return self.cursor_pos;
|
||||||
}
|
}
|
||||||
@ -1261,40 +1343,6 @@ pub fn showDesktopNotification(
|
|||||||
c.g_application_send_notification(g_app, body.ptr, notification);
|
c.g_application_send_notification(g_app, body.ptr, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showContextMenu(self: *Surface, x: f32, y: f32) void {
|
|
||||||
const window: *Window = self.container.window() orelse {
|
|
||||||
log.info(
|
|
||||||
"showContextMenu invalid for container={s}",
|
|
||||||
.{@tagName(self.container)},
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert surface coordinate into coordinate space of the
|
|
||||||
// context menu's parent
|
|
||||||
var point: c.graphene_point_t = .{ .x = x, .y = y };
|
|
||||||
if (c.gtk_widget_compute_point(
|
|
||||||
self.primaryWidget(),
|
|
||||||
c.gtk_widget_get_parent(@ptrCast(window.context_menu)),
|
|
||||||
&c.GRAPHENE_POINT_INIT(point.x, point.y),
|
|
||||||
@ptrCast(&point),
|
|
||||||
) == 0) {
|
|
||||||
log.warn("failed computing point for context menu", .{});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect: c.GdkRectangle = .{
|
|
||||||
.x = @intFromFloat(point.x),
|
|
||||||
.y = @intFromFloat(point.y),
|
|
||||||
.width = 1,
|
|
||||||
.height = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect);
|
|
||||||
self.app.refreshContextMenu(window.window, self.core_surface.hasSelection());
|
|
||||||
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
||||||
log.debug("gl surface realized", .{});
|
log.debug("gl surface realized", .{});
|
||||||
|
|
||||||
@ -1465,7 +1513,7 @@ fn gtkMouseDown(
|
|||||||
// word and returns false. We can use this to handle the context menu
|
// word and returns false. We can use this to handle the context menu
|
||||||
// opening under normal scenarios.
|
// opening under normal scenarios.
|
||||||
if (!consumed and button == .right) {
|
if (!consumed and button == .right) {
|
||||||
self.showContextMenu(@floatCast(x), @floatCast(y));
|
self.context_menu.popupAt(@intFromFloat(x), @intFromFloat(y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2073,15 +2121,14 @@ fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo
|
|||||||
/// Adds the unfocused_widget to the overlay. If the unfocused_widget has already been added, this
|
/// Adds the unfocused_widget to the overlay. If the unfocused_widget has already been added, this
|
||||||
/// is a no-op
|
/// is a no-op
|
||||||
pub fn dimSurface(self: *Surface) void {
|
pub fn dimSurface(self: *Surface) void {
|
||||||
const window = self.container.window() orelse {
|
_ = self.container.window() orelse {
|
||||||
log.warn("dimSurface invalid for container={}", .{self.container});
|
log.warn("dimSurface invalid for container={}", .{self.container});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't dim surface if context menu is open.
|
// Don't dim surface if context menu is open.
|
||||||
// This means we got unfocused due to it opening.
|
// This means we got unfocused due to it opening.
|
||||||
const context_menu_open = c.gtk_widget_get_visible(window.context_menu);
|
if (self.context_menu.isVisible()) return;
|
||||||
if (context_menu_open == 1) return;
|
|
||||||
|
|
||||||
if (self.unfocused_widget != null) return;
|
if (self.unfocused_widget != null) return;
|
||||||
self.unfocused_widget = c.gtk_drawing_area_new();
|
self.unfocused_widget = c.gtk_drawing_area_new();
|
||||||
@ -2298,3 +2345,40 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
if (!adwaita.versionAtLeast(1, 5, 0)) return;
|
||||||
|
const dialog = gobject.ext.cast(adw.AlertDialog, source_object.?).?;
|
||||||
|
const self = userdataSelf(ud orelse return);
|
||||||
|
|
||||||
|
const response = dialog.chooseFinish(result);
|
||||||
|
if (std.mem.orderZ(u8, "ok", response) == .eq) {
|
||||||
|
const title_entry = gobject.ext.cast(gtk.Entry, dialog.getExtraChild().?).?;
|
||||||
|
const title = std.mem.span(title_entry.getBuffer().getText());
|
||||||
|
|
||||||
|
// if the new title is empty and the user has set the title previously, restore the terminal provided title
|
||||||
|
if (title.len == 0) {
|
||||||
|
if (self.getTerminalTitle()) |terminal_title| {
|
||||||
|
self.setTitle(terminal_title, .user) catch |err| {
|
||||||
|
log.err("failed to set title={}", .{err});
|
||||||
|
};
|
||||||
|
self.app.core_app.alloc.free(self.title_from_terminal.?);
|
||||||
|
self.title_from_terminal = null;
|
||||||
|
}
|
||||||
|
} else if (title.len > 0) {
|
||||||
|
// if this is the first time the user is setting the title, save the current terminal provided title
|
||||||
|
if (self.title_from_terminal == null and self.title_text != null) {
|
||||||
|
self.title_from_terminal = self.app.core_app.alloc.dupeZ(u8, self.title_text.?) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => {
|
||||||
|
log.err("failed to allocate memory for title={}", .{err});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setTitle(title, .user) catch |err| {
|
||||||
|
log.err("failed to set title={}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -108,8 +108,8 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
|
|||||||
self.elem = elem;
|
self.elem = elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setLabelText(self: *Tab, title: [:0]const u8) void {
|
pub fn setTitleText(self: *Tab, title: [:0]const u8) void {
|
||||||
self.window.notebook.setTabLabel(self, title);
|
self.window.notebook.setTabTitle(self, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
|
pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
|
||||||
|
@ -165,7 +165,7 @@ pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
|||||||
_ = self.tab_view.reorderPage(page, position);
|
_ = self.tab_view.reorderPage(page, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setTabLabel(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||||
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
const page = self.tab_view.getPage(@ptrCast(tab.box));
|
||||||
page.setTitle(title.ptr);
|
page.setTitle(title.ptr);
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
|||||||
const position = self.newTabInsertPosition(tab);
|
const position = self.newTabInsertPosition(tab);
|
||||||
const box_widget: *gtk.Widget = @ptrCast(tab.box);
|
const box_widget: *gtk.Widget = @ptrCast(tab.box);
|
||||||
const page = self.tab_view.insert(box_widget, position);
|
const page = self.tab_view.insert(box_widget, position);
|
||||||
self.setTabLabel(tab, title);
|
self.setTabTitle(tab, title);
|
||||||
self.tab_view.setSelectedPage(page);
|
self.tab_view.setSelectedPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ const CoreSurface = @import("../../Surface.zig");
|
|||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const Color = configpkg.Config.Color;
|
const Color = configpkg.Config.Color;
|
||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
|
const Menu = @import("menu.zig").Menu;
|
||||||
const Tab = @import("Tab.zig");
|
const Tab = @import("Tab.zig");
|
||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
const adwaita = @import("adwaita.zig");
|
const adwaita = @import("adwaita.zig");
|
||||||
@ -31,6 +32,10 @@ const log = std.log.scoped(.gtk);
|
|||||||
|
|
||||||
app: *App,
|
app: *App,
|
||||||
|
|
||||||
|
/// Used to deduplicate updateConfig invocations
|
||||||
|
last_config: usize,
|
||||||
|
|
||||||
|
/// Local copy of any configuration
|
||||||
config: DerivedConfig,
|
config: DerivedConfig,
|
||||||
|
|
||||||
/// Our window
|
/// Our window
|
||||||
@ -46,7 +51,8 @@ tab_overview: ?*c.GtkWidget,
|
|||||||
/// The notebook (tab grouping) for this window.
|
/// The notebook (tab grouping) for this window.
|
||||||
notebook: TabView,
|
notebook: TabView,
|
||||||
|
|
||||||
context_menu: *c.GtkWidget,
|
/// The "main" menu that is attached to a button in the headerbar.
|
||||||
|
titlebar_menu: Menu(Window, "titlebar_menu", true),
|
||||||
|
|
||||||
/// The libadwaita widget for receiving toast send requests.
|
/// The libadwaita widget for receiving toast send requests.
|
||||||
toast_overlay: *c.GtkWidget,
|
toast_overlay: *c.GtkWidget,
|
||||||
@ -107,12 +113,13 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
// Set up our own state
|
// Set up our own state
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
|
.last_config = @intFromPtr(&app.config),
|
||||||
.config = DerivedConfig.init(&app.config),
|
.config = DerivedConfig.init(&app.config),
|
||||||
.window = undefined,
|
.window = undefined,
|
||||||
.headerbar = undefined,
|
.headerbar = undefined,
|
||||||
.tab_overview = null,
|
.tab_overview = null,
|
||||||
.notebook = undefined,
|
.notebook = undefined,
|
||||||
.context_menu = undefined,
|
.titlebar_menu = undefined,
|
||||||
.toast_overlay = undefined,
|
.toast_overlay = undefined,
|
||||||
.winproto = .none,
|
.winproto = .none,
|
||||||
};
|
};
|
||||||
@ -137,6 +144,9 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
// Create our box which will hold our widgets in the main content area.
|
// Create our box which will hold our widgets in the main content area.
|
||||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
|
||||||
|
// Set up the menus
|
||||||
|
self.titlebar_menu.init(self);
|
||||||
|
|
||||||
// Setup our notebook
|
// Setup our notebook
|
||||||
self.notebook.init(self);
|
self.notebook.init(self);
|
||||||
|
|
||||||
@ -174,7 +184,15 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
const btn = c.gtk_menu_button_new();
|
const btn = c.gtk_menu_button_new();
|
||||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
c.gtk_menu_button_set_popover(@ptrCast(btn), @ptrCast(@alignCast(self.titlebar_menu.asWidget())));
|
||||||
|
_ = c.g_signal_connect_data(
|
||||||
|
btn,
|
||||||
|
"notify::active",
|
||||||
|
c.G_CALLBACK(>kTitlebarMenuActivate),
|
||||||
|
self,
|
||||||
|
null,
|
||||||
|
c.G_CONNECT_DEFAULT,
|
||||||
|
);
|
||||||
self.headerbar.packEnd(btn);
|
self.headerbar.packEnd(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,11 +277,6 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
|
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
|
|
||||||
c.gtk_widget_set_parent(self.context_menu, box);
|
|
||||||
c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0);
|
|
||||||
c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START);
|
|
||||||
|
|
||||||
// We register a key event controller with the window so
|
// We register a key event controller with the window so
|
||||||
// we can catch key events when our surface may not be
|
// we can catch key events when our surface may not be
|
||||||
// focused (i.e. when the libadw tab overview is shown).
|
// focused (i.e. when the libadw tab overview is shown).
|
||||||
@ -272,7 +285,6 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
c.gtk_widget_add_controller(gtk_widget, ec_key_press);
|
c.gtk_widget_add_controller(gtk_widget, ec_key_press);
|
||||||
|
|
||||||
// All of our events
|
// All of our events
|
||||||
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
|
|
||||||
_ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
||||||
_ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
_ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||||
@ -355,6 +367,13 @@ pub fn updateConfig(
|
|||||||
self: *Window,
|
self: *Window,
|
||||||
config: *const configpkg.Config,
|
config: *const configpkg.Config,
|
||||||
) !void {
|
) !void {
|
||||||
|
// avoid multiple reconfigs when we have many surfaces contained in this
|
||||||
|
// window using the integer value of config as a simple marker to know if
|
||||||
|
// we've "seen" this particular config before
|
||||||
|
const this_config = @intFromPtr(config);
|
||||||
|
if (self.last_config == this_config) return;
|
||||||
|
self.last_config = this_config;
|
||||||
|
|
||||||
self.config = DerivedConfig.init(config);
|
self.config = DerivedConfig.init(config);
|
||||||
|
|
||||||
// We always resync our appearance whenever the config changes.
|
// We always resync our appearance whenever the config changes.
|
||||||
@ -459,16 +478,19 @@ fn initActions(self: *Window) void {
|
|||||||
const actions = .{
|
const actions = .{
|
||||||
.{ "about", >kActionAbout },
|
.{ "about", >kActionAbout },
|
||||||
.{ "close", >kActionClose },
|
.{ "close", >kActionClose },
|
||||||
.{ "new_window", >kActionNewWindow },
|
.{ "new-window", >kActionNewWindow },
|
||||||
.{ "new_tab", >kActionNewTab },
|
.{ "new-tab", >kActionNewTab },
|
||||||
.{ "split_right", >kActionSplitRight },
|
.{ "close-tab", >kActionCloseTab },
|
||||||
.{ "split_down", >kActionSplitDown },
|
.{ "split-right", >kActionSplitRight },
|
||||||
.{ "split_left", >kActionSplitLeft },
|
.{ "split-down", >kActionSplitDown },
|
||||||
.{ "split_up", >kActionSplitUp },
|
.{ "split-left", >kActionSplitLeft },
|
||||||
.{ "toggle_inspector", >kActionToggleInspector },
|
.{ "split-up", >kActionSplitUp },
|
||||||
|
.{ "toggle-inspector", >kActionToggleInspector },
|
||||||
.{ "copy", >kActionCopy },
|
.{ "copy", >kActionCopy },
|
||||||
.{ "paste", >kActionPaste },
|
.{ "paste", >kActionPaste },
|
||||||
.{ "reset", >kActionReset },
|
.{ "reset", >kActionReset },
|
||||||
|
.{ "clear", >kActionClear },
|
||||||
|
.{ "prompt-title", >kActionPromptTitle },
|
||||||
};
|
};
|
||||||
|
|
||||||
inline for (actions) |entry| {
|
inline for (actions) |entry| {
|
||||||
@ -487,8 +509,6 @@ fn initActions(self: *Window) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Window) void {
|
pub fn deinit(self: *Window) void {
|
||||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
|
||||||
|
|
||||||
self.winproto.deinit(self.app.core_app.alloc);
|
self.winproto.deinit(self.app.core_app.alloc);
|
||||||
|
|
||||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||||
@ -752,16 +772,6 @@ fn adwTabOverviewFocusTimer(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
|
||||||
_ = v;
|
|
||||||
log.debug("refocus term request", .{});
|
|
||||||
const self = userdataSelf(ud.?);
|
|
||||||
|
|
||||||
self.focusCurrentTab();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||||
_ = v;
|
_ = v;
|
||||||
log.debug("window close request", .{});
|
log.debug("window close request", .{});
|
||||||
@ -919,11 +929,7 @@ fn gtkActionClose(
|
|||||||
ud: ?*anyopaque,
|
ud: ?*anyopaque,
|
||||||
) callconv(.C) void {
|
) callconv(.C) void {
|
||||||
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||||
const surface = self.actionSurface() orelse return;
|
c.gtk_window_destroy(self.window);
|
||||||
_ = surface.performBindingAction(.{ .close_surface = {} }) catch |err| {
|
|
||||||
log.warn("error performing binding action error={}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkActionNewWindow(
|
fn gtkActionNewWindow(
|
||||||
@ -948,6 +954,19 @@ fn gtkActionNewTab(
|
|||||||
gtkTabNewClick(undefined, ud);
|
gtkTabNewClick(undefined, ud);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gtkActionCloseTab(
|
||||||
|
_: *c.GSimpleAction,
|
||||||
|
_: *c.GVariant,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
const surface = self.actionSurface() orelse return;
|
||||||
|
_ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| {
|
||||||
|
log.warn("error performing binding action error={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn gtkActionSplitRight(
|
fn gtkActionSplitRight(
|
||||||
_: *c.GSimpleAction,
|
_: *c.GSimpleAction,
|
||||||
_: *c.GVariant,
|
_: *c.GVariant,
|
||||||
@ -1052,13 +1071,55 @@ fn gtkActionReset(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gtkActionClear(
|
||||||
|
_: *c.GSimpleAction,
|
||||||
|
_: *c.GVariant,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
const surface = self.actionSurface() orelse return;
|
||||||
|
_ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| {
|
||||||
|
log.warn("error performing binding action error={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkActionPromptTitle(
|
||||||
|
_: *c.GSimpleAction,
|
||||||
|
_: *c.GVariant,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
const surface = self.actionSurface() orelse return;
|
||||||
|
_ = surface.performBindingAction(.{ .prompt_surface_title = {} }) catch |err| {
|
||||||
|
log.warn("error performing binding action error={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the surface to use for an action.
|
/// Returns the surface to use for an action.
|
||||||
fn actionSurface(self: *Window) ?*CoreSurface {
|
pub fn actionSurface(self: *Window) ?*CoreSurface {
|
||||||
const tab = self.notebook.currentTab() orelse return null;
|
const tab = self.notebook.currentTab() orelse return null;
|
||||||
const surface = tab.focus_child orelse return null;
|
const surface = tab.focus_child orelse return null;
|
||||||
return &surface.core_surface;
|
return &surface.core_surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gtkTitlebarMenuActivate(
|
||||||
|
btn: *c.GtkMenuButton,
|
||||||
|
_: *c.GParamSpec,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
// debian 12 is stuck on GTK 4.8
|
||||||
|
if (!version.atLeast(4, 10, 0)) return;
|
||||||
|
const active = c.gtk_menu_button_get_active(btn) != 0;
|
||||||
|
const self = userdataSelf(ud orelse return);
|
||||||
|
if (active) {
|
||||||
|
self.titlebar_menu.refresh();
|
||||||
|
} else {
|
||||||
|
self.focusCurrentTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn userdataSelf(ud: *anyopaque) *Window {
|
fn userdataSelf(ud: *anyopaque) *Window {
|
||||||
return @ptrCast(@alignCast(ud));
|
return @ptrCast(@alignCast(ud));
|
||||||
}
|
}
|
||||||
|
57
src/apprt/gtk/blueprint_compiler.zig
Normal file
57
src/apprt/gtk/blueprint_compiler.zig
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const c = @cImport({
|
||||||
|
@cInclude("adwaita.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
const alloc = gpa.allocator();
|
||||||
|
|
||||||
|
var it = try std.process.argsWithAllocator(alloc);
|
||||||
|
defer it.deinit();
|
||||||
|
|
||||||
|
_ = it.next();
|
||||||
|
|
||||||
|
const major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10);
|
||||||
|
const minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10);
|
||||||
|
const micro = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMicroVersion, 10);
|
||||||
|
const output = it.next() orelse return error.NoOutput;
|
||||||
|
const input = it.next() orelse return error.NoInput;
|
||||||
|
|
||||||
|
if (c.ADW_MAJOR_VERSION < major or
|
||||||
|
(c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor) or
|
||||||
|
(c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION == minor and c.ADW_MICRO_VERSION < micro))
|
||||||
|
{
|
||||||
|
// If the Adwaita version is too old, generate an "empty" file.
|
||||||
|
const file = try std.fs.createFileAbsolute(output, .{
|
||||||
|
.truncate = true,
|
||||||
|
});
|
||||||
|
try file.writeAll(
|
||||||
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
\\<interface domain="com.mitchellh.ghostty"/>
|
||||||
|
);
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var compiler = std.process.Child.init(
|
||||||
|
&.{
|
||||||
|
"blueprint-compiler",
|
||||||
|
"compile",
|
||||||
|
"--output",
|
||||||
|
output,
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
alloc,
|
||||||
|
);
|
||||||
|
|
||||||
|
const term = try compiler.spawnAndWait();
|
||||||
|
switch (term) {
|
||||||
|
.Exited => |rc| {
|
||||||
|
if (rc != 0) std.posix.exit(1);
|
||||||
|
},
|
||||||
|
else => std.posix.exit(1),
|
||||||
|
}
|
||||||
|
}
|
@ -54,7 +54,19 @@ const icons = [_]struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const ui_files = [_][]const u8{};
|
pub const ui_files = [_][]const u8{};
|
||||||
pub const blueprint_files = [_][]const u8{};
|
|
||||||
|
pub const VersionedBlueprint = struct {
|
||||||
|
major: u16,
|
||||||
|
minor: u16,
|
||||||
|
micro: u16,
|
||||||
|
name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const blueprint_files = [_]VersionedBlueprint{
|
||||||
|
.{ .major = 1, .minor = 5, .micro = 0, .name = "prompt-title-dialog" },
|
||||||
|
.{ .major = 1, .minor = 0, .micro = 0, .name = "menu-surface-context_menu" },
|
||||||
|
.{ .major = 1, .minor = 0, .micro = 0, .name = "menu-window-titlebar_menu" },
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
@ -69,9 +81,9 @@ pub fn main() !void {
|
|||||||
var it = try std.process.argsWithAllocator(alloc);
|
var it = try std.process.argsWithAllocator(alloc);
|
||||||
defer it.deinit();
|
defer it.deinit();
|
||||||
|
|
||||||
while (it.next()) |filename| {
|
while (it.next()) |argument| {
|
||||||
if (std.mem.eql(u8, std.fs.path.extension(filename), ".ui")) {
|
if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
|
||||||
try extra_ui_files.append(try alloc.dupe(u8, filename));
|
try extra_ui_files.append(try alloc.dupe(u8, argument));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +153,7 @@ pub const dependencies = deps: {
|
|||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
for (blueprint_files) |blueprint_file| {
|
for (blueprint_files) |blueprint_file| {
|
||||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file});
|
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name});
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
break :deps deps;
|
break :deps deps;
|
||||||
|
136
src/apprt/gtk/menu.zig
Normal file
136
src/apprt/gtk/menu.zig
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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, .blp);
|
||||||
|
defer builder.deinit();
|
||||||
|
|
||||||
|
const menu_model = builder.getObject(gio.MenuModel, "menu").?;
|
||||||
|
|
||||||
|
const menu_widget = gtk.PopoverMenu.newFromModelFull(menu_model, .{ .nested = true });
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setVisible(self: *const Self, visible: bool) void {
|
||||||
|
self.menu_widget.as(gtk.Widget).setVisible(@intFromBool(visible));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 core_surface = self.parent.actionSurface() orelse break :window .{
|
||||||
|
@ptrCast(@alignCast(self.parent.window)),
|
||||||
|
false,
|
||||||
|
};
|
||||||
|
const has_selection = core_surface.hasSelection();
|
||||||
|
break :window .{ @ptrCast(@alignCast(self.parent.window)), has_selection };
|
||||||
|
},
|
||||||
|
Surface => surface: {
|
||||||
|
const window = self.parent.container.window() orelse return;
|
||||||
|
const has_selection = self.parent.core_surface.hasSelection();
|
||||||
|
break :surface .{ @ptrCast(@alignCast(window.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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-window .notebook separator {
|
.terminal-window .notebook paned > separator {
|
||||||
background-color: rgba(36, 36, 36, 1);
|
background-color: rgba(36, 36, 36, 1);
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ window.ssd.no-border-radius {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-window .notebook separator {
|
.terminal-window .notebook paned > separator {
|
||||||
background-color: rgba(250, 250, 250, 1);
|
background-color: rgba(250, 250, 250, 1);
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
102
src/apprt/gtk/ui/menu-surface-context_menu.blp
Normal file
102
src/apprt/gtk/ui/menu-surface-context_menu.blp
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
src/apprt/gtk/ui/menu-window-titlebar_menu.blp
Normal file
111
src/apprt/gtk/ui/menu-window-titlebar_menu.blp
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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: _("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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/apprt/gtk/ui/prompt-title-dialog.blp
Normal file
16
src/apprt/gtk/ui/prompt-title-dialog.blp
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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 {};
|
||||||
|
}
|
@ -443,6 +443,7 @@ pub fn add(
|
|||||||
.{ "glib", "glib2" },
|
.{ "glib", "glib2" },
|
||||||
.{ "gtk", "gtk4" },
|
.{ "gtk", "gtk4" },
|
||||||
.{ "gdk", "gdk4" },
|
.{ "gdk", "gdk4" },
|
||||||
|
.{ "adw", "adw1" },
|
||||||
};
|
};
|
||||||
inline for (gobject_imports) |import| {
|
inline for (gobject_imports) |import| {
|
||||||
const name, const module = import;
|
const name, const module = import;
|
||||||
@ -451,7 +452,6 @@ pub fn add(
|
|||||||
|
|
||||||
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||||
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
|
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
|
||||||
step.root_module.addImport("adw", gobject.module("adw1"));
|
|
||||||
|
|
||||||
if (self.config.x11) {
|
if (self.config.x11) {
|
||||||
step.linkSystemLibrary2("X11", dynamic_link_opts);
|
step.linkSystemLibrary2("X11", dynamic_link_opts);
|
||||||
@ -500,14 +500,24 @@ pub fn add(
|
|||||||
|
|
||||||
const generate = b.addRunArtifact(generate_gresource_xml);
|
const generate = b.addRunArtifact(generate_gresource_xml);
|
||||||
|
|
||||||
|
const gtk_blueprint_compiler = b.addExecutable(.{
|
||||||
|
.name = "gtk_blueprint_compiler",
|
||||||
|
.root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"),
|
||||||
|
.target = b.host,
|
||||||
|
});
|
||||||
|
gtk_blueprint_compiler.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||||
|
gtk_blueprint_compiler.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
|
||||||
|
gtk_blueprint_compiler.linkLibC();
|
||||||
|
|
||||||
for (gresource.blueprint_files) |blueprint_file| {
|
for (gresource.blueprint_files) |blueprint_file| {
|
||||||
const blueprint_compiler = b.addSystemCommand(&.{
|
const blueprint_compiler = b.addRunArtifact(gtk_blueprint_compiler);
|
||||||
"blueprint-compiler",
|
blueprint_compiler.addArgs(&.{
|
||||||
"compile",
|
b.fmt("{d}", .{blueprint_file.major}),
|
||||||
"--output",
|
b.fmt("{d}", .{blueprint_file.minor}),
|
||||||
|
b.fmt("{d}", .{blueprint_file.micro}),
|
||||||
});
|
});
|
||||||
const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file}));
|
const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file.name}));
|
||||||
blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file})));
|
blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name})));
|
||||||
generate.addFileArg(ui_file);
|
generate.addFileArg(ui_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,14 @@ FROM docker.io/library/debian:${DISTRO_VERSION}
|
|||||||
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
|
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
|
||||||
apt-get -qq -y --no-install-recommends install \
|
apt-get -qq -y --no-install-recommends install \
|
||||||
# Build Tools
|
# Build Tools
|
||||||
|
blueprint-compiler \
|
||||||
build-essential \
|
build-essential \
|
||||||
libbz2-dev \
|
libbz2-dev \
|
||||||
libonig-dev \
|
libonig-dev \
|
||||||
|
libxml2-utils \
|
||||||
lintian \
|
lintian \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
|
libxml2-utils \
|
||||||
pandoc \
|
pandoc \
|
||||||
wget \
|
wget \
|
||||||
# Ghostty Dependencies
|
# Ghostty Dependencies
|
||||||
|
@ -4,6 +4,11 @@ _\$XDG_CONFIG_HOME/ghostty/config_
|
|||||||
|
|
||||||
: Location of the default configuration file.
|
: Location of the default configuration file.
|
||||||
|
|
||||||
|
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_
|
||||||
|
|
||||||
|
: **On macOS**, location of the default configuration file. This location takes
|
||||||
|
precedence over the XDG environment locations.
|
||||||
|
|
||||||
_\$LOCALAPPDATA/ghostty/config_
|
_\$LOCALAPPDATA/ghostty/config_
|
||||||
|
|
||||||
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
|
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
|
||||||
@ -23,6 +28,11 @@ for configuration files.
|
|||||||
|
|
||||||
: Default location for configuration files.
|
: Default location for configuration files.
|
||||||
|
|
||||||
|
**$HOME/Library/Application Support/com.mitchellh.ghostty**
|
||||||
|
|
||||||
|
: **MACOS ONLY** default location for configuration files. This location takes
|
||||||
|
precedence over the XDG environment locations.
|
||||||
|
|
||||||
**LOCALAPPDATA**
|
**LOCALAPPDATA**
|
||||||
|
|
||||||
: **WINDOWS ONLY:** alternate location to search for configuration files.
|
: **WINDOWS ONLY:** alternate location to search for configuration files.
|
||||||
|
@ -4,6 +4,11 @@ _\$XDG_CONFIG_HOME/ghostty/config_
|
|||||||
|
|
||||||
: Location of the default configuration file.
|
: Location of the default configuration file.
|
||||||
|
|
||||||
|
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_
|
||||||
|
|
||||||
|
: **On macOS**, location of the default configuration file. This location takes
|
||||||
|
precedence over the XDG environment locations.
|
||||||
|
|
||||||
_\$LOCALAPPDATA/ghostty/config_
|
_\$LOCALAPPDATA/ghostty/config_
|
||||||
|
|
||||||
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
|
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
|
||||||
@ -15,6 +20,11 @@ for configuration files.
|
|||||||
|
|
||||||
: Default location for configuration files.
|
: Default location for configuration files.
|
||||||
|
|
||||||
|
**$HOME/Library/Application Support/com.mitchellh.ghostty**
|
||||||
|
|
||||||
|
: **MACOS ONLY** default location for configuration files. This location takes
|
||||||
|
precedence over the XDG environment locations.
|
||||||
|
|
||||||
**LOCALAPPDATA**
|
**LOCALAPPDATA**
|
||||||
|
|
||||||
: **WINDOWS ONLY:** alternate location to search for configuration files.
|
: **WINDOWS ONLY:** alternate location to search for configuration files.
|
||||||
|
@ -11,6 +11,11 @@ is on the roadmap but not yet supported. The configuration file must be placed
|
|||||||
at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config`
|
at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config`
|
||||||
if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
||||||
|
|
||||||
|
**If you are using macOS, the configuration file can also be placed at
|
||||||
|
`$HOME/Library/Application Support/com.mitchellh.ghostty/config`.** This is the
|
||||||
|
default configuration location for macOS. It will be searched before any of the
|
||||||
|
XDG environment locations listed above.
|
||||||
|
|
||||||
The file format is documented below as an example:
|
The file format is documented below as an example:
|
||||||
|
|
||||||
# The syntax is "key = value". The whitespace around the equals doesn't matter.
|
# The syntax is "key = value". The whitespace around the equals doesn't matter.
|
||||||
|
@ -4354,7 +4354,9 @@ pub const ColorList = struct {
|
|||||||
count += 1;
|
count += 1;
|
||||||
if (count > 64) return error.InvalidValue;
|
if (count > 64) return error.InvalidValue;
|
||||||
|
|
||||||
const color = try Color.parseCLI(raw);
|
// Trim whitespace from each color value
|
||||||
|
const trimmed = std.mem.trim(u8, raw, " \t");
|
||||||
|
const color = try Color.parseCLI(trimmed);
|
||||||
try self.colors.append(alloc, color);
|
try self.colors.append(alloc, color);
|
||||||
try self.colors_c.append(alloc, color.cval());
|
try self.colors_c.append(alloc, color.cval());
|
||||||
}
|
}
|
||||||
@ -4423,6 +4425,14 @@ pub const ColorList = struct {
|
|||||||
try p.parseCLI(alloc, "black,white");
|
try p.parseCLI(alloc, "black,white");
|
||||||
try testing.expectEqual(2, p.colors.items.len);
|
try testing.expectEqual(2, p.colors.items.len);
|
||||||
|
|
||||||
|
// Test whitespace handling
|
||||||
|
try p.parseCLI(alloc, "black, white"); // space after comma
|
||||||
|
try testing.expectEqual(2, p.colors.items.len);
|
||||||
|
try p.parseCLI(alloc, "black , white"); // spaces around comma
|
||||||
|
try testing.expectEqual(2, p.colors.items.len);
|
||||||
|
try p.parseCLI(alloc, " black , white "); // extra spaces at ends
|
||||||
|
try testing.expectEqual(2, p.colors.items.len);
|
||||||
|
|
||||||
// Error cases
|
// Error cases
|
||||||
try testing.expectError(error.ValueRequired, p.parseCLI(alloc, null));
|
try testing.expectError(error.ValueRequired, p.parseCLI(alloc, null));
|
||||||
try testing.expectError(error.InvalidValue, p.parseCLI(alloc, " "));
|
try testing.expectError(error.InvalidValue, p.parseCLI(alloc, " "));
|
||||||
|
@ -349,7 +349,6 @@ pub const Action = union(enum) {
|
|||||||
toggle_tab_overview: void,
|
toggle_tab_overview: void,
|
||||||
|
|
||||||
/// Change the title of the current focused surface via a prompt.
|
/// Change the title of the current focused surface via a prompt.
|
||||||
/// This only works on macOS currently.
|
|
||||||
prompt_surface_title: void,
|
prompt_surface_title: void,
|
||||||
|
|
||||||
/// Create a new split in the given direction.
|
/// Create a new split in the given direction.
|
||||||
|
@ -86,7 +86,9 @@ pub const Action = union(enum) {
|
|||||||
final: u8,
|
final: u8,
|
||||||
|
|
||||||
/// The list of separators used for CSI params. The value of the
|
/// The list of separators used for CSI params. The value of the
|
||||||
/// bit can be mapped to Sep.
|
/// bit can be mapped to Sep. The index of this bit set specifies
|
||||||
|
/// the separator AFTER that param. For example: 0;4:3 would have
|
||||||
|
/// index 1 set.
|
||||||
pub const SepList = std.StaticBitSet(MAX_PARAMS);
|
pub const SepList = std.StaticBitSet(MAX_PARAMS);
|
||||||
|
|
||||||
/// The separator used for CSI params.
|
/// The separator used for CSI params.
|
||||||
@ -192,7 +194,19 @@ pub const Action = union(enum) {
|
|||||||
/// 4 because we also use the intermediates array for UTF8 decoding which
|
/// 4 because we also use the intermediates array for UTF8 decoding which
|
||||||
/// can be at most 4 bytes.
|
/// can be at most 4 bytes.
|
||||||
const MAX_INTERMEDIATE = 4;
|
const MAX_INTERMEDIATE = 4;
|
||||||
const MAX_PARAMS = 16;
|
|
||||||
|
/// Maximum number of CSI parameters. This is arbitrary. Practically, the
|
||||||
|
/// only CSI command that uses more than 3 parameters is the SGR command
|
||||||
|
/// which can be infinitely long. 24 is a reasonable limit based on empirical
|
||||||
|
/// data. This used to be 16 but Kakoune has a SGR command that uses 17
|
||||||
|
/// parameters.
|
||||||
|
///
|
||||||
|
/// We could in the future make this the static limit and then allocate after
|
||||||
|
/// but that's a lot more work and practically its so rare to exceed this
|
||||||
|
/// number. I implore TUI authors to not use more than this number of CSI
|
||||||
|
/// params, but I suspect we'll introduce a slow path with heap allocation
|
||||||
|
/// one day.
|
||||||
|
const MAX_PARAMS = 24;
|
||||||
|
|
||||||
/// Current state of the state machine
|
/// Current state of the state machine
|
||||||
state: State = .ground,
|
state: State = .ground,
|
||||||
@ -689,6 +703,64 @@ test "csi: SGR mixed colon and semicolon with blank" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is from a Kakoune actual SGR sequence also.
|
||||||
|
test "csi: SGR mixed colon and semicolon setting underline, bg, fg" {
|
||||||
|
var p = init();
|
||||||
|
_ = p.next(0x1B);
|
||||||
|
for ("[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136") |c| {
|
||||||
|
const a = p.next(c);
|
||||||
|
try testing.expect(a[0] == null);
|
||||||
|
try testing.expect(a[1] == null);
|
||||||
|
try testing.expect(a[2] == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const a = p.next('m');
|
||||||
|
try testing.expect(p.state == .ground);
|
||||||
|
try testing.expect(a[0] == null);
|
||||||
|
try testing.expect(a[1].? == .csi_dispatch);
|
||||||
|
try testing.expect(a[2] == null);
|
||||||
|
|
||||||
|
const d = a[1].?.csi_dispatch;
|
||||||
|
try testing.expect(d.final == 'm');
|
||||||
|
try testing.expectEqual(17, d.params.len);
|
||||||
|
try testing.expectEqual(@as(u16, 4), d.params[0]);
|
||||||
|
try testing.expect(d.params_sep.isSet(0));
|
||||||
|
try testing.expectEqual(@as(u16, 3), d.params[1]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(1));
|
||||||
|
try testing.expectEqual(@as(u16, 38), d.params[2]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(2));
|
||||||
|
try testing.expectEqual(@as(u16, 2), d.params[3]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(3));
|
||||||
|
try testing.expectEqual(@as(u16, 51), d.params[4]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(4));
|
||||||
|
try testing.expectEqual(@as(u16, 51), d.params[5]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(5));
|
||||||
|
try testing.expectEqual(@as(u16, 51), d.params[6]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(6));
|
||||||
|
try testing.expectEqual(@as(u16, 48), d.params[7]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(7));
|
||||||
|
try testing.expectEqual(@as(u16, 2), d.params[8]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(8));
|
||||||
|
try testing.expectEqual(@as(u16, 170), d.params[9]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(9));
|
||||||
|
try testing.expectEqual(@as(u16, 170), d.params[10]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(10));
|
||||||
|
try testing.expectEqual(@as(u16, 170), d.params[11]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(11));
|
||||||
|
try testing.expectEqual(@as(u16, 58), d.params[12]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(12));
|
||||||
|
try testing.expectEqual(@as(u16, 2), d.params[13]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(13));
|
||||||
|
try testing.expectEqual(@as(u16, 255), d.params[14]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(14));
|
||||||
|
try testing.expectEqual(@as(u16, 97), d.params[15]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(15));
|
||||||
|
try testing.expectEqual(@as(u16, 136), d.params[16]);
|
||||||
|
try testing.expect(!d.params_sep.isSet(16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "csi: colon for non-m final" {
|
test "csi: colon for non-m final" {
|
||||||
var p = init();
|
var p = init();
|
||||||
_ = p.next(0x1B);
|
_ = p.next(0x1B);
|
||||||
|
@ -103,12 +103,16 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
/// Next returns the next attribute or null if there are no more attributes.
|
/// Next returns the next attribute or null if there are no more attributes.
|
||||||
pub fn next(self: *Parser) ?Attribute {
|
pub fn next(self: *Parser) ?Attribute {
|
||||||
if (self.idx > self.params.len) return null;
|
if (self.idx >= self.params.len) {
|
||||||
|
// If we're at index zero it means we must have an empty
|
||||||
|
// list and an empty list implicitly means unset.
|
||||||
|
if (self.idx == 0) {
|
||||||
|
// Add one to ensure we don't loop on unset
|
||||||
|
self.idx += 1;
|
||||||
|
return .unset;
|
||||||
|
}
|
||||||
|
|
||||||
// Implicitly means unset
|
return null;
|
||||||
if (self.params.len == 0) {
|
|
||||||
self.idx += 1;
|
|
||||||
return .unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const slice = self.params[self.idx..self.params.len];
|
const slice = self.params[self.idx..self.params.len];
|
||||||
@ -788,7 +792,6 @@ test "sgr: direct fg colon with colorspace and extra param" {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const v = p.next().?;
|
const v = p.next().?;
|
||||||
std.log.warn("WHAT={}", .{v});
|
|
||||||
try testing.expect(v == .direct_color_fg);
|
try testing.expect(v == .direct_color_fg);
|
||||||
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
|
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
|
||||||
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
|
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
|
||||||
@ -864,3 +867,50 @@ test "sgr: kakoune input" {
|
|||||||
|
|
||||||
//try testing.expect(p.next() == null);
|
//try testing.expect(p.next() == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Discussion #5930, another input sent by kakoune
|
||||||
|
test "sgr: kakoune input issue underline, fg, and bg" {
|
||||||
|
// echo -e "\033[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136mset everything in one sequence, broken\033[m"
|
||||||
|
|
||||||
|
// This used to crash
|
||||||
|
var p: Parser = .{
|
||||||
|
.params = &[_]u16{ 4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136 },
|
||||||
|
.params_sep = sep: {
|
||||||
|
var list = SepList.initEmpty();
|
||||||
|
list.set(0);
|
||||||
|
break :sep list;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = p.next().?;
|
||||||
|
try testing.expect(v == .underline);
|
||||||
|
try testing.expectEqual(Attribute.Underline.curly, v.underline);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = p.next().?;
|
||||||
|
try testing.expect(v == .direct_color_fg);
|
||||||
|
try testing.expectEqual(@as(u8, 51), v.direct_color_fg.r);
|
||||||
|
try testing.expectEqual(@as(u8, 51), v.direct_color_fg.g);
|
||||||
|
try testing.expectEqual(@as(u8, 51), v.direct_color_fg.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = p.next().?;
|
||||||
|
try testing.expect(v == .direct_color_bg);
|
||||||
|
try testing.expectEqual(@as(u8, 170), v.direct_color_bg.r);
|
||||||
|
try testing.expectEqual(@as(u8, 170), v.direct_color_bg.g);
|
||||||
|
try testing.expectEqual(@as(u8, 170), v.direct_color_bg.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = p.next().?;
|
||||||
|
try testing.expect(v == .underline_color);
|
||||||
|
try testing.expectEqual(@as(u8, 255), v.underline_color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 97), v.underline_color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 136), v.underline_color.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expect(p.next() == null);
|
||||||
|
}
|
||||||
|
@ -932,7 +932,7 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
// SGR - Select Graphic Rendition
|
// SGR - Select Graphic Rendition
|
||||||
'm' => switch (input.intermediates.len) {
|
'm' => switch (input.intermediates.len) {
|
||||||
0 => if (@hasDecl(T, "setAttribute")) {
|
0 => if (@hasDecl(T, "setAttribute")) {
|
||||||
// log.info("parse SGR params={any}", .{action.params});
|
// log.info("parse SGR params={any}", .{input.params});
|
||||||
var p: sgr.Parser = .{
|
var p: sgr.Parser = .{
|
||||||
.params = input.params,
|
.params = input.params,
|
||||||
.params_sep = input.params_sep,
|
.params_sep = input.params_sep,
|
||||||
|
Reference in New Issue
Block a user