mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-08 12:54:28 +00:00
Merge branch 'main' into ko_kr
This commit is contained in:
35
src/App.zig
35
src/App.zig
@@ -76,34 +76,38 @@ first: bool = true,
|
||||
|
||||
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
|
||||
|
||||
/// Create a new app instance. This returns a stable pointer to the app
|
||||
/// instance which is required for callbacks.
|
||||
pub fn create(alloc: Allocator) CreateError!*App {
|
||||
var app = try alloc.create(App);
|
||||
errdefer alloc.destroy(app);
|
||||
try app.init(alloc);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
///
|
||||
/// After calling this function, well behaved apprts should then call
|
||||
/// `focusEvent` to set the initial focus state of the app.
|
||||
pub fn create(
|
||||
pub fn init(
|
||||
self: *App,
|
||||
alloc: Allocator,
|
||||
) CreateError!*App {
|
||||
var app = try alloc.create(App);
|
||||
errdefer alloc.destroy(app);
|
||||
|
||||
) CreateError!void {
|
||||
var font_grid_set = try font.SharedGridSet.init(alloc);
|
||||
errdefer font_grid_set.deinit();
|
||||
|
||||
app.* = .{
|
||||
self.* = .{
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
};
|
||||
errdefer app.surfaces.deinit(alloc);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *App) void {
|
||||
pub fn deinit(self: *App) void {
|
||||
// Clean up all our surfaces
|
||||
for (self.surfaces.items) |surface| surface.deinit();
|
||||
self.surfaces.deinit(self.alloc);
|
||||
@@ -114,7 +118,13 @@ pub fn destroy(self: *App) void {
|
||||
// should gracefully close all surfaces.
|
||||
assert(self.font_grid_set.count() == 0);
|
||||
self.font_grid_set.deinit();
|
||||
}
|
||||
|
||||
pub fn destroy(self: *App) void {
|
||||
// Deinitialize the app
|
||||
self.deinit();
|
||||
|
||||
// Free the app memory
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
@@ -444,6 +454,11 @@ pub fn performAction(
|
||||
.close_all_windows => _ = try rt_app.performAction(.app, .close_all_windows, {}),
|
||||
.toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}),
|
||||
.toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}),
|
||||
.check_for_updates => _ = try rt_app.performAction(.app, .check_for_updates, {}),
|
||||
.show_gtk_inspector => _ = try rt_app.performAction(.app, .show_gtk_inspector, {}),
|
||||
.undo => _ = try rt_app.performAction(.app, .undo, {}),
|
||||
|
||||
.redo => _ = try rt_app.performAction(.app, .redo, {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,17 @@ const EnvMap = std.process.EnvMap;
|
||||
|
||||
const PreExecFn = fn (*Command) void;
|
||||
|
||||
/// Path to the command to run. This must be an absolute path. This
|
||||
/// library does not do PATH lookup.
|
||||
path: []const u8,
|
||||
/// Path to the command to run. This doesn't have to be an absolute path,
|
||||
/// because use exec functions that search the PATH, if necessary.
|
||||
///
|
||||
/// This field is null-terminated to avoid a copy for the sake of
|
||||
/// adding a null terminator since POSIX systems are so common.
|
||||
path: [:0]const u8,
|
||||
|
||||
/// Command-line arguments. It is the responsibility of the caller to set
|
||||
/// args[0] to the command. If args is empty then args[0] will automatically
|
||||
/// be set to equal path.
|
||||
args: []const []const u8,
|
||||
args: []const [:0]const u8,
|
||||
|
||||
/// Environment variables for the child process. If this is null, inherits
|
||||
/// the environment variables from this process. These are the exact
|
||||
@@ -129,9 +132,8 @@ pub fn start(self: *Command, alloc: Allocator) !void {
|
||||
|
||||
fn startPosix(self: *Command, arena: Allocator) !void {
|
||||
// Null-terminate all our arguments
|
||||
const pathZ = try arena.dupeZ(u8, self.path);
|
||||
const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null);
|
||||
for (self.args, 0..) |arg, i| argsZ[i] = (try arena.dupeZ(u8, arg)).ptr;
|
||||
const argsZ = try arena.allocSentinel(?[*:0]const u8, self.args.len, null);
|
||||
for (self.args, 0..) |arg, i| argsZ[i] = arg.ptr;
|
||||
|
||||
// Determine our env vars
|
||||
const envp = if (self.env) |env_map|
|
||||
@@ -184,7 +186,9 @@ fn startPosix(self: *Command, arena: Allocator) !void {
|
||||
if (self.pre_exec) |f| f(self);
|
||||
|
||||
// Finally, replace our process.
|
||||
_ = posix.execveZ(pathZ, argsZ, envp) catch null;
|
||||
// Note: we must use the "p"-variant of exec here because we
|
||||
// do not guarantee our command is looked up already in the path.
|
||||
_ = posix.execvpeZ(self.path, argsZ, envp) catch null;
|
||||
|
||||
// If we are executing this code, the exec failed. In that scenario,
|
||||
// we return a very specific error that can be detected to determine
|
||||
@@ -319,7 +323,7 @@ fn setupFd(src: File.Handle, target: i32) !void {
|
||||
}
|
||||
}
|
||||
},
|
||||
.ios, .macos => {
|
||||
.freebsd, .ios, .macos => {
|
||||
// Mac doesn't support dup3 so we use dup2. We purposely clear
|
||||
// CLO_ON_EXEC for this fd.
|
||||
const flags = try posix.fcntl(src, posix.F.GETFD, 0);
|
||||
@@ -366,7 +370,7 @@ pub fn wait(self: Command, block: bool) !Exit {
|
||||
}
|
||||
};
|
||||
|
||||
return Exit.init(res.status);
|
||||
return .init(res.status);
|
||||
}
|
||||
|
||||
/// Sets command->data to data.
|
||||
|
||||
1282
src/Surface.zig
1282
src/Surface.zig
File diff suppressed because it is too large
Load Diff
@@ -107,6 +107,9 @@ pub const Action = union(Key) {
|
||||
/// Toggle the quick terminal in or out.
|
||||
toggle_quick_terminal,
|
||||
|
||||
/// Toggle the command palette. This currently only works on macOS.
|
||||
toggle_command_palette,
|
||||
|
||||
/// Toggle the visibility of all Ghostty terminal windows.
|
||||
toggle_visibility,
|
||||
|
||||
@@ -162,6 +165,9 @@ pub const Action = union(Key) {
|
||||
/// Control whether the inspector is shown or hidden.
|
||||
inspector: Inspector,
|
||||
|
||||
/// Show the GTK inspector.
|
||||
show_gtk_inspector,
|
||||
|
||||
/// The inspector for the given target has changes and should be
|
||||
/// rendered at the next opportunity.
|
||||
render_inspector,
|
||||
@@ -202,6 +208,10 @@ pub const Action = union(Key) {
|
||||
/// happen and can be ignored or cause a restart it isn't that important.
|
||||
quit_timer: QuitTimer,
|
||||
|
||||
/// Set the window floating state. A floating window is one that is
|
||||
/// always on top of other windows even when not focused.
|
||||
float_window: FloatWindow,
|
||||
|
||||
/// Set the secure input functionality on or off. "Secure input" means
|
||||
/// that the user is currently at some sort of prompt where they may be
|
||||
/// entering a password or other sensitive information. This can be used
|
||||
@@ -244,6 +254,19 @@ pub const Action = union(Key) {
|
||||
/// Closes the currently focused window.
|
||||
close_window,
|
||||
|
||||
/// Called when the bell character is seen. The apprt should do whatever
|
||||
/// it needs to ring the bell. This is usually a sound or visual effect.
|
||||
ring_bell,
|
||||
|
||||
/// Undo the last action. See the "undo" keybinding for more
|
||||
/// details on what can and cannot be undone.
|
||||
undo,
|
||||
|
||||
/// Redo the last undone action.
|
||||
redo,
|
||||
|
||||
check_for_updates,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
@@ -257,6 +280,7 @@ pub const Action = union(Key) {
|
||||
toggle_tab_overview,
|
||||
toggle_window_decorations,
|
||||
toggle_quick_terminal,
|
||||
toggle_command_palette,
|
||||
toggle_visibility,
|
||||
move_tab,
|
||||
goto_tab,
|
||||
@@ -270,6 +294,7 @@ pub const Action = union(Key) {
|
||||
initial_size,
|
||||
cell_size,
|
||||
inspector,
|
||||
show_gtk_inspector,
|
||||
render_inspector,
|
||||
desktop_notification,
|
||||
set_title,
|
||||
@@ -281,12 +306,17 @@ pub const Action = union(Key) {
|
||||
renderer_health,
|
||||
open_config,
|
||||
quit_timer,
|
||||
float_window,
|
||||
secure_input,
|
||||
key_sequence,
|
||||
color_change,
|
||||
reload_config,
|
||||
config_change,
|
||||
close_window,
|
||||
ring_bell,
|
||||
undo,
|
||||
redo,
|
||||
check_for_updates,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
@@ -311,7 +341,7 @@ pub const Action = union(Key) {
|
||||
|
||||
break :cvalue @Type(.{ .@"union" = .{
|
||||
.layout = .@"extern",
|
||||
.tag_type = Key,
|
||||
.tag_type = null,
|
||||
.fields = &union_fields,
|
||||
.decls = &.{},
|
||||
} });
|
||||
@@ -323,6 +353,13 @@ pub const Action = union(Key) {
|
||||
value: CValue,
|
||||
};
|
||||
|
||||
comptime {
|
||||
// For ABI compatibility, we expect that this is our union size.
|
||||
// At the time of writing, we don't promise ABI compatibility
|
||||
// so we can change this but I want to be aware of it.
|
||||
assert(@sizeOf(CValue) == 16);
|
||||
}
|
||||
|
||||
/// Returns the value type for the given key.
|
||||
pub fn Value(comptime key: Key) type {
|
||||
inline for (@typeInfo(Action).@"union".fields) |field| {
|
||||
@@ -409,6 +446,12 @@ pub const Fullscreen = enum(c_int) {
|
||||
macos_non_native_padded_notch,
|
||||
};
|
||||
|
||||
pub const FloatWindow = enum(c_int) {
|
||||
on,
|
||||
off,
|
||||
toggle,
|
||||
};
|
||||
|
||||
pub const SecureInput = enum(c_int) {
|
||||
on,
|
||||
off,
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
const internal_os = @import("../os/main.zig");
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
pub const App = struct {};
|
||||
pub const Window = struct {};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,8 @@ const darwin_enabled = builtin.target.os.tag.isDarwin() and
|
||||
|
||||
const log = std.log.scoped(.glfw);
|
||||
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
|
||||
pub const App = struct {
|
||||
app: *CoreApp,
|
||||
config: Config,
|
||||
@@ -48,7 +50,7 @@ pub const App = struct {
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
pub fn init(core_app: *CoreApp, _: Options) !App {
|
||||
pub fn init(self: *App, core_app: *CoreApp, _: Options) !void {
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{});
|
||||
log.warn("You should use the AppKit-based app instead. The official download", .{});
|
||||
@@ -105,7 +107,7 @@ pub const App = struct {
|
||||
// We want the event loop to wake up instantly so we can process our tick.
|
||||
glfw.postEmptyEvent();
|
||||
|
||||
return .{
|
||||
self.* = .{
|
||||
.app = core_app,
|
||||
.config = config,
|
||||
.darwin = darwin,
|
||||
@@ -228,12 +230,14 @@ pub const App = struct {
|
||||
.toggle_tab_overview,
|
||||
.toggle_window_decorations,
|
||||
.toggle_quick_terminal,
|
||||
.toggle_command_palette,
|
||||
.toggle_visibility,
|
||||
.goto_tab,
|
||||
.move_tab,
|
||||
.inspector,
|
||||
.render_inspector,
|
||||
.quit_timer,
|
||||
.float_window,
|
||||
.secure_input,
|
||||
.key_sequence,
|
||||
.desktop_notification,
|
||||
@@ -246,6 +250,11 @@ pub const App = struct {
|
||||
.toggle_maximize,
|
||||
.prompt_title,
|
||||
.reset_window_size,
|
||||
.ring_bell,
|
||||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.show_gtk_inspector,
|
||||
=> {
|
||||
log.info("unimplemented action={}", .{action});
|
||||
return false;
|
||||
@@ -963,46 +972,46 @@ pub const Surface = struct {
|
||||
.repeat => .repeat,
|
||||
};
|
||||
const key: input.Key = switch (glfw_key) {
|
||||
.a => .a,
|
||||
.b => .b,
|
||||
.c => .c,
|
||||
.d => .d,
|
||||
.e => .e,
|
||||
.f => .f,
|
||||
.g => .g,
|
||||
.h => .h,
|
||||
.i => .i,
|
||||
.j => .j,
|
||||
.k => .k,
|
||||
.l => .l,
|
||||
.m => .m,
|
||||
.n => .n,
|
||||
.o => .o,
|
||||
.p => .p,
|
||||
.q => .q,
|
||||
.r => .r,
|
||||
.s => .s,
|
||||
.t => .t,
|
||||
.u => .u,
|
||||
.v => .v,
|
||||
.w => .w,
|
||||
.x => .x,
|
||||
.y => .y,
|
||||
.z => .z,
|
||||
.zero => .zero,
|
||||
.one => .one,
|
||||
.two => .two,
|
||||
.three => .three,
|
||||
.four => .four,
|
||||
.five => .five,
|
||||
.six => .six,
|
||||
.seven => .seven,
|
||||
.eight => .eight,
|
||||
.nine => .nine,
|
||||
.up => .up,
|
||||
.down => .down,
|
||||
.right => .right,
|
||||
.left => .left,
|
||||
.a => .key_a,
|
||||
.b => .key_b,
|
||||
.c => .key_c,
|
||||
.d => .key_d,
|
||||
.e => .key_e,
|
||||
.f => .key_f,
|
||||
.g => .key_g,
|
||||
.h => .key_h,
|
||||
.i => .key_i,
|
||||
.j => .key_j,
|
||||
.k => .key_k,
|
||||
.l => .key_l,
|
||||
.m => .key_m,
|
||||
.n => .key_n,
|
||||
.o => .key_o,
|
||||
.p => .key_p,
|
||||
.q => .key_q,
|
||||
.r => .key_r,
|
||||
.s => .key_s,
|
||||
.t => .key_t,
|
||||
.u => .key_u,
|
||||
.v => .key_v,
|
||||
.w => .key_w,
|
||||
.x => .key_x,
|
||||
.y => .key_y,
|
||||
.z => .key_z,
|
||||
.zero => .digit_0,
|
||||
.one => .digit_1,
|
||||
.two => .digit_2,
|
||||
.three => .digit_3,
|
||||
.four => .digit_4,
|
||||
.five => .digit_5,
|
||||
.six => .digit_6,
|
||||
.seven => .digit_7,
|
||||
.eight => .digit_8,
|
||||
.nine => .digit_9,
|
||||
.up => .arrow_up,
|
||||
.down => .arrow_down,
|
||||
.right => .arrow_right,
|
||||
.left => .arrow_left,
|
||||
.home => .home,
|
||||
.end => .end,
|
||||
.page_up => .page_up,
|
||||
@@ -1033,34 +1042,34 @@ pub const Surface = struct {
|
||||
.F23 => .f23,
|
||||
.F24 => .f24,
|
||||
.F25 => .f25,
|
||||
.kp_0 => .kp_0,
|
||||
.kp_1 => .kp_1,
|
||||
.kp_2 => .kp_2,
|
||||
.kp_3 => .kp_3,
|
||||
.kp_4 => .kp_4,
|
||||
.kp_5 => .kp_5,
|
||||
.kp_6 => .kp_6,
|
||||
.kp_7 => .kp_7,
|
||||
.kp_8 => .kp_8,
|
||||
.kp_9 => .kp_9,
|
||||
.kp_decimal => .kp_decimal,
|
||||
.kp_divide => .kp_divide,
|
||||
.kp_multiply => .kp_multiply,
|
||||
.kp_subtract => .kp_subtract,
|
||||
.kp_add => .kp_add,
|
||||
.kp_enter => .kp_enter,
|
||||
.kp_equal => .kp_equal,
|
||||
.grave_accent => .grave_accent,
|
||||
.kp_0 => .numpad_0,
|
||||
.kp_1 => .numpad_1,
|
||||
.kp_2 => .numpad_2,
|
||||
.kp_3 => .numpad_3,
|
||||
.kp_4 => .numpad_4,
|
||||
.kp_5 => .numpad_5,
|
||||
.kp_6 => .numpad_6,
|
||||
.kp_7 => .numpad_7,
|
||||
.kp_8 => .numpad_8,
|
||||
.kp_9 => .numpad_9,
|
||||
.kp_decimal => .numpad_decimal,
|
||||
.kp_divide => .numpad_divide,
|
||||
.kp_multiply => .numpad_multiply,
|
||||
.kp_subtract => .numpad_subtract,
|
||||
.kp_add => .numpad_add,
|
||||
.kp_enter => .numpad_enter,
|
||||
.kp_equal => .numpad_equal,
|
||||
.grave_accent => .backquote,
|
||||
.minus => .minus,
|
||||
.equal => .equal,
|
||||
.space => .space,
|
||||
.semicolon => .semicolon,
|
||||
.apostrophe => .apostrophe,
|
||||
.apostrophe => .quote,
|
||||
.comma => .comma,
|
||||
.period => .period,
|
||||
.slash => .slash,
|
||||
.left_bracket => .left_bracket,
|
||||
.right_bracket => .right_bracket,
|
||||
.left_bracket => .bracket_left,
|
||||
.right_bracket => .bracket_right,
|
||||
.backslash => .backslash,
|
||||
.enter => .enter,
|
||||
.tab => .tab,
|
||||
@@ -1072,20 +1081,20 @@ pub const Surface = struct {
|
||||
.num_lock => .num_lock,
|
||||
.print_screen => .print_screen,
|
||||
.pause => .pause,
|
||||
.left_shift => .left_shift,
|
||||
.left_control => .left_control,
|
||||
.left_alt => .left_alt,
|
||||
.left_super => .left_super,
|
||||
.right_shift => .right_shift,
|
||||
.right_control => .right_control,
|
||||
.right_alt => .right_alt,
|
||||
.right_super => .right_super,
|
||||
.left_shift => .shift_left,
|
||||
.left_control => .control_left,
|
||||
.left_alt => .alt_left,
|
||||
.left_super => .meta_left,
|
||||
.right_shift => .shift_right,
|
||||
.right_control => .control_right,
|
||||
.right_alt => .alt_right,
|
||||
.right_super => .meta_right,
|
||||
.menu => .context_menu,
|
||||
|
||||
.menu,
|
||||
.world_1,
|
||||
.world_2,
|
||||
.unknown,
|
||||
=> .invalid,
|
||||
=> .unidentified,
|
||||
};
|
||||
|
||||
// This is a hack for GLFW. We require our apprts to send both
|
||||
@@ -1105,7 +1114,6 @@ pub const Surface = struct {
|
||||
const key_event: input.KeyEvent = .{
|
||||
.action = action,
|
||||
.key = key,
|
||||
.physical_key = key,
|
||||
.mods = mods,
|
||||
.consumed_mods = .{},
|
||||
.composing = false,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub const App = @import("gtk/App.zig");
|
||||
pub const Surface = @import("gtk/Surface.zig");
|
||||
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
@@ -40,6 +40,7 @@ const Window = @import("Window.zig");
|
||||
const ConfigErrorsDialog = @import("ConfigErrorsDialog.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const GlobalShortcuts = @import("GlobalShortcuts.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
@@ -54,6 +55,11 @@ pub const c = @cImport({
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
|
||||
/// message so that we can call `drawFrame` ourselves from the app thread,
|
||||
/// because GTK's `GLArea` does not support drawing from a different thread.
|
||||
pub const must_draw_from_app_thread = true;
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
core_app: *CoreApp,
|
||||
@@ -74,6 +80,9 @@ cursor_none: ?*gdk.Cursor,
|
||||
/// The clipboard confirmation window, if it is currently open.
|
||||
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
||||
|
||||
/// The config errors dialog, if it is currently open.
|
||||
config_errors_dialog: ?ConfigErrorsDialog = null,
|
||||
|
||||
/// The window containing the quick terminal.
|
||||
/// Null when never initialized.
|
||||
quick_terminal: ?*Window = null,
|
||||
@@ -92,6 +101,8 @@ css_provider: *gtk.CssProvider,
|
||||
/// Providers for loading custom stylesheets defined by user
|
||||
custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .{},
|
||||
|
||||
global_shortcuts: ?GlobalShortcuts,
|
||||
|
||||
/// The timer used to quit the application after the last window is closed.
|
||||
quit_timer: union(enum) {
|
||||
off: void,
|
||||
@@ -99,7 +110,7 @@ quit_timer: union(enum) {
|
||||
expired: void,
|
||||
} = .{ .off = {} },
|
||||
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
|
||||
_ = opts;
|
||||
|
||||
// Log our GTK version
|
||||
@@ -137,8 +148,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
if (config.@"async-backend" != .auto) {
|
||||
const result: bool = switch (config.@"async-backend") {
|
||||
.auto => unreachable,
|
||||
.epoll => xev.prefer(.epoll),
|
||||
.io_uring => xev.prefer(.io_uring),
|
||||
.epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
|
||||
.io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
|
||||
};
|
||||
|
||||
if (result) {
|
||||
@@ -159,6 +170,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
opengl: bool = false,
|
||||
/// disable GLES, Ghostty can't use GLES
|
||||
@"gl-disable-gles": bool = false,
|
||||
// GTK's new renderer can cause blurry font when using fractional scaling.
|
||||
@"gl-no-fractional": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
@@ -190,7 +202,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
gdk_disable.@"gles-api" = true;
|
||||
gdk_disable.vulkan = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
break :environment;
|
||||
}
|
||||
if (gtk_version.runtimeAtLeast(4, 14, 0)) {
|
||||
@@ -201,8 +212,12 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
gdk_debug.@"gl-disable-gles" = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
|
||||
if (gtk_version.runtimeUntil(4, 17, 5)) {
|
||||
// Removed at GTK v4.17.5
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
}
|
||||
break :environment;
|
||||
}
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
@@ -263,7 +278,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
const single_instance = switch (config.@"gtk-single-instance") {
|
||||
.true => true,
|
||||
.false => false,
|
||||
.desktop => internal_os.launchedFromDesktop(),
|
||||
.desktop => switch (config.@"launched-from".?) {
|
||||
.desktop, .systemd, .dbus => true,
|
||||
.cli => false,
|
||||
},
|
||||
};
|
||||
|
||||
// Setup the flags for our application.
|
||||
@@ -278,7 +296,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// can develop Ghostty in Ghostty.
|
||||
const app_id: [:0]const u8 = app_id: {
|
||||
if (config.class) |class| {
|
||||
if (isValidAppId(class)) {
|
||||
if (gio.Application.idIsValid(class) != 0) {
|
||||
break :app_id class;
|
||||
} else {
|
||||
log.warn("invalid 'class' in config, ignoring", .{});
|
||||
@@ -314,8 +332,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
.prefer_dark;
|
||||
},
|
||||
.system => .prefer_light,
|
||||
.dark => .prefer_dark,
|
||||
.light => .force_dark,
|
||||
.dark => .force_dark,
|
||||
.light => .force_light,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -387,11 +405,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if the config allows it (this allows
|
||||
// for launching Ghostty in the "background" without immediately opening
|
||||
// a window)
|
||||
// a window). An initial window will not be immediately created if we were
|
||||
// launched by D-Bus activation or systemd. D-Bus activation will send it's
|
||||
// own `activate` or `new-window` signal later.
|
||||
//
|
||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||
if (config.@"initial-window")
|
||||
gio_app.activate();
|
||||
if (config.@"initial-window") switch (config.@"launched-from".?) {
|
||||
.desktop, .cli => gio_app.activate(),
|
||||
.dbus, .systemd => {},
|
||||
};
|
||||
|
||||
// Internally, GTK ensures that only one instance of this provider exists in the provider list
|
||||
// for the display.
|
||||
@@ -402,7 +424,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
|
||||
);
|
||||
|
||||
return .{
|
||||
self.* = .{
|
||||
.core_app = core_app,
|
||||
.app = adw_app,
|
||||
.config = config,
|
||||
@@ -415,6 +437,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// our "activate" call above will open a window.
|
||||
.running = gio_app.getIsRemote() == 0,
|
||||
.css_provider = css_provider,
|
||||
.global_shortcuts = .init(core_app.alloc, gio_app),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -436,6 +459,8 @@ pub fn terminate(self: *App) void {
|
||||
|
||||
self.winproto.deinit(self.core_app.alloc);
|
||||
|
||||
if (self.global_shortcuts) |*shortcuts| shortcuts.deinit();
|
||||
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
@@ -468,6 +493,7 @@ pub fn performAction(
|
||||
.config_change => self.configChange(target, value.config),
|
||||
.reload_config => try self.reloadConfig(target, value),
|
||||
.inspector => self.controlInspector(target, value),
|
||||
.show_gtk_inspector => self.showGTKInspector(),
|
||||
.desktop_notification => self.showDesktopNotification(target, value),
|
||||
.set_title => try self.setTitle(target, value),
|
||||
.pwd => try self.setPwd(target, value),
|
||||
@@ -484,9 +510,12 @@ pub fn performAction(
|
||||
.prompt_title => try self.promptTitle(target),
|
||||
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
|
||||
.secure_input => self.setSecureInput(target, value),
|
||||
.ring_bell => try self.ringBell(target),
|
||||
.toggle_command_palette => try self.toggleCommandPalette(target),
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
.float_window,
|
||||
.toggle_visibility,
|
||||
.cell_size,
|
||||
.key_sequence,
|
||||
@@ -494,6 +523,9 @@ pub fn performAction(
|
||||
.renderer_health,
|
||||
.color_change,
|
||||
.reset_window_size,
|
||||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
@@ -670,6 +702,12 @@ fn controlInspector(
|
||||
surface.controlInspector(mode);
|
||||
}
|
||||
|
||||
fn showGTKInspector(
|
||||
_: *const App,
|
||||
) void {
|
||||
gtk.Window.setInteractiveDebugging(@intFromBool(true));
|
||||
}
|
||||
|
||||
fn toggleMaximize(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
@@ -740,7 +778,7 @@ fn toggleWindowDecorations(
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
"toggleWindowDecorations invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
@@ -775,6 +813,30 @@ fn toggleQuickTerminal(self: *App) !bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn ringBell(_: *App, target: apprt.Target) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.ringBell(),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleCommandPalette(_: *App, target: apprt.Target) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| {
|
||||
const window = surface.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleCommandPalette invalid for container={s}",
|
||||
.{@tagName(surface.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleCommandPalette();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
||||
switch (mode) {
|
||||
.start => self.startQuitTimer(),
|
||||
@@ -995,6 +1057,12 @@ fn syncConfigChanges(self: *App, window: ?*Window) !void {
|
||||
ConfigErrorsDialog.maybePresent(self, window);
|
||||
try self.syncActionAccelerators();
|
||||
|
||||
if (self.global_shortcuts) |*shortcuts| {
|
||||
shortcuts.refreshSession(self) catch |err| {
|
||||
log.warn("failed to refresh global shortcuts={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// Load our runtime and custom CSS. If this fails then our window is just stuck
|
||||
// with the old CSS but we don't want to fail the entire sync operation.
|
||||
self.loadRuntimeCss() catch |err| switch (err) {
|
||||
@@ -1013,6 +1081,8 @@ fn syncActionAccelerators(self: *App) !void {
|
||||
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
|
||||
try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
|
||||
try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
|
||||
try self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector);
|
||||
try self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette);
|
||||
try self.syncActionAccelerator("win.close", .{ .close_window = {} });
|
||||
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
|
||||
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
|
||||
@@ -1282,6 +1352,13 @@ pub fn run(self: *App) !void {
|
||||
// Setup our actions
|
||||
self.initActions();
|
||||
|
||||
// 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
|
||||
// state.
|
||||
self.syncConfigChanges(null) catch |err| {
|
||||
log.warn("error handling configuration changes err={}", .{err});
|
||||
};
|
||||
|
||||
while (self.running) {
|
||||
_ = glib.MainContext.iteration(self.ctx, 1);
|
||||
|
||||
@@ -1506,7 +1583,7 @@ fn adwNotifyDark(
|
||||
style_manager: *adw.StyleManager,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *App,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const color_scheme: apprt.ColorScheme = if (style_manager.getDark() == 0)
|
||||
.light
|
||||
else
|
||||
@@ -1600,6 +1677,27 @@ fn gtkActionPresentSurface(
|
||||
);
|
||||
}
|
||||
|
||||
fn gtkActionShowGTKInspector(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *App,
|
||||
) callconv(.c) void {
|
||||
self.core_app.performAction(self, .show_gtk_inspector) catch |err| {
|
||||
log.err("error showing GTK inspector err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkActionNewWindow(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *App,
|
||||
) callconv(.c) void {
|
||||
log.info("received new window action", .{});
|
||||
_ = self.core_app.mailbox.push(.{
|
||||
.new_window = .{},
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
/// This is called to setup the action map that this application supports.
|
||||
/// This should be called only once on startup.
|
||||
fn initActions(self: *App) void {
|
||||
@@ -1618,7 +1716,10 @@ fn initActions(self: *App) void {
|
||||
.{ "open-config", gtkActionOpenConfig, null },
|
||||
.{ "reload-config", gtkActionReloadConfig, null },
|
||||
.{ "present-surface", gtkActionPresentSurface, t },
|
||||
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
|
||||
.{ "new-window", gtkActionNewWindow, null },
|
||||
};
|
||||
|
||||
inline for (actions) |entry| {
|
||||
const action = gio.SimpleAction.new(entry[0], entry[2]);
|
||||
defer action.unref();
|
||||
@@ -1633,32 +1734,3 @@ fn initActions(self: *App) void {
|
||||
action_map.addAction(action.as(gio.Action));
|
||||
}
|
||||
}
|
||||
|
||||
fn isValidAppId(app_id: [:0]const u8) bool {
|
||||
if (app_id.len > 255 or app_id.len == 0) return false;
|
||||
if (app_id[0] == '.') return false;
|
||||
if (app_id[app_id.len - 1] == '.') return false;
|
||||
|
||||
var hasDot = false;
|
||||
for (app_id) |char| {
|
||||
switch (char) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {},
|
||||
'.' => hasDot = true,
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
if (!hasDot) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
test "isValidAppId" {
|
||||
try testing.expect(isValidAppId("foo.bar"));
|
||||
try testing.expect(isValidAppId("foo.bar.baz"));
|
||||
try testing.expect(!isValidAppId("foo"));
|
||||
try testing.expect(!isValidAppId("foo.bar?"));
|
||||
try testing.expect(!isValidAppId("foo."));
|
||||
try testing.expect(!isValidAppId(".foo"));
|
||||
try testing.expect(!isValidAppId(""));
|
||||
try testing.expect(!isValidAppId("foo" ** 86));
|
||||
}
|
||||
|
||||
@@ -18,88 +18,37 @@ pub fn init(
|
||||
/// The minor version of the minimum Adwaita version that is required to use
|
||||
/// this resource.
|
||||
comptime minor: u16,
|
||||
/// `blp` signifies that the resource is a Blueprint that has been compiled
|
||||
/// to GTK Builder XML at compile time. `ui` signifies that the resource is
|
||||
/// a GTK Builder XML file that is included in the Ghostty source (perhaps
|
||||
/// because the Blueprint compiler on some target platforms cannot compile a
|
||||
/// Blueprint that generates the necessary resources).
|
||||
comptime kind: enum { blp, ui },
|
||||
) Builder {
|
||||
const resource_path = comptime resource_path: {
|
||||
const gresource = @import("gresource.zig");
|
||||
switch (kind) {
|
||||
.blp => {
|
||||
// Check to make sure that our file is listed as a
|
||||
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
|
||||
// could crash at runtime when we try and load a nonexistent
|
||||
// GResource.
|
||||
for (gresource.blueprint_files) |file| {
|
||||
if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
|
||||
// Use @embedFile to make sure that the `.blp` file exists
|
||||
// at compile time. Zig _should_ discard the data so that
|
||||
// it doesn't end up in the final executable. At runtime we
|
||||
// will load the data from a GResource.
|
||||
const blp_filename = std.fmt.comptimePrint(
|
||||
"ui/{d}.{d}/{s}.blp",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
_ = @embedFile(blp_filename);
|
||||
break :resource_path std.fmt.comptimePrint(
|
||||
"/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
||||
},
|
||||
.ui => {
|
||||
// Check to make sure that our file is listed as a `ui_file` in
|
||||
// `gresource.zig`. If it isn't Ghostty could crash at runtime
|
||||
// when we try and load a nonexistent GResource.
|
||||
for (gresource.ui_files) |file| {
|
||||
if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
|
||||
// Use @embedFile to make sure that the `.ui` file exists
|
||||
// at compile time. Zig _should_ discard the data so that
|
||||
// it doesn't end up in the final executable. At runtime we
|
||||
// will load the data from a GResource.
|
||||
const ui_filename = std.fmt.comptimePrint(
|
||||
"ui/{d}.{d}/{s}.ui",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
_ = @embedFile(ui_filename);
|
||||
// Also use @embedFile to make sure that a matching `.blp`
|
||||
// file exists at compile time. Zig _should_ discard the
|
||||
// data so that it doesn't end up in the final executable.
|
||||
const blp_filename = std.fmt.comptimePrint(
|
||||
"ui/{d}.{d}/{s}.blp",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
_ = @embedFile(blp_filename);
|
||||
break :resource_path std.fmt.comptimePrint(
|
||||
"/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
} else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
|
||||
},
|
||||
}
|
||||
// Check to make sure that our file is listed as a
|
||||
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
|
||||
// could crash at runtime when we try and load a nonexistent
|
||||
// GResource.
|
||||
for (gresource.blueprint_files) |file| {
|
||||
if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
|
||||
// Use @embedFile to make sure that the `.blp` file exists
|
||||
// at compile time. Zig _should_ discard the data so that
|
||||
// it doesn't end up in the final executable. At runtime we
|
||||
// will load the data from a GResource.
|
||||
const blp_filename = std.fmt.comptimePrint(
|
||||
"ui/{d}.{d}/{s}.blp",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
_ = @embedFile(blp_filename);
|
||||
break :resource_path std.fmt.comptimePrint(
|
||||
"/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
||||
};
|
||||
|
||||
return .{
|
||||
|
||||
@@ -69,16 +69,16 @@ fn init(
|
||||
request: apprt.ClipboardRequest,
|
||||
is_secure_input: bool,
|
||||
) !void {
|
||||
var builder = switch (DialogType) {
|
||||
var builder: Builder = switch (DialogType) {
|
||||
adw.AlertDialog => switch (request) {
|
||||
.osc_52_read => Builder.init("ccw-osc-52-read", 1, 5, .blp),
|
||||
.osc_52_write => Builder.init("ccw-osc-52-write", 1, 5, .blp),
|
||||
.paste => Builder.init("ccw-paste", 1, 5, .blp),
|
||||
.osc_52_read => .init("ccw-osc-52-read", 1, 5),
|
||||
.osc_52_write => .init("ccw-osc-52-write", 1, 5),
|
||||
.paste => .init("ccw-paste", 1, 5),
|
||||
},
|
||||
adw.MessageDialog => switch (request) {
|
||||
.osc_52_read => Builder.init("ccw-osc-52-read", 1, 2, .ui),
|
||||
.osc_52_write => Builder.init("ccw-osc-52-write", 1, 2, .ui),
|
||||
.paste => Builder.init("ccw-paste", 1, 2, .ui),
|
||||
.osc_52_read => .init("ccw-osc-52-read", 1, 2),
|
||||
.osc_52_write => .init("ccw-osc-52-write", 1, 2),
|
||||
.paste => .init("ccw-paste", 1, 2),
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
@@ -152,7 +152,7 @@ fn init(
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void {
|
||||
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
if (std.mem.orderZ(u8, response, "ok") == .eq) {
|
||||
self.core_surface.completeClipboardRequest(
|
||||
self.pending_req,
|
||||
@@ -165,7 +165,7 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation)
|
||||
self.destroy();
|
||||
}
|
||||
|
||||
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void {
|
||||
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
|
||||
self.text_view.as(gtk.Widget).removeCssClass("blurred");
|
||||
|
||||
@@ -173,7 +173,7 @@ fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv
|
||||
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
|
||||
}
|
||||
|
||||
fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void {
|
||||
fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
|
||||
self.text_view.as(gtk.Widget).addCssClass("blurred");
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ fn responseCallback(
|
||||
_: *DialogType,
|
||||
response: [*:0]const u8,
|
||||
target: *Target,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const alloc = target.allocator();
|
||||
defer alloc.destroy(target);
|
||||
|
||||
@@ -141,7 +141,7 @@ pub const Target = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.C) c_int {
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
|
||||
|
||||
// Confusingly, `isActive` returns 1 when active,
|
||||
|
||||
252
src/apprt/gtk/CommandPalette.zig
Normal file
252
src/apprt/gtk/CommandPalette.zig
Normal file
@@ -0,0 +1,252 @@
|
||||
const CommandPalette = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
const inputpkg = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const log = std.log.scoped(.command_palette);
|
||||
|
||||
window: *Window,
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
/// The dialog object containing the palette UI.
|
||||
dialog: *adw.Dialog,
|
||||
|
||||
/// The search input text field.
|
||||
search: *gtk.SearchEntry,
|
||||
|
||||
/// The view containing each result row.
|
||||
view: *gtk.ListView,
|
||||
|
||||
/// The model that provides filtered data for the view to display.
|
||||
model: *gtk.SingleSelection,
|
||||
|
||||
/// The list that serves as the data source of the model.
|
||||
/// This is where all command data is ultimately stored.
|
||||
source: *gio.ListStore,
|
||||
|
||||
pub fn init(self: *CommandPalette, window: *Window) !void {
|
||||
// Register the custom command type *before* initializing the builder
|
||||
// If we don't do this now, the builder will complain that it doesn't know
|
||||
// about this type and fail to initialize
|
||||
_ = Command.getGObjectType();
|
||||
|
||||
var builder = Builder.init("command-palette", 1, 5);
|
||||
defer builder.deinit();
|
||||
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.arena = .init(window.app.core_app.alloc),
|
||||
.dialog = builder.getObject(adw.Dialog, "command-palette").?,
|
||||
.search = builder.getObject(gtk.SearchEntry, "search").?,
|
||||
.view = builder.getObject(gtk.ListView, "view").?,
|
||||
.model = builder.getObject(gtk.SingleSelection, "model").?,
|
||||
.source = builder.getObject(gio.ListStore, "source").?,
|
||||
};
|
||||
|
||||
// Manually take a reference here so that the dialog
|
||||
// remains in memory after closing
|
||||
self.dialog.ref();
|
||||
errdefer self.dialog.unref();
|
||||
|
||||
_ = gtk.SearchEntry.signals.stop_search.connect(
|
||||
self.search,
|
||||
*CommandPalette,
|
||||
searchStopped,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
_ = gtk.SearchEntry.signals.activate.connect(
|
||||
self.search,
|
||||
*CommandPalette,
|
||||
searchActivated,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
_ = gtk.ListView.signals.activate.connect(
|
||||
self.view,
|
||||
*CommandPalette,
|
||||
rowActivated,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
try self.updateConfig(&self.window.app.config);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CommandPalette) void {
|
||||
self.arena.deinit();
|
||||
self.dialog.unref();
|
||||
}
|
||||
|
||||
pub fn toggle(self: *CommandPalette) void {
|
||||
self.dialog.present(self.window.window.as(gtk.Widget));
|
||||
// Focus on the search bar when opening the dialog
|
||||
_ = self.search.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void {
|
||||
// Clear existing binds and clear allocated data
|
||||
self.source.removeAll();
|
||||
_ = self.arena.reset(.retain_capacity);
|
||||
|
||||
for (config.@"command-palette-entry".value.items) |command| {
|
||||
// Filter out actions that are not implemented
|
||||
// or don't make sense for GTK
|
||||
switch (command.action) {
|
||||
.close_all_windows,
|
||||
.toggle_secure_input,
|
||||
.check_for_updates,
|
||||
.redo,
|
||||
.undo,
|
||||
.reset_window_size,
|
||||
.toggle_window_float_on_top,
|
||||
=> continue,
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
const cmd = try Command.new(
|
||||
self.arena.allocator(),
|
||||
command,
|
||||
config.keybind.set,
|
||||
);
|
||||
const cmd_ref = cmd.as(gobject.Object);
|
||||
self.source.append(cmd_ref);
|
||||
cmd_ref.unref();
|
||||
}
|
||||
}
|
||||
|
||||
fn activated(self: *CommandPalette, pos: c_uint) void {
|
||||
// Use self.model and not self.source here to use the list of *visible* results
|
||||
const object = self.model.as(gio.ListModel).getObject(pos) orelse return;
|
||||
const cmd = gobject.ext.cast(Command, object) orelse return;
|
||||
|
||||
// Close before running the action in order to avoid being replaced by another
|
||||
// dialog (such as the change title dialog). If that occurs then the command
|
||||
// palette dialog won't be counted as having closed properly and cannot
|
||||
// receive focus when reopened.
|
||||
_ = self.dialog.close();
|
||||
|
||||
const action = inputpkg.Binding.Action.parse(
|
||||
std.mem.span(cmd.cmd_c.action_key),
|
||||
) catch |err| {
|
||||
log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err });
|
||||
return;
|
||||
};
|
||||
|
||||
self.window.performBindingAction(action);
|
||||
}
|
||||
|
||||
fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
|
||||
// ESC was pressed - close the palette
|
||||
_ = self.dialog.close();
|
||||
}
|
||||
|
||||
fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
|
||||
// If Enter is pressed, activate the selected entry
|
||||
self.activated(self.model.getSelected());
|
||||
}
|
||||
|
||||
fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
|
||||
self.activated(pos);
|
||||
}
|
||||
|
||||
/// Object that wraps around a command.
|
||||
///
|
||||
/// As GTK list models only accept objects that are within the GObject hierarchy,
|
||||
/// we have to construct a wrapper to be easily consumed by the list model.
|
||||
const Command = extern struct {
|
||||
parent: Parent,
|
||||
cmd_c: inputpkg.Command.C,
|
||||
|
||||
pub const getGObjectType = gobject.ext.defineClass(Command, .{
|
||||
.name = "GhosttyCommand",
|
||||
.classInit = Class.init,
|
||||
});
|
||||
|
||||
pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command {
|
||||
const self = gobject.ext.newInstance(Command, .{});
|
||||
var buf: [64]u8 = undefined;
|
||||
|
||||
const action = action: {
|
||||
const trigger = keybinds.getTrigger(cmd.action) orelse break :action null;
|
||||
const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null;
|
||||
break :action try alloc.dupeZ(u8, accel);
|
||||
};
|
||||
|
||||
self.cmd_c = .{
|
||||
.title = cmd.title.ptr,
|
||||
.description = cmd.description.ptr,
|
||||
.action = if (action) |v| v.ptr else "",
|
||||
.action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}),
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn as(self: *Command, comptime T: type) *T {
|
||||
return gobject.ext.as(T, self);
|
||||
}
|
||||
|
||||
pub const Parent = gobject.Object;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent: Parent.Class,
|
||||
|
||||
pub const Instance = Command;
|
||||
|
||||
pub fn init(class: *Class) callconv(.c) void {
|
||||
const info = @typeInfo(inputpkg.Command.C).@"struct";
|
||||
|
||||
// Expose all fields on the Command.C struct as properties
|
||||
// that can be accessed by the GObject type system
|
||||
// (and by extension, blueprints)
|
||||
const properties = comptime props: {
|
||||
var props: [info.fields.len]type = undefined;
|
||||
|
||||
for (info.fields, 0..) |field, i| {
|
||||
const accessor = struct {
|
||||
fn getter(cmd: *Command) ?[:0]const u8 {
|
||||
return std.mem.span(@field(cmd.cmd_c, field.name));
|
||||
}
|
||||
};
|
||||
|
||||
// "Canonicalize" field names into the format GObject expects
|
||||
const prop_name = prop_name: {
|
||||
var buf: [field.name.len:0]u8 = undefined;
|
||||
_ = std.mem.replace(u8, field.name, "_", "-", &buf);
|
||||
break :prop_name buf;
|
||||
};
|
||||
|
||||
props[i] = gobject.ext.defineProperty(
|
||||
&prop_name,
|
||||
Command,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.default = null,
|
||||
.accessor = .{ .getter = &accessor.getter },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break :props props;
|
||||
};
|
||||
|
||||
gobject.ext.registerProperties(class, &properties);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -29,15 +29,38 @@ error_message: *gtk.TextBuffer,
|
||||
pub fn maybePresent(app: *App, window: ?*Window) void {
|
||||
if (app.config._diagnostics.empty()) return;
|
||||
|
||||
var builder = switch (DialogType) {
|
||||
adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5, .blp),
|
||||
adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2, .ui),
|
||||
else => unreachable,
|
||||
};
|
||||
defer builder.deinit();
|
||||
const config_errors_dialog = config_errors_dialog: {
|
||||
if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog;
|
||||
|
||||
const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
|
||||
const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
|
||||
var builder: Builder = switch (DialogType) {
|
||||
adw.AlertDialog => .init("config-errors-dialog", 1, 5),
|
||||
adw.MessageDialog => .init("config-errors-dialog", 1, 2),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
|
||||
const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
|
||||
|
||||
app.config_errors_dialog = .{
|
||||
.builder = builder,
|
||||
.dialog = dialog,
|
||||
.error_message = error_message,
|
||||
};
|
||||
|
||||
break :config_errors_dialog app.config_errors_dialog.?;
|
||||
};
|
||||
|
||||
{
|
||||
var start = std.mem.zeroes(gtk.TextIter);
|
||||
config_errors_dialog.error_message.getStartIter(&start);
|
||||
|
||||
var end = std.mem.zeroes(gtk.TextIter);
|
||||
config_errors_dialog.error_message.getEndIter(&end);
|
||||
|
||||
config_errors_dialog.error_message.delete(&start, &end);
|
||||
}
|
||||
|
||||
var msg_buf: [4095:0]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&msg_buf);
|
||||
@@ -52,22 +75,24 @@ pub fn maybePresent(app: *App, window: ?*Window) void {
|
||||
continue;
|
||||
};
|
||||
|
||||
error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
|
||||
error_message.insertAtCursor("\n", 1);
|
||||
config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
|
||||
config_errors_dialog.error_message.insertAtCursor("\n", 1);
|
||||
}
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
|
||||
|
||||
const parent = if (window) |w| w.window.as(gtk.Widget) else null;
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => dialog.as(adw.Dialog).present(parent),
|
||||
adw.MessageDialog => dialog.as(gtk.Window).present(),
|
||||
adw.AlertDialog => {
|
||||
const parent = if (window) |w| w.window.as(gtk.Widget) else null;
|
||||
config_errors_dialog.dialog.as(adw.Dialog).present(parent);
|
||||
},
|
||||
adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.C) void {
|
||||
fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void {
|
||||
if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit();
|
||||
app.config_errors_dialog = null;
|
||||
|
||||
if (std.mem.orderZ(u8, response, "reload") == .eq) {
|
||||
app.reloadConfig(.app, .{}) catch |err| {
|
||||
log.warn("error reloading config error={}", .{err});
|
||||
|
||||
421
src/apprt/gtk/GlobalShortcuts.zig
Normal file
421
src/apprt/gtk/GlobalShortcuts.zig
Normal file
@@ -0,0 +1,421 @@
|
||||
const GlobalShortcuts = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Binding = @import("../../input.zig").Binding;
|
||||
const key = @import("key.zig");
|
||||
|
||||
const log = std.log.scoped(.global_shortcuts);
|
||||
const Token = [16]u8;
|
||||
|
||||
app: *App,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
dbus: *gio.DBusConnection,
|
||||
|
||||
/// A mapping from a unique ID to an action.
|
||||
/// Currently the unique ID is simply the serialized representation of the
|
||||
/// trigger that was used for the action as triggers are unique in the keymap,
|
||||
/// but this may change in the future.
|
||||
map: std.StringArrayHashMapUnmanaged(Binding.Action) = .{},
|
||||
|
||||
/// The handle of the current global shortcuts portal session,
|
||||
/// as a D-Bus object path.
|
||||
handle: ?[:0]const u8 = null,
|
||||
|
||||
/// The D-Bus signal subscription for the response signal on requests.
|
||||
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
|
||||
response_subscription: c_uint = 0,
|
||||
|
||||
/// The D-Bus signal subscription for the keybind activate signal.
|
||||
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
|
||||
activate_subscription: c_uint = 0,
|
||||
|
||||
pub fn init(alloc: Allocator, gio_app: *gio.Application) ?GlobalShortcuts {
|
||||
const dbus = gio_app.getDbusConnection() orelse return null;
|
||||
|
||||
return .{
|
||||
// To be initialized later
|
||||
.app = undefined,
|
||||
.arena = .init(alloc),
|
||||
.dbus = dbus,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *GlobalShortcuts) void {
|
||||
self.close();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
fn close(self: *GlobalShortcuts) void {
|
||||
if (self.response_subscription != 0) {
|
||||
self.dbus.signalUnsubscribe(self.response_subscription);
|
||||
self.response_subscription = 0;
|
||||
}
|
||||
|
||||
if (self.activate_subscription != 0) {
|
||||
self.dbus.signalUnsubscribe(self.activate_subscription);
|
||||
self.activate_subscription = 0;
|
||||
}
|
||||
|
||||
if (self.handle) |handle| {
|
||||
// Close existing session
|
||||
self.dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
handle,
|
||||
"org.freedesktop.portal.Session",
|
||||
"Close",
|
||||
null,
|
||||
null,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
self.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void {
|
||||
// Ensure we have a valid reference to the app
|
||||
// (it was left uninitialized in `init`)
|
||||
self.app = app;
|
||||
|
||||
// Close any existing sessions
|
||||
self.close();
|
||||
|
||||
// Update map
|
||||
var trigger_buf: [256]u8 = undefined;
|
||||
|
||||
self.map.clearRetainingCapacity();
|
||||
var it = self.app.config.keybind.set.bindings.iterator();
|
||||
|
||||
while (it.next()) |entry| {
|
||||
const leaf = switch (entry.value_ptr.*) {
|
||||
// Global shortcuts can't have leaders
|
||||
.leader => continue,
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
if (!leaf.flags.global) continue;
|
||||
|
||||
const trigger = try key.xdgShortcutFromTrigger(
|
||||
&trigger_buf,
|
||||
entry.key_ptr.*,
|
||||
) orelse continue;
|
||||
|
||||
try self.map.put(
|
||||
self.arena.allocator(),
|
||||
try self.arena.allocator().dupeZ(u8, trigger),
|
||||
leaf.action,
|
||||
);
|
||||
}
|
||||
|
||||
if (self.map.count() > 0) {
|
||||
try self.request(.create_session);
|
||||
}
|
||||
}
|
||||
|
||||
fn shortcutActivated(
|
||||
_: *gio.DBusConnection,
|
||||
_: ?[*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
params: *glib.Variant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const self: *GlobalShortcuts = @ptrCast(@alignCast(ud));
|
||||
|
||||
// 2nd value in the tuple is the activated shortcut ID
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated
|
||||
var shortcut_id: [*:0]const u8 = undefined;
|
||||
params.getChild(1, "&s", &shortcut_id);
|
||||
log.debug("activated={s}", .{shortcut_id});
|
||||
|
||||
const action = self.map.get(std.mem.span(shortcut_id)) orelse return;
|
||||
|
||||
self.app.core_app.performAllAction(self.app, action) catch |err| {
|
||||
log.err("failed to perform action={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
const Method = enum {
|
||||
create_session,
|
||||
bind_shortcuts,
|
||||
|
||||
fn name(self: Method) [:0]const u8 {
|
||||
return switch (self) {
|
||||
.create_session => "CreateSession",
|
||||
.bind_shortcuts => "BindShortcuts",
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct the payload expected by the XDG portal call.
|
||||
fn makePayload(
|
||||
self: Method,
|
||||
shortcuts: *GlobalShortcuts,
|
||||
request_token: [:0]const u8,
|
||||
) ?*glib.Variant {
|
||||
switch (self) {
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
|
||||
.create_session => {
|
||||
var session_token: Token = undefined;
|
||||
return glib.Variant.newParsed(
|
||||
"({'handle_token': <%s>, 'session_handle_token': <%s>},)",
|
||||
request_token.ptr,
|
||||
generateToken(&session_token).ptr,
|
||||
);
|
||||
},
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
|
||||
.bind_shortcuts => {
|
||||
const handle = shortcuts.handle orelse return null;
|
||||
|
||||
const bind_type = glib.VariantType.new("a(sa{sv})");
|
||||
defer glib.free(bind_type);
|
||||
|
||||
var binds: glib.VariantBuilder = undefined;
|
||||
glib.VariantBuilder.init(&binds, bind_type);
|
||||
|
||||
var action_buf: [256]u8 = undefined;
|
||||
|
||||
var it = shortcuts.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const trigger = entry.key_ptr.*.ptr;
|
||||
const action = std.fmt.bufPrintZ(
|
||||
&action_buf,
|
||||
"{}",
|
||||
.{entry.value_ptr.*},
|
||||
) catch continue;
|
||||
|
||||
binds.addParsed(
|
||||
"(%s, {'description': <%s>, 'preferred_trigger': <%s>})",
|
||||
trigger,
|
||||
action.ptr,
|
||||
trigger,
|
||||
);
|
||||
}
|
||||
|
||||
return glib.Variant.newParsed(
|
||||
"(%o, %*, '', {'handle_token': <%s>})",
|
||||
handle.ptr,
|
||||
binds.end(),
|
||||
request_token.ptr,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn onResponse(self: Method, shortcuts: *GlobalShortcuts, vardict: *glib.Variant) void {
|
||||
switch (self) {
|
||||
.create_session => {
|
||||
var handle: ?[*:0]u8 = null;
|
||||
if (vardict.lookup("session_handle", "&s", &handle) == 0) {
|
||||
log.err(
|
||||
"session handle not found in response={s}",
|
||||
.{vardict.print(@intFromBool(true))},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
shortcuts.handle = shortcuts.arena.allocator().dupeZ(u8, std.mem.span(handle.?)) catch {
|
||||
log.err("out of memory: failed to clone session handle", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
log.debug("session_handle={?s}", .{handle});
|
||||
|
||||
// Subscribe to keybind activations
|
||||
shortcuts.activate_subscription = shortcuts.dbus.signalSubscribe(
|
||||
null,
|
||||
"org.freedesktop.portal.GlobalShortcuts",
|
||||
"Activated",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
handle,
|
||||
.{ .match_arg0_path = true },
|
||||
shortcutActivated,
|
||||
shortcuts,
|
||||
null,
|
||||
);
|
||||
|
||||
shortcuts.request(.bind_shortcuts) catch |err| {
|
||||
log.err("failed to bind shortcuts={}", .{err});
|
||||
return;
|
||||
};
|
||||
},
|
||||
.bind_shortcuts => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Submit a request to the global shortcuts portal.
|
||||
fn request(
|
||||
self: *GlobalShortcuts,
|
||||
comptime method: Method,
|
||||
) !void {
|
||||
// NOTE(pluiedev):
|
||||
// XDG Portals are really, really poorly-designed pieces of hot garbage.
|
||||
// How the protocol is _initially_ designed to work is as follows:
|
||||
//
|
||||
// 1. The client calls a method which returns the path of a Request object;
|
||||
// 2. The client waits for the Response signal under said object path;
|
||||
// 3. When the signal arrives, the actual return value and status code
|
||||
// become available for the client for further processing.
|
||||
//
|
||||
// THIS DOES NOT WORK. Once the first two steps are complete, the client
|
||||
// needs to immediately start listening for the third step, but an overeager
|
||||
// server implementation could easily send the Response signal before the
|
||||
// client is even ready, causing communications to break down over a simple
|
||||
// race condition/two generals' problem that even _TCP_ had figured out
|
||||
// decades ago. Worse yet, you get exactly _one_ chance to listen for the
|
||||
// signal, or else your communication attempt so far has all been in vain.
|
||||
//
|
||||
// And they know this. Instead of fixing their freaking protocol, they just
|
||||
// ask clients to manually construct the expected object path and subscribe
|
||||
// to the request signal beforehand, making the whole response value of
|
||||
// the original call COMPLETELY MEANINGLESS.
|
||||
//
|
||||
// Furthermore, this is _entirely undocumented_ aside from one tiny
|
||||
// paragraph under the documentation for the Request interface, and
|
||||
// anyone would be forgiven for missing it without reading the libportal
|
||||
// source code.
|
||||
//
|
||||
// When in Rome, do as the Romans do, I guess...?
|
||||
|
||||
const callbacks = struct {
|
||||
fn gotResponseHandle(
|
||||
source: ?*gobject.Object,
|
||||
res: *gio.AsyncResult,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const dbus_ = gobject.ext.cast(gio.DBusConnection, source.?).?;
|
||||
|
||||
var err: ?*glib.Error = null;
|
||||
defer if (err) |err_| err_.free();
|
||||
|
||||
const params_ = dbus_.callFinish(res, &err) orelse {
|
||||
if (err) |err_| log.err("request failed={s} ({})", .{
|
||||
err_.f_message orelse "(unknown)",
|
||||
err_.f_code,
|
||||
});
|
||||
return;
|
||||
};
|
||||
defer params_.unref();
|
||||
|
||||
// TODO: XDG recommends updating the signal subscription if the actual
|
||||
// returned request path is not the same as the expected request
|
||||
// path, to retain compatibility with older versions of XDG portals.
|
||||
// Although it suffers from the race condition outlined above,
|
||||
// we should still implement this at some point.
|
||||
}
|
||||
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response
|
||||
fn responded(
|
||||
dbus: *gio.DBusConnection,
|
||||
_: ?[*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
_: [*:0]const u8,
|
||||
params_: *glib.Variant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const self_: *GlobalShortcuts = @ptrCast(@alignCast(ud));
|
||||
|
||||
// Unsubscribe from the response signal
|
||||
if (self_.response_subscription != 0) {
|
||||
dbus.signalUnsubscribe(self_.response_subscription);
|
||||
self_.response_subscription = 0;
|
||||
}
|
||||
|
||||
var response: u32 = 0;
|
||||
var vardict: ?*glib.Variant = null;
|
||||
params_.get("(u@a{sv})", &response, &vardict);
|
||||
|
||||
switch (response) {
|
||||
0 => {
|
||||
log.debug("request successful", .{});
|
||||
method.onResponse(self_, vardict.?);
|
||||
},
|
||||
1 => log.debug("request was cancelled by user", .{}),
|
||||
2 => log.warn("request ended unexpectedly", .{}),
|
||||
else => log.err("unrecognized response code={}", .{response}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var request_token_buf: Token = undefined;
|
||||
const request_token = generateToken(&request_token_buf);
|
||||
|
||||
const payload = method.makePayload(self, request_token) orelse return;
|
||||
const request_path = try self.getRequestPath(request_token);
|
||||
|
||||
self.response_subscription = self.dbus.signalSubscribe(
|
||||
null,
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
request_path,
|
||||
null,
|
||||
.{},
|
||||
callbacks.responded,
|
||||
self,
|
||||
null,
|
||||
);
|
||||
|
||||
self.dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.GlobalShortcuts",
|
||||
method.name(),
|
||||
payload,
|
||||
null,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
callbacks.gotResponseHandle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate a random token suitable for use in requests.
|
||||
fn generateToken(buf: *Token) [:0]const u8 {
|
||||
// u28 takes up 7 bytes in hex, 8 bytes for "ghostty_" and 1 byte for NUL
|
||||
// 7 + 8 + 1 = 16
|
||||
return std.fmt.bufPrintZ(
|
||||
buf,
|
||||
"ghostty_{x:0<7}",
|
||||
.{std.crypto.random.int(u28)},
|
||||
) catch unreachable;
|
||||
}
|
||||
|
||||
/// Get the XDG portal request path for the current Ghostty instance.
|
||||
///
|
||||
/// If this sounds like nonsense, see `request` for an explanation as to
|
||||
/// why we need to do this.
|
||||
fn getRequestPath(self: *GlobalShortcuts, token: [:0]const u8) ![:0]const u8 {
|
||||
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
|
||||
// for the syntax XDG portals expect.
|
||||
|
||||
// `getUniqueName` should never return null here as we're using an ordinary
|
||||
// message bus connection. If it doesn't, something is very wrong
|
||||
const unique_name = std.mem.span(self.dbus.getUniqueName().?);
|
||||
|
||||
const object_path = try std.mem.joinZ(self.arena.allocator(), "/", &.{
|
||||
"/org/freedesktop/portal/desktop/request",
|
||||
unique_name[1..], // Remove leading `:`
|
||||
token,
|
||||
});
|
||||
|
||||
// Sanitize the unique name by replacing every `.` with `_`.
|
||||
// In effect, this will turn a unique name like `:1.192` into `1_192`.
|
||||
// Valid D-Bus object path components never contain `.`s anyway, so we're
|
||||
// free to replace all instances of `.` here and avoid extra allocation.
|
||||
std.mem.replaceScalar(u8, object_path, '.', '_');
|
||||
|
||||
return object_path;
|
||||
}
|
||||
@@ -221,12 +221,12 @@ fn translateMouseButton(button: c_uint) ?c_int {
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
|
||||
fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
log.debug("imgui widget destroy", .{});
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
|
||||
fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
log.debug("gl surface realized", .{});
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
@@ -242,7 +242,7 @@ fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
|
||||
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
|
||||
}
|
||||
|
||||
fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
|
||||
fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
_ = area;
|
||||
log.debug("gl surface unrealized", .{});
|
||||
|
||||
@@ -250,7 +250,7 @@ fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
|
||||
cimgui.ImGui_ImplOpenGL3_Shutdown();
|
||||
}
|
||||
|
||||
fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.C) void {
|
||||
fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor = area.as(gtk.Widget).getScaleFactor();
|
||||
@@ -273,7 +273,7 @@ fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget)
|
||||
active_style.* = style.*;
|
||||
}
|
||||
|
||||
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.C) c_int {
|
||||
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
|
||||
// Setup our frame. We render twice because some ImGui behaviors
|
||||
@@ -307,7 +307,7 @@ fn gtkMouseMotion(
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
|
||||
@@ -325,7 +325,7 @@ fn gtkMouseDown(
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
@@ -343,7 +343,7 @@ fn gtkMouseUp(
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
@@ -359,7 +359,7 @@ fn gtkMouseScroll(
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
@@ -373,7 +373,7 @@ fn gtkMouseScroll(
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) void {
|
||||
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
@@ -381,7 +381,7 @@ fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C)
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
|
||||
}
|
||||
|
||||
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) void {
|
||||
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
@@ -393,7 +393,7 @@ fn gtkInputCommit(
|
||||
_: *gtk.IMMulticontext,
|
||||
bytes: [*:0]u8,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
@@ -407,7 +407,7 @@ fn gtkKeyPressed(
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
return @intFromBool(self.keyEvent(
|
||||
.press,
|
||||
ec_key,
|
||||
@@ -423,7 +423,7 @@ fn gtkKeyReleased(
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
_ = self.keyEvent(
|
||||
.release,
|
||||
ec_key,
|
||||
|
||||
@@ -50,12 +50,12 @@ first: bool = true,
|
||||
pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
|
||||
self.* = .{
|
||||
.surface = surface,
|
||||
.config = DerivedConfig.init(config),
|
||||
.config = .init(config),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
|
||||
self.config = DerivedConfig.init(config);
|
||||
self.config = .init(config);
|
||||
}
|
||||
|
||||
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
|
||||
@@ -104,7 +104,7 @@ pub fn maybeShow(self: *ResizeOverlay) void {
|
||||
|
||||
/// Actually update the overlay widget. This should only be called from a GTK
|
||||
/// idle handler.
|
||||
fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c_int {
|
||||
fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||
|
||||
// No matter what our idler is complete with this callback
|
||||
@@ -198,7 +198,7 @@ fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
|
||||
|
||||
/// If this fires, it means that the delay period has expired and the resize
|
||||
/// overlay widget should be hidden.
|
||||
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.C) c_int {
|
||||
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||
self.timer = null;
|
||||
if (self.label) |label| hide(label);
|
||||
|
||||
@@ -138,7 +138,7 @@ pub fn init(
|
||||
.container = container,
|
||||
.top_left = .{ .surface = tl },
|
||||
.bottom_right = .{ .surface = br },
|
||||
.orientation = Orientation.fromDirection(direction),
|
||||
.orientation = .fromDirection(direction),
|
||||
};
|
||||
|
||||
// Replace the previous containers element with our split. This allows a
|
||||
|
||||
@@ -41,10 +41,6 @@ const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_surface);
|
||||
|
||||
/// This is detected by the OpenGL renderer to move to a single-threaded
|
||||
/// draw operation. This basically puts locks around our draw path.
|
||||
pub const opengl_single_threaded_draw = true;
|
||||
|
||||
pub const Options = struct {
|
||||
/// The parent surface to inherit settings such as font size, working
|
||||
/// directory, etc. from.
|
||||
@@ -394,7 +390,10 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
|
||||
// Various other GL properties
|
||||
gl_area_widget.setCursorFromName("text");
|
||||
gl_area.setRequiredVersion(3, 3);
|
||||
gl_area.setRequiredVersion(
|
||||
renderer.OpenGL.MIN_VERSION_MAJOR,
|
||||
renderer.OpenGL.MIN_VERSION_MINOR,
|
||||
);
|
||||
gl_area.setHasStencilBuffer(0);
|
||||
gl_area.setHasDepthBuffer(0);
|
||||
gl_area.setUseEs(0);
|
||||
@@ -683,12 +682,13 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
|
||||
fn realize(self: *Surface) !void {
|
||||
// If this surface has already been realized, then we don't need to
|
||||
// reinitialize. This can happen if a surface is moved from one GDK surface
|
||||
// to another (i.e. a tab is pulled out into a window).
|
||||
// reinitialize. This can happen if a surface is moved from one GDK
|
||||
// surface to another (i.e. a tab is pulled out into a window).
|
||||
if (self.realized) {
|
||||
// If we have no OpenGL state though, we do need to reinitialize.
|
||||
// We allow the renderer to figure that out
|
||||
try self.core_surface.renderer.displayRealize();
|
||||
// We allow the renderer to figure that out, and then queue a draw.
|
||||
try self.core_surface.renderer.displayRealized();
|
||||
self.redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -746,7 +746,21 @@ pub fn deinit(self: *Surface) void {
|
||||
self.core_surface.deinit();
|
||||
self.core_surface = undefined;
|
||||
|
||||
if (self.cgroup_path) |path| self.app.core_app.alloc.free(path);
|
||||
// Remove the cgroup if we have one. We do this after deiniting the core
|
||||
// surface to ensure all processes have exited.
|
||||
if (self.cgroup_path) |path| {
|
||||
internal_os.cgroup.remove(path) catch |err| {
|
||||
// We don't want this to be fatal in any way so we just log
|
||||
// and continue. A dangling empty cgroup is not a big deal
|
||||
// and this should be rare.
|
||||
log.warn(
|
||||
"failed to remove cgroup for surface path={s} err={}",
|
||||
.{ path, err },
|
||||
);
|
||||
};
|
||||
|
||||
self.app.core_app.alloc.free(path);
|
||||
}
|
||||
|
||||
// Free all our GTK stuff
|
||||
//
|
||||
@@ -780,7 +794,7 @@ pub fn primaryWidget(self: *Surface) *gtk.Widget {
|
||||
}
|
||||
|
||||
fn render(self: *Surface) !void {
|
||||
try self.core_surface.renderer.drawFrame(self);
|
||||
try self.core_surface.renderer.drawFrame(true);
|
||||
}
|
||||
|
||||
/// Called by core surface to get the cgroup.
|
||||
@@ -1025,7 +1039,7 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo
|
||||
self.update_title_timer = glib.timeoutAdd(75, updateTitleTimerExpired, self);
|
||||
}
|
||||
|
||||
fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.C) c_int {
|
||||
fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *Surface = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
self.updateTitleLabels();
|
||||
@@ -1061,7 +1075,7 @@ pub fn promptTitle(self: *Surface) !void {
|
||||
if (!adw_version.atLeast(1, 5, 0)) return;
|
||||
const window = self.container.window() orelse return;
|
||||
|
||||
var builder = Builder.init("prompt-title-dialog", 1, 5, .blp);
|
||||
var builder = Builder.init("prompt-title-dialog", 1, 5);
|
||||
defer builder.deinit();
|
||||
|
||||
const entry = builder.getObject(gtk.Entry, "title_entry").?;
|
||||
@@ -1191,7 +1205,7 @@ pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void {
|
||||
return;
|
||||
}
|
||||
|
||||
self.url_widget = URLWidget.init(self.overlay, uriZ);
|
||||
self.url_widget = .init(self.overlay, uriZ);
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
@@ -1265,7 +1279,7 @@ fn gtkClipboardRead(
|
||||
source: ?*gobject.Object,
|
||||
res: *gio.AsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const clipboard = gobject.ext.cast(gdk.Clipboard, source orelse return) orelse return;
|
||||
const req: *ClipboardRequest = @ptrCast(@alignCast(ud orelse return));
|
||||
const self = req.self;
|
||||
@@ -1349,7 +1363,7 @@ pub fn showDesktopNotification(
|
||||
app.sendNotification(body.ptr, notification);
|
||||
}
|
||||
|
||||
fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
|
||||
fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void {
|
||||
log.debug("gl surface realized", .{});
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
@@ -1377,7 +1391,7 @@ fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
|
||||
|
||||
/// This is called when the underlying OpenGL resources must be released.
|
||||
/// This is usually due to the OpenGL area changing GDK surfaces.
|
||||
fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
|
||||
fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void {
|
||||
log.debug("gl surface unrealized", .{});
|
||||
|
||||
// See gtkRealize for why we do this here.
|
||||
@@ -1405,7 +1419,7 @@ fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
|
||||
}
|
||||
|
||||
/// render signal
|
||||
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_int {
|
||||
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.c) c_int {
|
||||
self.render() catch |err| {
|
||||
log.err("surface failed to render: {}", .{err});
|
||||
return 0;
|
||||
@@ -1415,7 +1429,7 @@ fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_i
|
||||
}
|
||||
|
||||
/// resize signal
|
||||
fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.C) void {
|
||||
fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.c) void {
|
||||
// Some debug output to help understand what GTK is telling us.
|
||||
{
|
||||
const scale_factor = scale: {
|
||||
@@ -1471,7 +1485,7 @@ fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface)
|
||||
}
|
||||
|
||||
/// "destroy" signal for surface
|
||||
fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.C) void {
|
||||
fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.c) void {
|
||||
log.debug("gl destroy", .{});
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
@@ -1505,7 +1519,7 @@ fn gtkMouseDown(
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
|
||||
|
||||
const gtk_mods = event.getModifierState();
|
||||
@@ -1538,7 +1552,7 @@ fn gtkMouseUp(
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
|
||||
|
||||
const gtk_mods = event.getModifierState();
|
||||
@@ -1557,13 +1571,13 @@ fn gtkMouseMotion(
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const event = ec.as(gtk.EventController).getCurrentEvent() orelse return;
|
||||
|
||||
const scaled = self.scaledCoordinates(x, y);
|
||||
|
||||
const pos: apprt.CursorPos = .{
|
||||
.x = @floatCast(@max(0, scaled.x)),
|
||||
.x = @floatCast(scaled.x),
|
||||
.y = @floatCast(scaled.y),
|
||||
};
|
||||
|
||||
@@ -1603,7 +1617,7 @@ fn gtkMouseMotion(
|
||||
fn gtkMouseLeave(
|
||||
ec_motion: *gtk.EventControllerMotion,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const event = ec_motion.as(gtk.EventController).getCurrentEvent() orelse return;
|
||||
|
||||
// Get our modifiers
|
||||
@@ -1618,14 +1632,14 @@ fn gtkMouseLeave(
|
||||
fn gtkMouseScrollPrecisionBegin(
|
||||
_: *gtk.EventControllerScroll,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.precision_scroll = true;
|
||||
}
|
||||
|
||||
fn gtkMouseScrollPrecisionEnd(
|
||||
_: *gtk.EventControllerScroll,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.precision_scroll = false;
|
||||
}
|
||||
|
||||
@@ -1634,7 +1648,7 @@ fn gtkMouseScroll(
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *Surface,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
const scaled = self.scaledCoordinates(x, y);
|
||||
|
||||
// GTK doesn't support any of the scroll mods.
|
||||
@@ -1664,7 +1678,7 @@ fn gtkKeyPressed(
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *Surface,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
return @intFromBool(self.keyEvent(
|
||||
.press,
|
||||
ec_key,
|
||||
@@ -1680,7 +1694,7 @@ fn gtkKeyReleased(
|
||||
keycode: c_uint,
|
||||
state: gdk.ModifierType,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
_ = self.keyEvent(
|
||||
.release,
|
||||
ec_key,
|
||||
@@ -1840,7 +1854,7 @@ pub fn keyEvent(
|
||||
// (These are keybinds explicitly marked as requesting physical mapping).
|
||||
const physical_key = keycode: for (input.keycodes.entries) |entry| {
|
||||
if (entry.native == keycode) break :keycode entry.key;
|
||||
} else .invalid;
|
||||
} else .unidentified;
|
||||
|
||||
// Get our modifier for the event
|
||||
const mods: input.Mods = gtk_key.eventMods(
|
||||
@@ -1861,52 +1875,6 @@ pub fn keyEvent(
|
||||
break :consumed gtk_key.translateMods(@bitCast(masked));
|
||||
};
|
||||
|
||||
// If we're not in a dead key state, we want to translate our text
|
||||
// to some input.Key.
|
||||
const key = if (!self.im_composing) key: {
|
||||
// First, try to convert the keyval directly to a key. This allows the
|
||||
// use of key remapping and identification of keypad numerics (as
|
||||
// opposed to their ASCII counterparts)
|
||||
if (gtk_key.keyFromKeyval(keyval)) |key| {
|
||||
break :key key;
|
||||
}
|
||||
|
||||
// A completed key. If the length of the key is one then we can
|
||||
// attempt to translate it to a key enum and call the key
|
||||
// callback. First try plain ASCII.
|
||||
if (self.im_len > 0) {
|
||||
if (input.Key.fromASCII(self.im_buf[0])) |key| {
|
||||
break :key key;
|
||||
}
|
||||
}
|
||||
|
||||
// If that doesn't work then we try to translate the kevval..
|
||||
if (keyval_unicode != 0) {
|
||||
if (std.math.cast(u8, keyval_unicode)) |byte| {
|
||||
if (input.Key.fromASCII(byte)) |key| {
|
||||
break :key key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If that doesn't work we use the unshifted value...
|
||||
if (std.math.cast(u8, keyval_unicode_unshifted)) |ascii| {
|
||||
if (input.Key.fromASCII(ascii)) |key| {
|
||||
break :key key;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have im text then this is invalid. This means that
|
||||
// the keypress generated some character that we don't know about
|
||||
// in our key enum. We don't want to use the physical key because
|
||||
// it can be simply wrong. For example on "Turkish Q" the "i" key
|
||||
// on a US layout results in "ı" which is not the same as "i" so
|
||||
// we shouldn't use the physical key.
|
||||
if (self.im_len > 0 or keyval_unicode_unshifted != 0) break :key .invalid;
|
||||
|
||||
break :key physical_key;
|
||||
} else .invalid;
|
||||
|
||||
// log.debug("key pressed key={} keyval={x} physical_key={} composing={} text_len={} mods={}", .{
|
||||
// key,
|
||||
// keyval,
|
||||
@@ -1936,8 +1904,7 @@ pub fn keyEvent(
|
||||
// Invoke the core Ghostty logic to handle this input.
|
||||
const effect = self.core_surface.keyCallback(.{
|
||||
.action = action,
|
||||
.key = key,
|
||||
.physical_key = physical_key,
|
||||
.key = physical_key,
|
||||
.mods = mods,
|
||||
.consumed_mods = consumed_mods,
|
||||
.composing = self.im_composing,
|
||||
@@ -1971,7 +1938,7 @@ pub fn keyEvent(
|
||||
fn gtkInputPreeditStart(
|
||||
_: *gtk.IMMulticontext,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
// log.warn("GTKIM: preedit start", .{});
|
||||
|
||||
// Start our composing state for the input method and reset our
|
||||
@@ -1983,7 +1950,7 @@ fn gtkInputPreeditStart(
|
||||
fn gtkInputPreeditChanged(
|
||||
ctx: *gtk.IMMulticontext,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
// Any preedit change should mark that we're composing. Its possible this
|
||||
// is false using fcitx5-hangul and typing "dkssud<space>" ("안녕"). The
|
||||
// second "s" results in a "commit" for "안" which sets composing to false,
|
||||
@@ -2009,7 +1976,7 @@ fn gtkInputPreeditChanged(
|
||||
fn gtkInputPreeditEnd(
|
||||
_: *gtk.IMMulticontext,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
// log.warn("GTKIM: preedit end", .{});
|
||||
|
||||
// End our composing state for GTK, allowing us to commit the text.
|
||||
@@ -2025,7 +1992,7 @@ fn gtkInputCommit(
|
||||
_: *gtk.IMMulticontext,
|
||||
bytes: [*:0]u8,
|
||||
self: *Surface,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const str = std.mem.sliceTo(bytes, 0);
|
||||
|
||||
// log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{
|
||||
@@ -2088,8 +2055,7 @@ fn gtkInputCommit(
|
||||
// invalid key, which should produce no PTY encoding).
|
||||
_ = self.core_surface.keyCallback(.{
|
||||
.action = .press,
|
||||
.key = .invalid,
|
||||
.physical_key = .invalid,
|
||||
.key = .unidentified,
|
||||
.mods = .{},
|
||||
.consumed_mods = .{},
|
||||
.composing = false,
|
||||
@@ -2100,7 +2066,7 @@ fn gtkInputCommit(
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void {
|
||||
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void {
|
||||
if (!self.realized) return;
|
||||
|
||||
// Notify our IM context
|
||||
@@ -2125,7 +2091,7 @@ fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void {
|
||||
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void {
|
||||
if (!self.realized) return;
|
||||
|
||||
// Notify our IM context
|
||||
@@ -2243,7 +2209,7 @@ fn gtkDrop(
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *Surface,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
|
||||
if (g_value_holds(value, gdk.FileList.getGObjectType())) {
|
||||
@@ -2359,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
|
||||
env.remove("GDK_DISABLE");
|
||||
env.remove("GSK_RENDERER");
|
||||
|
||||
// Remove some environment variables that are set when Ghostty is launched
|
||||
// from a `.desktop` file, by D-Bus activation, or systemd.
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
|
||||
env.remove("DBUS_STARTER_ADDRESS");
|
||||
env.remove("DBUS_STARTER_BUS_TYPE");
|
||||
env.remove("INVOCATION_ID");
|
||||
env.remove("JOURNAL_STREAM");
|
||||
|
||||
// Unset environment varies set by snaps if we're running in a snap.
|
||||
// This allows Ghostty to further launch additional snaps.
|
||||
if (env.get("SNAP")) |_| {
|
||||
@@ -2395,7 +2370,7 @@ fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
|
||||
fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
|
||||
if (!adw_version.supportsDialogs()) return;
|
||||
const dialog = gobject.ext.cast(adw.AlertDialog, source_object.?).?;
|
||||
const self: *Surface = @ptrCast(@alignCast(ud));
|
||||
@@ -2439,3 +2414,91 @@ pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void {
|
||||
.toggle => self.is_secure_input = !self.is_secure_input,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ringBell(self: *Surface) !void {
|
||||
const features = self.app.config.@"bell-features";
|
||||
const window = self.container.window() orelse {
|
||||
log.warn("failed to ring bell: surface is not attached to any window", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
// System beep
|
||||
if (features.system) system: {
|
||||
const surface = window.window.as(gtk.Native).getSurface() orelse break :system;
|
||||
surface.beep();
|
||||
}
|
||||
|
||||
if (features.audio) audio: {
|
||||
// Play a user-specified audio file.
|
||||
|
||||
const pathname, const required = switch (self.app.config.@"bell-audio-path" orelse break :audio) {
|
||||
.optional => |path| .{ path, false },
|
||||
.required => |path| .{ path, true },
|
||||
};
|
||||
|
||||
const volume = std.math.clamp(self.app.config.@"bell-audio-volume", 0.0, 1.0);
|
||||
|
||||
std.debug.assert(std.fs.path.isAbsolute(pathname));
|
||||
const media_file = gtk.MediaFile.newForFilename(pathname);
|
||||
|
||||
if (required) {
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
gtkStreamError,
|
||||
null,
|
||||
.{ .detail = "error" },
|
||||
);
|
||||
}
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
media_file,
|
||||
?*anyopaque,
|
||||
gtkStreamEnded,
|
||||
null,
|
||||
.{ .detail = "ended" },
|
||||
);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
media_stream.setVolume(volume);
|
||||
media_stream.play();
|
||||
}
|
||||
|
||||
if (features.attention) {
|
||||
// Request user attention
|
||||
window.winproto.setUrgent(true) catch |err| {
|
||||
log.err("failed to request user attention={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// Mark tab as needing attention
|
||||
if (self.container.tab()) |tab| tab: {
|
||||
const page = window.notebook.getTabPage(tab) orelse break :tab;
|
||||
|
||||
// Need attention if we're not the currently selected tab
|
||||
if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a stream that is in an error state.
|
||||
fn gtkStreamError(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void {
|
||||
const path = path: {
|
||||
const file = media_file.getFile() orelse break :path null;
|
||||
break :path file.getPath();
|
||||
};
|
||||
defer if (path) |p| glib.free(p);
|
||||
|
||||
const media_stream = media_file.as(gtk.MediaStream);
|
||||
const err = media_stream.getError() orelse return;
|
||||
|
||||
log.warn("error playing bell from {s}: {s} {d} {s}", .{
|
||||
path orelse "<<unknown>>",
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "",
|
||||
});
|
||||
}
|
||||
|
||||
/// Stream is finished, release the memory.
|
||||
fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void {
|
||||
media_file.unref();
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn closeWithConfirmation(tab: *Tab) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.C) void {
|
||||
fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void {
|
||||
log.debug("tab box destroy", .{});
|
||||
|
||||
const alloc = self.window.app.core_app.alloc;
|
||||
|
||||
@@ -7,6 +7,7 @@ const std = @import("std");
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const glib = @import("glib");
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
@@ -114,9 +115,12 @@ pub fn gotoNthTab(self: *TabView, position: c_int) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
|
||||
return self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
}
|
||||
|
||||
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
|
||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
return self.tab_view.getPagePosition(page);
|
||||
return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
|
||||
}
|
||||
|
||||
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
|
||||
@@ -161,17 +165,16 @@ pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
_ = self.tab_view.reorderPage(page, position);
|
||||
_ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
|
||||
}
|
||||
|
||||
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
const page = self.getTabPage(tab) orelse return;
|
||||
page.setTitle(title.ptr);
|
||||
}
|
||||
|
||||
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
const page = self.getTabPage(tab) orelse return;
|
||||
page.setTooltip(tooltip.ptr);
|
||||
}
|
||||
|
||||
@@ -203,8 +206,7 @@ pub fn closeTab(self: *TabView, tab: *Tab) void {
|
||||
if (n > 1) self.forcing_close = false;
|
||||
}
|
||||
|
||||
const page = self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
self.tab_view.closePage(page);
|
||||
if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
@@ -226,7 +228,7 @@ pub fn createWindow(window: *Window) !*Window {
|
||||
return new_window;
|
||||
}
|
||||
|
||||
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void {
|
||||
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void {
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
|
||||
tab.window = self.window;
|
||||
@@ -238,11 +240,18 @@ fn adwClosePage(
|
||||
_: *adw.TabView,
|
||||
page: *adw.TabPage,
|
||||
self: *TabView,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
|
||||
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
|
||||
if (!self.forcing_close) tab.closeWithConfirmation();
|
||||
if (!self.forcing_close) {
|
||||
// We cannot trigger a close directly in here as the page will stay
|
||||
// alive until this handler returns, breaking the assumption where
|
||||
// no pages means they are all destroyed.
|
||||
//
|
||||
// Schedule the close request to happen in the next event cycle.
|
||||
_ = glib.idleAddOnce(glibIdleOnceCloseTab, tab);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -250,7 +259,7 @@ fn adwClosePage(
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *adw.TabView,
|
||||
self: *TabView,
|
||||
) callconv(.C) ?*adw.TabView {
|
||||
) callconv(.c) ?*adw.TabView {
|
||||
const window = createWindow(self.window) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
@@ -258,8 +267,18 @@ fn adwTabViewCreateWindow(
|
||||
return window.notebook.tab_view;
|
||||
}
|
||||
|
||||
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
|
||||
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void {
|
||||
const page = self.tab_view.getSelectedPage() orelse return;
|
||||
|
||||
// If the tab was previously marked as needing attention
|
||||
// (e.g. due to a bell character), we now unmark that
|
||||
page.setNeedsAttention(@intFromBool(false));
|
||||
|
||||
const title = page.getTitle();
|
||||
self.window.setTitle(std.mem.span(title));
|
||||
}
|
||||
|
||||
fn glibIdleOnceCloseTab(data: ?*anyopaque) callconv(.c) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(data orelse return));
|
||||
tab.closeWithConfirmation();
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ fn gtkLeftEnter(
|
||||
_: f64,
|
||||
_: f64,
|
||||
right: *gtk.Label,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
right.as(gtk.Widget).removeCssClass("hidden");
|
||||
}
|
||||
|
||||
@@ -110,6 +110,6 @@ fn gtkLeftEnter(
|
||||
fn gtkLeftLeave(
|
||||
_: *gtk.EventControllerMotion,
|
||||
right: *gtk.Label,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
right.as(gtk.Widget).addCssClass("hidden");
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const Color = configpkg.Config.Color;
|
||||
const Surface = @import("Surface.zig");
|
||||
const Menu = @import("menu.zig").Menu;
|
||||
@@ -33,6 +34,7 @@ const gtk_key = @import("key.zig");
|
||||
const TabView = @import("TabView.zig");
|
||||
const HeaderBar = @import("headerbar.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const CommandPalette = @import("CommandPalette.zig");
|
||||
const winprotopkg = @import("winproto.zig");
|
||||
const gtk_version = @import("gtk_version.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
@@ -53,6 +55,9 @@ window: *adw.ApplicationWindow,
|
||||
/// The header bar for the window.
|
||||
headerbar: HeaderBar,
|
||||
|
||||
/// The tab bar for the window.
|
||||
tab_bar: *adw.TabBar,
|
||||
|
||||
/// The tab overview for the window. This is possibly null since there is no
|
||||
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
||||
tab_overview: ?*adw.TabOverview,
|
||||
@@ -66,6 +71,9 @@ titlebar_menu: Menu(Window, "titlebar_menu", true),
|
||||
/// The libadwaita widget for receiving toast send requests.
|
||||
toast_overlay: *adw.ToastOverlay,
|
||||
|
||||
/// The command palette.
|
||||
command_palette: CommandPalette,
|
||||
|
||||
/// See adwTabOverviewOpen for why we have this.
|
||||
adw_tab_overview_focus_timer: ?c_uint = null,
|
||||
|
||||
@@ -81,10 +89,12 @@ pub const DerivedConfig = struct {
|
||||
gtk_tabs_location: configpkg.Config.GtkTabsLocation,
|
||||
gtk_wide_tabs: bool,
|
||||
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
|
||||
window_show_tab_bar: configpkg.Config.WindowShowTabBar,
|
||||
|
||||
quick_terminal_position: configpkg.Config.QuickTerminalPosition,
|
||||
quick_terminal_size: configpkg.Config.QuickTerminalSize,
|
||||
quick_terminal_autohide: bool,
|
||||
quick_terminal_keyboard_interactivity: configpkg.Config.QuickTerminalKeyboardInteractivity,
|
||||
|
||||
maximize: bool,
|
||||
fullscreen: bool,
|
||||
@@ -100,10 +110,12 @@ pub const DerivedConfig = struct {
|
||||
.gtk_tabs_location = config.@"gtk-tabs-location",
|
||||
.gtk_wide_tabs = config.@"gtk-wide-tabs",
|
||||
.gtk_toolbar_style = config.@"gtk-toolbar-style",
|
||||
.window_show_tab_bar = config.@"window-show-tab-bar",
|
||||
|
||||
.quick_terminal_position = config.@"quick-terminal-position",
|
||||
.quick_terminal_size = config.@"quick-terminal-size",
|
||||
.quick_terminal_autohide = config.@"quick-terminal-autohide",
|
||||
.quick_terminal_keyboard_interactivity = config.@"quick-terminal-keyboard-interactivity",
|
||||
|
||||
.maximize = config.maximize,
|
||||
.fullscreen = config.fullscreen,
|
||||
@@ -131,18 +143,20 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.last_config = @intFromPtr(&app.config),
|
||||
.config = DerivedConfig.init(&app.config),
|
||||
.config = .init(&app.config),
|
||||
.window = undefined,
|
||||
.headerbar = undefined,
|
||||
.tab_bar = undefined,
|
||||
.tab_overview = null,
|
||||
.notebook = undefined,
|
||||
.titlebar_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
.command_palette = undefined,
|
||||
.winproto = .none,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
self.window = adw.ApplicationWindow.new(app.app.as(gtk.Application));
|
||||
self.window = .new(app.app.as(gtk.Application));
|
||||
const gtk_window = self.window.as(gtk.Window);
|
||||
const gtk_widget = self.window.as(gtk.Widget);
|
||||
errdefer gtk_window.destroy();
|
||||
@@ -166,6 +180,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Setup our notebook
|
||||
self.notebook.init(self);
|
||||
|
||||
if (adw_version.supportsDialogs()) try self.command_palette.init(self);
|
||||
|
||||
// If we are using Adwaita, then we can support the tab overview.
|
||||
self.tab_overview = if (adw_version.supportsTabOverview()) overview: {
|
||||
const tab_overview = adw.TabOverview.new();
|
||||
@@ -215,8 +231,9 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (!adw_version.supportsTabOverview()) unreachable;
|
||||
const btn = switch (self.config.gtk_tabs_location) {
|
||||
.top, .bottom => btn: {
|
||||
|
||||
const btn = switch (self.config.window_show_tab_bar) {
|
||||
.always, .auto => btn: {
|
||||
const btn = gtk.ToggleButton.new();
|
||||
btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs"));
|
||||
btn.as(gtk.Button).setIconName("view-grid-symbolic");
|
||||
@@ -228,8 +245,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
);
|
||||
break :btn btn.as(gtk.Widget);
|
||||
},
|
||||
|
||||
.hidden => btn: {
|
||||
.never => btn: {
|
||||
const btn = adw.TabButton.new();
|
||||
btn.setView(self.notebook.tab_view);
|
||||
btn.as(gtk.Actionable).setActionName("overview.open");
|
||||
@@ -242,12 +258,19 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
}
|
||||
|
||||
{
|
||||
const btn = gtk.Button.newFromIconName("tab-new-symbolic");
|
||||
const btn = adw.SplitButton.new();
|
||||
btn.setIconName("tab-new-symbolic");
|
||||
btn.as(gtk.Widget).setTooltipText(i18n._("New Tab"));
|
||||
_ = gtk.Button.signals.clicked.connect(
|
||||
btn.setDropdownTooltip(i18n._("New Split"));
|
||||
|
||||
var builder = Builder.init("menu-headerbar-split_menu", 1, 0);
|
||||
defer builder.deinit();
|
||||
btn.setMenuModel(builder.getObject(gio.MenuModel, "menu"));
|
||||
|
||||
_ = adw.SplitButton.signals.clicked.connect(
|
||||
btn,
|
||||
*Window,
|
||||
gtkTabNewClick,
|
||||
adwNewTabClick,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
@@ -281,6 +304,15 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
.detail = "is-active",
|
||||
},
|
||||
);
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
self.window,
|
||||
*Window,
|
||||
gtkWindowUpdateScaleFactor,
|
||||
self,
|
||||
.{
|
||||
.detail = "scale-factor",
|
||||
},
|
||||
);
|
||||
|
||||
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
|
||||
// need to stick the headerbar into the content box.
|
||||
@@ -309,7 +341,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
}
|
||||
|
||||
// Setup our toast overlay if we have one
|
||||
self.toast_overlay = adw.ToastOverlay.new();
|
||||
self.toast_overlay = .new();
|
||||
self.toast_overlay.setChild(self.notebook.asWidget());
|
||||
box.append(self.toast_overlay.as(gtk.Widget));
|
||||
|
||||
@@ -359,21 +391,16 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Our actions for the menu
|
||||
initActions(self);
|
||||
|
||||
self.tab_bar = adw.TabBar.new();
|
||||
self.tab_bar.setView(self.notebook.tab_view);
|
||||
|
||||
if (adw_version.supportsToolbarView()) {
|
||||
const toolbar_view = adw.ToolbarView.new();
|
||||
toolbar_view.addTopBar(self.headerbar.asWidget());
|
||||
|
||||
if (self.config.gtk_tabs_location != .hidden) {
|
||||
const tab_bar = adw.TabBar.new();
|
||||
tab_bar.setView(self.notebook.tab_view);
|
||||
|
||||
if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0);
|
||||
|
||||
switch (self.config.gtk_tabs_location) {
|
||||
.top => toolbar_view.addTopBar(tab_bar.as(gtk.Widget)),
|
||||
.bottom => toolbar_view.addBottomBar(tab_bar.as(gtk.Widget)),
|
||||
.hidden => unreachable,
|
||||
}
|
||||
switch (self.config.gtk_tabs_location) {
|
||||
.top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)),
|
||||
.bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)),
|
||||
}
|
||||
toolbar_view.setContent(box.as(gtk.Widget));
|
||||
|
||||
@@ -388,23 +415,18 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Set our application window content.
|
||||
self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget));
|
||||
self.window.setContent(self.tab_overview.?.as(gtk.Widget));
|
||||
} else tab_bar: {
|
||||
if (self.config.gtk_tabs_location == .hidden) break :tab_bar;
|
||||
} else {
|
||||
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
|
||||
// an AdwToolbarView.
|
||||
const tab_bar = adw.TabBar.new();
|
||||
tab_bar.as(gtk.Widget).addCssClass("inline");
|
||||
self.tab_bar.as(gtk.Widget).addCssClass("inline");
|
||||
|
||||
switch (self.config.gtk_tabs_location) {
|
||||
.top => box.insertChildAfter(
|
||||
tab_bar.as(gtk.Widget),
|
||||
self.tab_bar.as(gtk.Widget),
|
||||
self.headerbar.asWidget(),
|
||||
),
|
||||
.bottom => box.append(tab_bar.as(gtk.Widget)),
|
||||
.hidden => unreachable,
|
||||
.bottom => box.append(self.tab_bar.as(gtk.Widget)),
|
||||
}
|
||||
tab_bar.setView(self.notebook.tab_view);
|
||||
|
||||
if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0);
|
||||
}
|
||||
|
||||
// If we want the window to be maximized, we do that here.
|
||||
@@ -439,10 +461,13 @@ pub fn updateConfig(
|
||||
if (self.last_config == this_config) return;
|
||||
self.last_config = this_config;
|
||||
|
||||
self.config = DerivedConfig.init(config);
|
||||
self.config = .init(config);
|
||||
|
||||
// We always resync our appearance whenever the config changes.
|
||||
try self.syncAppearance();
|
||||
|
||||
// Update binds inside the command palette
|
||||
try self.command_palette.updateConfig(config);
|
||||
}
|
||||
|
||||
/// Updates appearance based on config settings. Will be called once upon window
|
||||
@@ -526,6 +551,16 @@ pub fn syncAppearance(self: *Window) !void {
|
||||
}
|
||||
}
|
||||
|
||||
self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs));
|
||||
self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) {
|
||||
.auto, .never => @intFromBool(true),
|
||||
.always => @intFromBool(false),
|
||||
});
|
||||
self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) {
|
||||
.always, .auto => @intFromBool(true),
|
||||
.never => @intFromBool(false),
|
||||
});
|
||||
|
||||
self.winproto.syncAppearance() catch |err| {
|
||||
log.warn("failed to sync winproto appearance error={}", .{err});
|
||||
};
|
||||
@@ -560,6 +595,7 @@ fn initActions(self: *Window) void {
|
||||
.{ "split-left", gtkActionSplitLeft },
|
||||
.{ "split-up", gtkActionSplitUp },
|
||||
.{ "toggle-inspector", gtkActionToggleInspector },
|
||||
.{ "toggle-command-palette", gtkActionToggleCommandPalette },
|
||||
.{ "copy", gtkActionCopy },
|
||||
.{ "paste", gtkActionPaste },
|
||||
.{ "reset", gtkActionReset },
|
||||
@@ -583,6 +619,7 @@ fn initActions(self: *Window) void {
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
self.winproto.deinit(self.app.core_app.alloc);
|
||||
if (adw_version.supportsDialogs()) self.command_palette.deinit();
|
||||
|
||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||
_ = glib.Source.remove(timer);
|
||||
@@ -712,6 +749,15 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
||||
};
|
||||
}
|
||||
|
||||
/// Toggle the window decorations for this window.
|
||||
pub fn toggleCommandPalette(self: *Window) void {
|
||||
if (adw_version.supportsDialogs()) {
|
||||
self.command_palette.toggle();
|
||||
} else {
|
||||
log.warn("libadwaita 1.5+ is required for the command palette", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// Grabs focus on the currently selected tab.
|
||||
pub fn focusCurrentTab(self: *Window) void {
|
||||
const tab = self.notebook.currentTab() orelse return;
|
||||
@@ -775,25 +821,55 @@ fn gtkWindowNotifyIsActive(
|
||||
_: *adw.ApplicationWindow,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
if (!self.isQuickTerminal()) return;
|
||||
) callconv(.c) void {
|
||||
self.winproto.setUrgent(false) catch |err| {
|
||||
log.err("failed to unrequest user attention={}", .{err});
|
||||
};
|
||||
|
||||
// Hide when we're unfocused
|
||||
if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) {
|
||||
self.toggleVisibility();
|
||||
if (self.isQuickTerminal()) {
|
||||
// Hide when we're unfocused
|
||||
if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) {
|
||||
self.toggleVisibility();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
||||
// sends an undefined value.
|
||||
fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {
|
||||
fn gtkWindowUpdateScaleFactor(
|
||||
_: *adw.ApplicationWindow,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
// On some platforms (namely X11) we need to refresh our appearance when
|
||||
// the scale factor changes. In theory this could be more fine-grained as
|
||||
// a full refresh could be expensive, but a) this *should* be rare, and
|
||||
// b) quite noticeable visual bugs would occur if this is not present.
|
||||
self.winproto.syncAppearance() catch |err| {
|
||||
log.err(
|
||||
"failed to sync appearance after scale factor has been updated={}",
|
||||
.{err},
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/// Perform a binding action on the window's action surface.
|
||||
pub fn performBindingAction(self: *Window, action: input.Binding.Action) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .new_tab = {} }) catch |err| {
|
||||
_ = surface.performBindingAction(action) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_tab = {} });
|
||||
}
|
||||
|
||||
/// Create a new surface (tab or split).
|
||||
fn adwNewTabClick(_: *adw.SplitButton, self: *Window) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_tab = {} });
|
||||
}
|
||||
|
||||
/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
|
||||
/// because we need to return an AdwTabPage from this function.
|
||||
fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage {
|
||||
@@ -840,7 +916,7 @@ fn adwTabOverviewOpen(
|
||||
|
||||
fn adwTabOverviewFocusTimer(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c_int {
|
||||
) callconv(.c) c_int {
|
||||
if (!adw_version.supportsTabOverview()) unreachable;
|
||||
const self: *Window = @ptrCast(@alignCast(ud orelse return 0));
|
||||
self.adw_tab_overview_focus_timer = null;
|
||||
@@ -927,7 +1003,7 @@ fn gtkActionAbout(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const name = "Ghostty";
|
||||
const icon = "com.mitchellh.ghostty";
|
||||
const website = "https://ghostty.org";
|
||||
@@ -971,7 +1047,7 @@ fn gtkActionClose(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.closeWithConfirmation();
|
||||
}
|
||||
|
||||
@@ -979,153 +1055,112 @@ fn gtkActionNewWindow(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .new_window = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_window = {} });
|
||||
}
|
||||
|
||||
fn gtkActionNewTab(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
// We can use undefined because the button is not used.
|
||||
gtkTabNewClick(undefined, self);
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_tab = {} });
|
||||
}
|
||||
|
||||
fn gtkActionCloseTab(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .close_tab = {} });
|
||||
}
|
||||
|
||||
fn gtkActionSplitRight(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .new_split = .right }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_split = .right });
|
||||
}
|
||||
|
||||
fn gtkActionSplitDown(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .new_split = .down }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_split = .down });
|
||||
}
|
||||
|
||||
fn gtkActionSplitLeft(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .new_split = .left }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_split = .left });
|
||||
}
|
||||
|
||||
fn gtkActionSplitUp(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .new_split = .up }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .new_split = .up });
|
||||
}
|
||||
|
||||
fn gtkActionToggleInspector(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .inspector = .toggle });
|
||||
}
|
||||
|
||||
fn gtkActionToggleCommandPalette(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .inspector = .toggle }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
self.performBindingAction(.toggle_command_palette);
|
||||
}
|
||||
|
||||
fn gtkActionCopy(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .copy_to_clipboard = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .copy_to_clipboard = {} });
|
||||
}
|
||||
|
||||
fn gtkActionPaste(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .paste_from_clipboard = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .paste_from_clipboard = {} });
|
||||
}
|
||||
|
||||
fn gtkActionReset(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .reset = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .reset = {} });
|
||||
}
|
||||
|
||||
fn gtkActionClear(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .clear_screen = {} });
|
||||
}
|
||||
|
||||
fn gtkActionPromptTitle(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
const surface = self.actionSurface() orelse return;
|
||||
_ = surface.performBindingAction(.{ .prompt_surface_title = {} }) catch |err| {
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.{ .prompt_surface_title = {} });
|
||||
}
|
||||
|
||||
/// Returns the surface to use for an action.
|
||||
@@ -1139,7 +1174,7 @@ fn gtkTitlebarMenuActivate(
|
||||
btn: *gtk.MenuButton,
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
// debian 12 is stuck on GTK 4.8
|
||||
if (!gtk_version.atLeast(4, 10, 0)) return;
|
||||
const active = btn.getActive() != 0;
|
||||
|
||||
@@ -4,62 +4,157 @@ pub const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
const adwaita_version = std.SemanticVersion{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
.patch = c.ADW_MICRO_VERSION,
|
||||
};
|
||||
const required_blueprint_version = std.SemanticVersion{
|
||||
.major = 0,
|
||||
.minor = 16,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const alloc = gpa.allocator();
|
||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer _ = debug_allocator.deinit();
|
||||
const alloc = debug_allocator.allocator();
|
||||
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
_ = it.next();
|
||||
|
||||
const 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 required_adwaita_version = std.SemanticVersion{
|
||||
.major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10),
|
||||
.minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10),
|
||||
.patch = 0,
|
||||
};
|
||||
const output = it.next() orelse return error.NoOutput;
|
||||
const input = it.next() orelse return error.NoInput;
|
||||
|
||||
if (c.ADW_MAJOR_VERSION < major or (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor)) {
|
||||
// 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;
|
||||
if (adwaita_version.order(required_adwaita_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`libadwaita` is too old.
|
||||
\\
|
||||
\\Ghostty requires a version {} or newer of `libadwaita` to
|
||||
\\compile this blueprint. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
, .{required_adwaita_version});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
|
||||
var compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"compile",
|
||||
"--output",
|
||||
output,
|
||||
input,
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
{
|
||||
var stdout: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stdout.deinit(alloc);
|
||||
var stderr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stderr.deinit(alloc);
|
||||
|
||||
const term = compiler.spawnAndWait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.log.err(
|
||||
\\`blueprint-compiler` not found.
|
||||
var blueprint_compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"--version",
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
blueprint_compiler.stdout_behavior = .Pipe;
|
||||
blueprint_compiler.stderr_behavior = .Pipe;
|
||||
try blueprint_compiler.spawn();
|
||||
try blueprint_compiler.collectOutput(
|
||||
alloc,
|
||||
&stdout,
|
||||
&stderr,
|
||||
std.math.maxInt(u16),
|
||||
);
|
||||
const term = blueprint_compiler.wait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) std.process.exit(1);
|
||||
},
|
||||
else => std.process.exit(1),
|
||||
}
|
||||
|
||||
const version = try std.SemanticVersion.parse(std.mem.trim(u8, stdout.items, &std.ascii.whitespace));
|
||||
if (version.order(required_blueprint_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` is the wrong version.
|
||||
\\
|
||||
\\Ghostty requires `blueprint-compiler` as a build-time dependency starting from version 1.2.
|
||||
\\Please install it, ensure that it is available on your PATH, and then retry building Ghostty.
|
||||
, .{});
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) std.process.exit(1);
|
||||
},
|
||||
else => std.process.exit(1),
|
||||
{
|
||||
var stdout: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stdout.deinit(alloc);
|
||||
var stderr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stderr.deinit(alloc);
|
||||
|
||||
var blueprint_compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"compile",
|
||||
"--output",
|
||||
output,
|
||||
input,
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
blueprint_compiler.stdout_behavior = .Pipe;
|
||||
blueprint_compiler.stderr_behavior = .Pipe;
|
||||
try blueprint_compiler.spawn();
|
||||
try blueprint_compiler.collectOutput(
|
||||
alloc,
|
||||
&stdout,
|
||||
&stderr,
|
||||
std.math.maxInt(u16),
|
||||
);
|
||||
const term = blueprint_compiler.wait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) {
|
||||
std.debug.print("{s}", .{stderr.items});
|
||||
std.process.exit(1);
|
||||
}
|
||||
},
|
||||
else => {
|
||||
std.debug.print("{s}", .{stderr.items});
|
||||
std.process.exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const alloc = gpa.allocator();
|
||||
|
||||
const filename = filename: {
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
_ = it.next() orelse return error.NoFilename;
|
||||
break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
|
||||
};
|
||||
defer alloc.free(filename);
|
||||
|
||||
const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0);
|
||||
defer alloc.free(data);
|
||||
|
||||
if (gtk.initCheck() == 0) {
|
||||
std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
|
||||
return;
|
||||
}
|
||||
|
||||
adw.init();
|
||||
|
||||
const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
|
||||
defer builder.unref();
|
||||
}
|
||||
29
src/apprt/gtk/flatpak.zig
Normal file
29
src/apprt/gtk/flatpak.zig
Normal file
@@ -0,0 +1,29 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const glib = @import("glib");
|
||||
|
||||
pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
|
||||
if (comptime build_config.flatpak) {
|
||||
// Only consult Flatpak runtime data for host case.
|
||||
if (internal_os.isFlatpak()) {
|
||||
var result: internal_os.ResourcesDir = .{
|
||||
.app_path = try alloc.dupe(u8, "/app/share/ghostty"),
|
||||
};
|
||||
errdefer alloc.free(result.app_path.?);
|
||||
|
||||
const keyfile = glib.KeyFile.new();
|
||||
defer keyfile.unref();
|
||||
|
||||
if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
|
||||
const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
|
||||
defer glib.free(app_dir.ptr);
|
||||
|
||||
result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return try internal_os.resourcesDir(alloc);
|
||||
}
|
||||
@@ -53,19 +53,6 @@ const icons = [_]struct {
|
||||
},
|
||||
};
|
||||
|
||||
pub const VersionedBuilderXML = struct {
|
||||
major: u16,
|
||||
minor: u16,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub const ui_files = [_]VersionedBuilderXML{
|
||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-paste" },
|
||||
};
|
||||
|
||||
pub const VersionedBlueprint = struct {
|
||||
major: u16,
|
||||
minor: u16,
|
||||
@@ -75,21 +62,28 @@ pub const VersionedBlueprint = struct {
|
||||
pub const blueprint_files = [_]VersionedBlueprint{
|
||||
.{ .major = 1, .minor = 5, .name = "prompt-title-dialog" },
|
||||
.{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" },
|
||||
.{ .major = 1, .minor = 5, .name = "command-palette" },
|
||||
.{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
|
||||
.{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
|
||||
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
|
||||
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
|
||||
.{ .major = 1, .minor = 5, .name = "ccw-paste" },
|
||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
|
||||
.{ .major = 1, .minor = 2, .name = "ccw-paste" },
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const alloc = gpa.allocator();
|
||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer _ = debug_allocator.deinit();
|
||||
const alloc = debug_allocator.allocator();
|
||||
|
||||
var extra_ui_files = std.ArrayList([]const u8).init(alloc);
|
||||
var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
defer {
|
||||
for (extra_ui_files.items) |item| alloc.free(item);
|
||||
extra_ui_files.deinit();
|
||||
extra_ui_files.deinit(alloc);
|
||||
}
|
||||
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
@@ -97,7 +91,7 @@ pub fn main() !void {
|
||||
|
||||
while (it.next()) |argument| {
|
||||
if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
|
||||
try extra_ui_files.append(try alloc.dupe(u8, argument));
|
||||
try extra_ui_files.append(alloc, try alloc.dupe(u8, argument));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,16 +125,11 @@ pub fn main() !void {
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
|
||||
\\
|
||||
);
|
||||
for (ui_files) |ui_file| {
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0d}.{1d}/{2s}.ui\">src/apprt/gtk/ui/{0d}.{1d}/{2s}.ui</file>\n",
|
||||
.{ ui_file.major, ui_file.minor, ui_file.name },
|
||||
);
|
||||
}
|
||||
for (extra_ui_files.items) |ui_file| {
|
||||
const stem = std.fs.path.stem(ui_file);
|
||||
for (blueprint_files) |file| {
|
||||
if (!std.mem.eql(u8, file.name, stem)) continue;
|
||||
const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.name });
|
||||
defer alloc.free(expected);
|
||||
if (!std.mem.endsWith(u8, ui_file, expected)) continue;
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{d}.{d}/{s}.ui\">{s}</file>\n",
|
||||
.{ file.major, file.minor, file.name, ui_file },
|
||||
@@ -156,7 +145,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
pub const dependencies = deps: {
|
||||
const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
|
||||
const total = css_files.len + icons.len + blueprint_files.len;
|
||||
var deps: [total][]const u8 = undefined;
|
||||
var index: usize = 0;
|
||||
for (css_files) |css_file| {
|
||||
@@ -167,14 +156,6 @@ pub const dependencies = deps: {
|
||||
deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||
index += 1;
|
||||
}
|
||||
for (ui_files) |ui_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.ui", .{
|
||||
ui_file.major,
|
||||
ui_file.minor,
|
||||
ui_file.name,
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
for (blueprint_files) |blueprint_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{
|
||||
blueprint_file.major,
|
||||
|
||||
@@ -87,10 +87,23 @@ pub inline fn runtimeAtLeast(
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
pub inline fn runtimeUntil(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt;
|
||||
}
|
||||
|
||||
test "atLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
const funs = &.{ atLeast, runtimeAtLeast, runtimeUntil };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ const Window = struct {
|
||||
};
|
||||
|
||||
// Create the window
|
||||
self.window = gtk.ApplicationWindow.new(inspector.surface.app.app.as(gtk.Application));
|
||||
self.window = .new(inspector.surface.app.app.as(gtk.Application));
|
||||
errdefer self.window.as(gtk.Window).destroy();
|
||||
|
||||
self.window.as(gtk.Window).setTitle(i18n._("Ghostty: Terminal Inspector"));
|
||||
@@ -177,7 +177,7 @@ const Window = struct {
|
||||
}
|
||||
|
||||
/// "destroy" signal for the window
|
||||
fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.C) void {
|
||||
fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.c) void {
|
||||
log.debug("window destroy", .{});
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
@@ -20,10 +20,45 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u
|
||||
if (trigger.mods.super) try writer.writeAll("<Super>");
|
||||
|
||||
// Write our key
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Returns a XDG-compliant shortcuts string from a trigger.
|
||||
/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/
|
||||
pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("SHIFT+");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("CTRL+");
|
||||
if (trigger.mods.alt) try writer.writeAll("ALT+");
|
||||
if (trigger.mods.super) try writer.writeAll("LOGO+");
|
||||
|
||||
// Write our key
|
||||
// NOTE: While the spec specifies that only libxkbcommon keysyms are
|
||||
// expected, using GTK's keysyms should still work as they are identical
|
||||
// to *X11's* keysyms (which I assume is a subset of libxkbcommon's).
|
||||
// I haven't been able to any evidence to back up that assumption but
|
||||
// this works for now
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool {
|
||||
switch (trigger.key) {
|
||||
.physical, .translated => |k| {
|
||||
const keyval = keyvalFromKey(k) orelse return null;
|
||||
try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return null));
|
||||
.physical => |k| {
|
||||
const keyval = keyvalFromKey(k) orelse return false;
|
||||
try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false));
|
||||
},
|
||||
|
||||
.unicode => |cp| {
|
||||
@@ -35,10 +70,7 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u
|
||||
},
|
||||
}
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn translateMods(state: gdk.ModifierType) input.Mods {
|
||||
@@ -122,42 +154,42 @@ pub fn eventMods(
|
||||
// if only the modifier key is pressed, but our core logic
|
||||
// relies on it.
|
||||
switch (physical_key) {
|
||||
.left_shift => {
|
||||
.shift_left => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .left;
|
||||
},
|
||||
|
||||
.right_shift => {
|
||||
.shift_right => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .right;
|
||||
},
|
||||
|
||||
.left_control => {
|
||||
.control_left => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .left;
|
||||
},
|
||||
|
||||
.right_control => {
|
||||
.control_right => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .right;
|
||||
},
|
||||
|
||||
.left_alt => {
|
||||
.alt_left => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .left;
|
||||
},
|
||||
|
||||
.right_alt => {
|
||||
.alt_right => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .right;
|
||||
},
|
||||
|
||||
.left_super => {
|
||||
.meta_left => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .left;
|
||||
},
|
||||
|
||||
.right_super => {
|
||||
.meta_right => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .right;
|
||||
},
|
||||
@@ -182,7 +214,7 @@ pub fn keyvalFromKey(key: input.Key) ?c_uint {
|
||||
switch (key) {
|
||||
inline else => |key_comptime| {
|
||||
return comptime value: {
|
||||
@setEvalBranchQuota(10_000);
|
||||
@setEvalBranchQuota(50_000);
|
||||
for (keymap) |entry| {
|
||||
if (entry[1] == key_comptime) break :value entry[0];
|
||||
}
|
||||
@@ -199,7 +231,7 @@ test "accelFromTrigger" {
|
||||
|
||||
try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .translated = .q },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("<Shift><Ctrl><Alt><Super>backslash", (try accelFromTrigger(&buf, .{
|
||||
@@ -208,66 +240,81 @@ test "accelFromTrigger" {
|
||||
})).?);
|
||||
}
|
||||
|
||||
test "xdgShortcutFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
/// A raw entry in the keymap. Our keymap contains mappings between
|
||||
/// GDK keys and our own key enum.
|
||||
const RawEntry = struct { c_uint, input.Key };
|
||||
|
||||
const keymap: []const RawEntry = &.{
|
||||
.{ gdk.KEY_a, .a },
|
||||
.{ gdk.KEY_b, .b },
|
||||
.{ gdk.KEY_c, .c },
|
||||
.{ gdk.KEY_d, .d },
|
||||
.{ gdk.KEY_e, .e },
|
||||
.{ gdk.KEY_f, .f },
|
||||
.{ gdk.KEY_g, .g },
|
||||
.{ gdk.KEY_h, .h },
|
||||
.{ gdk.KEY_i, .i },
|
||||
.{ gdk.KEY_j, .j },
|
||||
.{ gdk.KEY_k, .k },
|
||||
.{ gdk.KEY_l, .l },
|
||||
.{ gdk.KEY_m, .m },
|
||||
.{ gdk.KEY_n, .n },
|
||||
.{ gdk.KEY_o, .o },
|
||||
.{ gdk.KEY_p, .p },
|
||||
.{ gdk.KEY_q, .q },
|
||||
.{ gdk.KEY_r, .r },
|
||||
.{ gdk.KEY_s, .s },
|
||||
.{ gdk.KEY_t, .t },
|
||||
.{ gdk.KEY_u, .u },
|
||||
.{ gdk.KEY_v, .v },
|
||||
.{ gdk.KEY_w, .w },
|
||||
.{ gdk.KEY_x, .x },
|
||||
.{ gdk.KEY_y, .y },
|
||||
.{ gdk.KEY_z, .z },
|
||||
.{ gdk.KEY_a, .key_a },
|
||||
.{ gdk.KEY_b, .key_b },
|
||||
.{ gdk.KEY_c, .key_c },
|
||||
.{ gdk.KEY_d, .key_d },
|
||||
.{ gdk.KEY_e, .key_e },
|
||||
.{ gdk.KEY_f, .key_f },
|
||||
.{ gdk.KEY_g, .key_g },
|
||||
.{ gdk.KEY_h, .key_h },
|
||||
.{ gdk.KEY_i, .key_i },
|
||||
.{ gdk.KEY_j, .key_j },
|
||||
.{ gdk.KEY_k, .key_k },
|
||||
.{ gdk.KEY_l, .key_l },
|
||||
.{ gdk.KEY_m, .key_m },
|
||||
.{ gdk.KEY_n, .key_n },
|
||||
.{ gdk.KEY_o, .key_o },
|
||||
.{ gdk.KEY_p, .key_p },
|
||||
.{ gdk.KEY_q, .key_q },
|
||||
.{ gdk.KEY_r, .key_r },
|
||||
.{ gdk.KEY_s, .key_s },
|
||||
.{ gdk.KEY_t, .key_t },
|
||||
.{ gdk.KEY_u, .key_u },
|
||||
.{ gdk.KEY_v, .key_v },
|
||||
.{ gdk.KEY_w, .key_w },
|
||||
.{ gdk.KEY_x, .key_x },
|
||||
.{ gdk.KEY_y, .key_y },
|
||||
.{ gdk.KEY_z, .key_z },
|
||||
|
||||
.{ gdk.KEY_0, .zero },
|
||||
.{ gdk.KEY_1, .one },
|
||||
.{ gdk.KEY_2, .two },
|
||||
.{ gdk.KEY_3, .three },
|
||||
.{ gdk.KEY_4, .four },
|
||||
.{ gdk.KEY_5, .five },
|
||||
.{ gdk.KEY_6, .six },
|
||||
.{ gdk.KEY_7, .seven },
|
||||
.{ gdk.KEY_8, .eight },
|
||||
.{ gdk.KEY_9, .nine },
|
||||
.{ gdk.KEY_0, .digit_0 },
|
||||
.{ gdk.KEY_1, .digit_1 },
|
||||
.{ gdk.KEY_2, .digit_2 },
|
||||
.{ gdk.KEY_3, .digit_3 },
|
||||
.{ gdk.KEY_4, .digit_4 },
|
||||
.{ gdk.KEY_5, .digit_5 },
|
||||
.{ gdk.KEY_6, .digit_6 },
|
||||
.{ gdk.KEY_7, .digit_7 },
|
||||
.{ gdk.KEY_8, .digit_8 },
|
||||
.{ gdk.KEY_9, .digit_9 },
|
||||
|
||||
.{ gdk.KEY_semicolon, .semicolon },
|
||||
.{ gdk.KEY_space, .space },
|
||||
.{ gdk.KEY_apostrophe, .apostrophe },
|
||||
.{ gdk.KEY_apostrophe, .quote },
|
||||
.{ gdk.KEY_comma, .comma },
|
||||
.{ gdk.KEY_grave, .grave_accent },
|
||||
.{ gdk.KEY_grave, .backquote },
|
||||
.{ gdk.KEY_period, .period },
|
||||
.{ gdk.KEY_slash, .slash },
|
||||
.{ gdk.KEY_minus, .minus },
|
||||
.{ gdk.KEY_equal, .equal },
|
||||
.{ gdk.KEY_bracketleft, .left_bracket },
|
||||
.{ gdk.KEY_bracketright, .right_bracket },
|
||||
.{ gdk.KEY_bracketleft, .bracket_left },
|
||||
.{ gdk.KEY_bracketright, .bracket_right },
|
||||
.{ gdk.KEY_backslash, .backslash },
|
||||
|
||||
.{ gdk.KEY_Up, .up },
|
||||
.{ gdk.KEY_Down, .down },
|
||||
.{ gdk.KEY_Right, .right },
|
||||
.{ gdk.KEY_Left, .left },
|
||||
.{ gdk.KEY_Up, .arrow_up },
|
||||
.{ gdk.KEY_Down, .arrow_down },
|
||||
.{ gdk.KEY_Right, .arrow_right },
|
||||
.{ gdk.KEY_Left, .arrow_left },
|
||||
.{ gdk.KEY_Home, .home },
|
||||
.{ gdk.KEY_End, .end },
|
||||
.{ gdk.KEY_Insert, .insert },
|
||||
@@ -310,45 +357,49 @@ const keymap: []const RawEntry = &.{
|
||||
.{ gdk.KEY_F24, .f24 },
|
||||
.{ gdk.KEY_F25, .f25 },
|
||||
|
||||
.{ gdk.KEY_KP_0, .kp_0 },
|
||||
.{ gdk.KEY_KP_1, .kp_1 },
|
||||
.{ gdk.KEY_KP_2, .kp_2 },
|
||||
.{ gdk.KEY_KP_3, .kp_3 },
|
||||
.{ gdk.KEY_KP_4, .kp_4 },
|
||||
.{ gdk.KEY_KP_5, .kp_5 },
|
||||
.{ gdk.KEY_KP_6, .kp_6 },
|
||||
.{ gdk.KEY_KP_7, .kp_7 },
|
||||
.{ gdk.KEY_KP_8, .kp_8 },
|
||||
.{ gdk.KEY_KP_9, .kp_9 },
|
||||
.{ gdk.KEY_KP_Decimal, .kp_decimal },
|
||||
.{ gdk.KEY_KP_Divide, .kp_divide },
|
||||
.{ gdk.KEY_KP_Multiply, .kp_multiply },
|
||||
.{ gdk.KEY_KP_Subtract, .kp_subtract },
|
||||
.{ gdk.KEY_KP_Add, .kp_add },
|
||||
.{ gdk.KEY_KP_Enter, .kp_enter },
|
||||
.{ gdk.KEY_KP_Equal, .kp_equal },
|
||||
.{ gdk.KEY_KP_0, .numpad_0 },
|
||||
.{ gdk.KEY_KP_1, .numpad_1 },
|
||||
.{ gdk.KEY_KP_2, .numpad_2 },
|
||||
.{ gdk.KEY_KP_3, .numpad_3 },
|
||||
.{ gdk.KEY_KP_4, .numpad_4 },
|
||||
.{ gdk.KEY_KP_5, .numpad_5 },
|
||||
.{ gdk.KEY_KP_6, .numpad_6 },
|
||||
.{ gdk.KEY_KP_7, .numpad_7 },
|
||||
.{ gdk.KEY_KP_8, .numpad_8 },
|
||||
.{ gdk.KEY_KP_9, .numpad_9 },
|
||||
.{ gdk.KEY_KP_Decimal, .numpad_decimal },
|
||||
.{ gdk.KEY_KP_Divide, .numpad_divide },
|
||||
.{ gdk.KEY_KP_Multiply, .numpad_multiply },
|
||||
.{ gdk.KEY_KP_Subtract, .numpad_subtract },
|
||||
.{ gdk.KEY_KP_Add, .numpad_add },
|
||||
.{ gdk.KEY_KP_Enter, .numpad_enter },
|
||||
.{ gdk.KEY_KP_Equal, .numpad_equal },
|
||||
|
||||
.{ gdk.KEY_KP_Separator, .kp_separator },
|
||||
.{ gdk.KEY_KP_Left, .kp_left },
|
||||
.{ gdk.KEY_KP_Right, .kp_right },
|
||||
.{ gdk.KEY_KP_Up, .kp_up },
|
||||
.{ gdk.KEY_KP_Down, .kp_down },
|
||||
.{ gdk.KEY_KP_Page_Up, .kp_page_up },
|
||||
.{ gdk.KEY_KP_Page_Down, .kp_page_down },
|
||||
.{ gdk.KEY_KP_Home, .kp_home },
|
||||
.{ gdk.KEY_KP_End, .kp_end },
|
||||
.{ gdk.KEY_KP_Insert, .kp_insert },
|
||||
.{ gdk.KEY_KP_Delete, .kp_delete },
|
||||
.{ gdk.KEY_KP_Begin, .kp_begin },
|
||||
.{ gdk.KEY_KP_Separator, .numpad_separator },
|
||||
.{ gdk.KEY_KP_Left, .numpad_left },
|
||||
.{ gdk.KEY_KP_Right, .numpad_right },
|
||||
.{ gdk.KEY_KP_Up, .numpad_up },
|
||||
.{ gdk.KEY_KP_Down, .numpad_down },
|
||||
.{ gdk.KEY_KP_Page_Up, .numpad_page_up },
|
||||
.{ gdk.KEY_KP_Page_Down, .numpad_page_down },
|
||||
.{ gdk.KEY_KP_Home, .numpad_home },
|
||||
.{ gdk.KEY_KP_End, .numpad_end },
|
||||
.{ gdk.KEY_KP_Insert, .numpad_insert },
|
||||
.{ gdk.KEY_KP_Delete, .numpad_delete },
|
||||
.{ gdk.KEY_KP_Begin, .numpad_begin },
|
||||
|
||||
.{ gdk.KEY_Shift_L, .left_shift },
|
||||
.{ gdk.KEY_Control_L, .left_control },
|
||||
.{ gdk.KEY_Alt_L, .left_alt },
|
||||
.{ gdk.KEY_Super_L, .left_super },
|
||||
.{ gdk.KEY_Shift_R, .right_shift },
|
||||
.{ gdk.KEY_Control_R, .right_control },
|
||||
.{ gdk.KEY_Alt_R, .right_alt },
|
||||
.{ gdk.KEY_Super_R, .right_super },
|
||||
.{ gdk.KEY_Copy, .copy },
|
||||
.{ gdk.KEY_Cut, .cut },
|
||||
.{ gdk.KEY_Paste, .paste },
|
||||
|
||||
.{ gdk.KEY_Shift_L, .shift_left },
|
||||
.{ gdk.KEY_Control_L, .control_left },
|
||||
.{ gdk.KEY_Alt_L, .alt_left },
|
||||
.{ gdk.KEY_Super_L, .meta_left },
|
||||
.{ gdk.KEY_Shift_R, .shift_right },
|
||||
.{ gdk.KEY_Control_R, .control_right },
|
||||
.{ gdk.KEY_Alt_R, .alt_right },
|
||||
.{ gdk.KEY_Super_R, .meta_right },
|
||||
|
||||
// TODO: media keys
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn Menu(
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0, .blp);
|
||||
var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0);
|
||||
defer builder.deinit();
|
||||
|
||||
const menu_model = builder.getObject(gio.MenuModel, "menu").?;
|
||||
@@ -130,7 +130,7 @@ pub fn Menu(
|
||||
}
|
||||
|
||||
/// Refocus tab that lost focus because of the popover menu
|
||||
fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.C) void {
|
||||
fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.c) void {
|
||||
const window: *Window = switch (T) {
|
||||
Window => self.parent,
|
||||
Surface => self.parent.container.window() orelse return,
|
||||
|
||||
@@ -73,3 +73,19 @@ window.ssd.no-border-radius {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.command-palette-search {
|
||||
font-size: 1.25rem;
|
||||
padding: 4px;
|
||||
-gtk-icon-size: 20px;
|
||||
}
|
||||
|
||||
.command-palette-search > image:first-child {
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.command-palette-search > image:last-child {
|
||||
margin-left: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
25
src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp
Normal file
25
src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp
Normal file
@@ -0,0 +1,25 @@
|
||||
using Gtk 4.0;
|
||||
|
||||
menu menu {
|
||||
section {
|
||||
item {
|
||||
label: _("Split Up");
|
||||
action: "win.split-up";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Down");
|
||||
action: "win.split-down";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Left");
|
||||
action: "win.split-left";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Split Right");
|
||||
action: "win.split-right";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,11 @@ menu menu {
|
||||
}
|
||||
|
||||
section {
|
||||
item {
|
||||
label: _("Command Palette");
|
||||
action: "win.toggle-command-palette";
|
||||
}
|
||||
|
||||
item {
|
||||
label: _("Terminal Inspector");
|
||||
action: "win.toggle-inspector";
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
DO NOT EDIT!
|
||||
This file was @generated by blueprint-compiler. Instead, edit the
|
||||
corresponding .blp file and regenerate this file with blueprint-compiler.
|
||||
-->
|
||||
<interface domain="com.mitchellh.ghostty">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
|
||||
<property name="heading" translatable="true">Authorize Clipboard Access</property>
|
||||
<property name="body" translatable="true">An application is attempting to read from the clipboard. The current clipboard contents are shown below.</property>
|
||||
<responses>
|
||||
<response id="cancel" translatable="true" appearance="suggested">Deny</response>
|
||||
<response id="ok" translatable="true" appearance="destructive">Allow</response>
|
||||
</responses>
|
||||
<property name="default-response">cancel</property>
|
||||
<property name="close-response">cancel</property>
|
||||
<property name="extra-child">
|
||||
<object class="GtkOverlay">
|
||||
<style>
|
||||
<class name="osd"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scroll">
|
||||
<property name="width-request">500</property>
|
||||
<property name="height-request">250</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="cursor-visible">false</property>
|
||||
<property name="editable">false</property>
|
||||
<property name="monospace">true</property>
|
||||
<property name="top-margin">8</property>
|
||||
<property name="left-margin">8</property>
|
||||
<property name="bottom-margin">8</property>
|
||||
<property name="right-margin">8</property>
|
||||
<style>
|
||||
<class name="clipboard-content-view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="reveal_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="halign">2</property>
|
||||
<property name="valign">1</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-reveal-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="hide_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="halign">2</property>
|
||||
<property name="valign">1</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<style>
|
||||
<class name="opaque"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-conceal-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
DO NOT EDIT!
|
||||
This file was @generated by blueprint-compiler. Instead, edit the
|
||||
corresponding .blp file and regenerate this file with blueprint-compiler.
|
||||
-->
|
||||
<interface domain="com.mitchellh.ghostty">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
|
||||
<property name="heading" translatable="true">Authorize Clipboard Access</property>
|
||||
<property name="body" translatable="true">An application is attempting to write to the clipboard. The current clipboard contents are shown below.</property>
|
||||
<responses>
|
||||
<response id="cancel" translatable="true" appearance="suggested">Deny</response>
|
||||
<response id="ok" translatable="true" appearance="destructive">Allow</response>
|
||||
</responses>
|
||||
<property name="default-response">cancel</property>
|
||||
<property name="close-response">cancel</property>
|
||||
<property name="extra-child">
|
||||
<object class="GtkOverlay">
|
||||
<style>
|
||||
<class name="osd"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scroll">
|
||||
<property name="width-request">500</property>
|
||||
<property name="height-request">250</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="cursor-visible">false</property>
|
||||
<property name="editable">false</property>
|
||||
<property name="monospace">true</property>
|
||||
<property name="top-margin">8</property>
|
||||
<property name="left-margin">8</property>
|
||||
<property name="bottom-margin">8</property>
|
||||
<property name="right-margin">8</property>
|
||||
<style>
|
||||
<class name="clipboard-content-view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="reveal_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="halign">2</property>
|
||||
<property name="valign">1</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-reveal-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="hide_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="halign">2</property>
|
||||
<property name="valign">1</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<style>
|
||||
<class name="opaque"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-conceal-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
DO NOT EDIT!
|
||||
This file was @generated by blueprint-compiler. Instead, edit the
|
||||
corresponding .blp file and regenerate this file with blueprint-compiler.
|
||||
-->
|
||||
<interface domain="com.mitchellh.ghostty">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="AdwMessageDialog" id="clipboard_confirmation_window">
|
||||
<property name="heading" translatable="true">Warning: Potentially Unsafe Paste</property>
|
||||
<property name="body" translatable="true">Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.</property>
|
||||
<responses>
|
||||
<response id="cancel" translatable="true" appearance="suggested">Cancel</response>
|
||||
<response id="ok" translatable="true" appearance="destructive">Paste</response>
|
||||
</responses>
|
||||
<property name="default-response">cancel</property>
|
||||
<property name="close-response">cancel</property>
|
||||
<property name="extra-child">
|
||||
<object class="GtkOverlay">
|
||||
<style>
|
||||
<class name="osd"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scroll">
|
||||
<property name="width-request">500</property>
|
||||
<property name="height-request">250</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="cursor-visible">false</property>
|
||||
<property name="editable">false</property>
|
||||
<property name="monospace">true</property>
|
||||
<property name="top-margin">8</property>
|
||||
<property name="left-margin">8</property>
|
||||
<property name="bottom-margin">8</property>
|
||||
<property name="right-margin">8</property>
|
||||
<style>
|
||||
<class name="clipboard-content-view"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="reveal_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="halign">2</property>
|
||||
<property name="valign">1</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-reveal-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="hide_button">
|
||||
<property name="visible">false</property>
|
||||
<property name="halign">2</property>
|
||||
<property name="valign">1</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-top">12</property>
|
||||
<style>
|
||||
<class name="opaque"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-conceal-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
DO NOT EDIT!
|
||||
This file was @generated by blueprint-compiler. Instead, edit the
|
||||
corresponding .blp file and regenerate this file with blueprint-compiler.
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="AdwMessageDialog" id="config_errors_dialog">
|
||||
<property name="heading" translatable="yes">Configuration Errors</property>
|
||||
<property name="body" translatable="yes">One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.</property>
|
||||
<responses>
|
||||
<response id="ignore" translatable="yes">Ignore</response>
|
||||
<response id="reload" translatable="yes" appearance="suggested">Reload Configuration</response>
|
||||
</responses>
|
||||
<property name="extra-child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="min-content-width">500</property>
|
||||
<property name="min-content-height">100</property>
|
||||
<child>
|
||||
<object class="GtkTextView">
|
||||
<property name="editable">false</property>
|
||||
<property name="cursor-visible">false</property>
|
||||
<property name="top-margin">8</property>
|
||||
<property name="bottom-margin">8</property>
|
||||
<property name="left-margin">8</property>
|
||||
<property name="right-margin">8</property>
|
||||
<property name="buffer">
|
||||
<object class="GtkTextBuffer" id="error_message"></object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
106
src/apprt/gtk/ui/1.5/command-palette.blp
Normal file
106
src/apprt/gtk/ui/1.5/command-palette.blp
Normal file
@@ -0,0 +1,106 @@
|
||||
using Gtk 4.0;
|
||||
using Gio 2.0;
|
||||
using Adw 1;
|
||||
|
||||
Adw.Dialog command-palette {
|
||||
content-width: 700;
|
||||
|
||||
Adw.ToolbarView {
|
||||
top-bar-style: flat;
|
||||
|
||||
[top]
|
||||
Adw.HeaderBar {
|
||||
[title]
|
||||
SearchEntry search {
|
||||
hexpand: true;
|
||||
placeholder-text: _("Execute a command…");
|
||||
|
||||
styles [
|
||||
"command-palette-search",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ScrolledWindow {
|
||||
min-content-height: 300;
|
||||
|
||||
ListView view {
|
||||
show-separators: true;
|
||||
single-click-activate: true;
|
||||
|
||||
model: SingleSelection model {
|
||||
model: FilterListModel {
|
||||
incremental: true;
|
||||
|
||||
filter: AnyFilter {
|
||||
StringFilter {
|
||||
expression: expr item as <$GhosttyCommand>.title;
|
||||
search: bind search.text;
|
||||
}
|
||||
|
||||
StringFilter {
|
||||
expression: expr item as <$GhosttyCommand>.action-key;
|
||||
search: bind search.text;
|
||||
}
|
||||
};
|
||||
|
||||
model: Gio.ListStore source {
|
||||
item-type: typeof<$GhosttyCommand>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
styles [
|
||||
"rich-list",
|
||||
]
|
||||
|
||||
factory: BuilderListItemFactory {
|
||||
template ListItem {
|
||||
child: Box {
|
||||
orientation: horizontal;
|
||||
spacing: 10;
|
||||
tooltip-text: bind template.item as <$GhosttyCommand>.description;
|
||||
|
||||
Box {
|
||||
orientation: vertical;
|
||||
hexpand: true;
|
||||
|
||||
Label {
|
||||
ellipsize: end;
|
||||
halign: start;
|
||||
wrap: false;
|
||||
single-line-mode: true;
|
||||
|
||||
styles [
|
||||
"title",
|
||||
]
|
||||
|
||||
label: bind template.item as <$GhosttyCommand>.title;
|
||||
}
|
||||
|
||||
Label {
|
||||
ellipsize: end;
|
||||
halign: start;
|
||||
wrap: false;
|
||||
single-line-mode: true;
|
||||
|
||||
styles [
|
||||
"subtitle",
|
||||
"monospace",
|
||||
]
|
||||
|
||||
label: bind template.item as <$GhosttyCommand>.action-key;
|
||||
}
|
||||
}
|
||||
|
||||
ShortcutLabel {
|
||||
accelerator: bind template.item as <$GhosttyCommand>.action;
|
||||
valign: center;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
# GTK UI files
|
||||
|
||||
This directory is for storing GTK resource definitions. With one exception, the
|
||||
files should be be in the Blueprint markup language.
|
||||
This directory is for storing GTK blueprints. GTK blueprints are compiled into
|
||||
GTK resource builder `.ui` files by `blueprint-compiler` at build time and then
|
||||
converted into an embeddable resource by `glib-compile-resources`.
|
||||
|
||||
Resource files should be stored in directories that represent the minimum
|
||||
Adwaita version needed to use that resource. Resource files should also be
|
||||
formatted using `blueprint-compiler format` as well to ensure consistency.
|
||||
Blueprint files should be stored in directories that represent the minimum
|
||||
Adwaita version needed to use that resource. Blueprint files should also be
|
||||
formatted using `blueprint-compiler format` as well to ensure consistency
|
||||
(formatting will be checked in CI).
|
||||
|
||||
The one exception to files being in Blueprint markup language is when Adwaita
|
||||
features are used that the `blueprint-compiler` on a supported platform does not
|
||||
compile. For example, Debian 12 includes Adwaita 1.2 and `blueprint-compiler`
|
||||
0.6.0. Adwaita 1.2 includes support for `MessageDialog` but `blueprint-compiler`
|
||||
0.6.0 does not. In cases like that the Blueprint markup should be compiled on a
|
||||
platform that provides a new enough `blueprint-compiler` and the resulting `.ui`
|
||||
file should be committed to the Ghostty source code. Care should be taken that
|
||||
the `.blp` file and the `.ui` file remain in sync.
|
||||
|
||||
In all other cases only the `.blp` should be committed to the Ghostty source
|
||||
code. The build process will use `blueprint-compiler` to generate the `.ui`
|
||||
files necessary at runtime.
|
||||
`blueprint-compiler` version 0.16.0 or newer is required to compile Blueprint
|
||||
files. If your system does not have `blueprint-compiler` or does not have a
|
||||
new enough version you can use the generated source tarballs, which contain
|
||||
precompiled versions of the blueprints.
|
||||
|
||||
@@ -146,4 +146,10 @@ pub const Window = union(Protocol) {
|
||||
inline else => |*v| try v.addSubprocessEnv(env),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.setUrgent(urgent),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,4 +70,6 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
|
||||
|
||||
pub fn setUrgent(_: *Window, _: bool) !void {}
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ const build_options = @import("build_options");
|
||||
const gdk = @import("gdk");
|
||||
const gdk_wayland = @import("gdk_wayland");
|
||||
const gobject = @import("gobject");
|
||||
const gtk4_layer_shell = @import("gtk4-layer-shell");
|
||||
const gtk = @import("gtk");
|
||||
const layer_shell = @import("gtk4-layer-shell");
|
||||
const wayland = @import("wayland");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
@@ -16,6 +16,7 @@ const ApprtWindow = @import("../Window.zig");
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
const log = std.log.scoped(.winproto_wayland);
|
||||
|
||||
@@ -34,6 +35,21 @@ pub const App = struct {
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
||||
///
|
||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
||||
/// creates a quick terminal, and we need to ensure this fails
|
||||
/// gracefully if this situation occurs.
|
||||
///
|
||||
/// FIXME: This is a temporary workaround - we should remove this when
|
||||
/// all of our supported distros drop support for affected old
|
||||
/// gtk4-layer-shell versions.
|
||||
///
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
@@ -45,16 +61,11 @@ pub const App = struct {
|
||||
_ = config;
|
||||
_ = app_id;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (gobject.typeCheckInstanceIsA(
|
||||
gdk_display.as(gobject.TypeInstance),
|
||||
gdk_wayland.WaylandDisplay.getGObjectType(),
|
||||
) == 0) return null;
|
||||
|
||||
const gdk_wayland_display = gobject.ext.cast(
|
||||
gdk_wayland.WaylandDisplay,
|
||||
gdk_display,
|
||||
) orelse return error.NoWaylandDisplay;
|
||||
) orelse return null;
|
||||
|
||||
const display: *wl.Display = @ptrCast(@alignCast(
|
||||
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
|
||||
));
|
||||
@@ -73,9 +84,9 @@ pub const App = struct {
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
if (context.kde_decoration_manager != null) {
|
||||
// FIXME: Roundtrip again because we have to wait for the decoration
|
||||
// manager to respond with the preferred default mode. Ew.
|
||||
// Do another round-trip to get the default decoration mode
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
@@ -97,20 +108,45 @@ pub const App = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
if (!gtk4_layer_shell.isSupported()) {
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
if (!layer_shell.isSupported()) {
|
||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 4,
|
||||
}) == .lt) {
|
||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
|
||||
const window = apprt_window.window.as(gtk.Window);
|
||||
|
||||
gtk4_layer_shell.initForWindow(window);
|
||||
gtk4_layer_shell.setLayer(window, .top);
|
||||
gtk4_layer_shell.setKeyboardMode(window, .on_demand);
|
||||
layer_shell.initForWindow(window);
|
||||
layer_shell.setLayer(window, .top);
|
||||
layer_shell.setNamespace(window, "ghostty-quick-terminal");
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) return null;
|
||||
return T;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
@@ -118,71 +154,54 @@ pub const App = struct {
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
||||
|
||||
switch (event) {
|
||||
// https://wayland.app/protocols/wayland#wl_registry:event:global
|
||||
.global => |global| {
|
||||
log.debug("wl_registry.global: interface={s}", .{global.interface});
|
||||
.global => |v| global: {
|
||||
// We don't actually do anything with this other than checking
|
||||
// for its existence, so we process this separately.
|
||||
if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq)
|
||||
context.xdg_wm_dialog_present = true;
|
||||
|
||||
if (registryBind(
|
||||
org.KdeKwinBlurManager,
|
||||
registry,
|
||||
global,
|
||||
)) |blur_manager| {
|
||||
context.kde_blur_manager = blur_manager;
|
||||
return;
|
||||
}
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
if (registryBind(
|
||||
org.KdeKwinServerDecorationManager,
|
||||
registry,
|
||||
global,
|
||||
)) |deco_manager| {
|
||||
context.kde_decoration_manager = deco_manager;
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
return;
|
||||
}
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
T.interface.name,
|
||||
) != .eq) break :global;
|
||||
|
||||
if (registryBind(
|
||||
org.KdeKwinSlideManager,
|
||||
registry,
|
||||
global,
|
||||
)) |slide_manager| {
|
||||
context.kde_slide_manager = slide_manager;
|
||||
return;
|
||||
@field(context, field.name) = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error binding interface {s} error={}",
|
||||
.{ v.interface, err },
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// We don't handle removal events
|
||||
.global_remove => {},
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind a Wayland interface to a global object. Returns non-null
|
||||
/// if the binding was successful, otherwise null.
|
||||
///
|
||||
/// The type T is the Wayland interface type that we're requesting.
|
||||
/// This function will verify that the global object is the correct
|
||||
/// interface and version before binding.
|
||||
fn registryBind(
|
||||
comptime T: type,
|
||||
registry: *wl.Registry,
|
||||
global: anytype,
|
||||
) ?*T {
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
global.interface,
|
||||
T.interface.name,
|
||||
) != .eq) return null;
|
||||
|
||||
return registry.bind(global.name, T, T.generated_version) catch |err| {
|
||||
log.warn("error binding interface {s} error={}", .{
|
||||
global.interface,
|
||||
err,
|
||||
});
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
@@ -207,15 +226,19 @@ pub const Window = struct {
|
||||
app_context: *App.Context,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur,
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
decoration: ?*org.KdeKwinServerDecoration,
|
||||
decoration: ?*org.KdeKwinServerDecoration = null,
|
||||
|
||||
/// Object that controls the slide-in/slide-out animations of the
|
||||
/// quick terminal. Always null for windows other than the quick terminal.
|
||||
slide: ?*org.KdeKwinSlide,
|
||||
slide: ?*org.KdeKwinSlide = null,
|
||||
|
||||
/// Object that, when present, denotes that the window is currently
|
||||
/// requesting attention from the user.
|
||||
activation_token: ?*xdg.ActivationTokenV1 = null,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
@@ -268,9 +291,7 @@ pub const Window = struct {
|
||||
.apprt_window = apprt_window,
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.blur_token = null,
|
||||
.decoration = deco,
|
||||
.slide = null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -315,6 +336,21 @@ pub const Window = struct {
|
||||
_ = env;
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
|
||||
// If there already is a token, destroy and unset it
|
||||
if (self.activation_token) |token| token.destroy();
|
||||
|
||||
self.activation_token = if (urgent) token: {
|
||||
const token = try activation.getActivationToken();
|
||||
token.setSurface(self.surface);
|
||||
token.setListener(*Window, onActivationTokenEvent, self);
|
||||
token.commit();
|
||||
break :token token;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn syncBlur(self: *Window) !void {
|
||||
const manager = self.app_context.kde_blur_manager orelse return;
|
||||
@@ -356,9 +392,24 @@ pub const Window = struct {
|
||||
|
||||
fn syncQuickTerminal(self: *Window) !void {
|
||||
const window = self.apprt_window.window.as(gtk.Window);
|
||||
const position = self.apprt_window.config.quick_terminal_position;
|
||||
const config = &self.apprt_window.config;
|
||||
|
||||
const anchored_edge: ?gtk4_layer_shell.ShellEdge = switch (position) {
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.quick_terminal_keyboard_interactivity) {
|
||||
.none => .none,
|
||||
.@"on-demand" => on_demand: {
|
||||
if (layer_shell.getProtocolVersion() < 4) {
|
||||
log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{});
|
||||
break :on_demand .exclusive;
|
||||
}
|
||||
break :on_demand .on_demand;
|
||||
},
|
||||
.exclusive => .exclusive,
|
||||
},
|
||||
);
|
||||
|
||||
const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) {
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
.top => .top,
|
||||
@@ -366,43 +417,41 @@ pub const Window = struct {
|
||||
.center => null,
|
||||
};
|
||||
|
||||
for (std.meta.tags(gtk4_layer_shell.ShellEdge)) |edge| {
|
||||
for (std.meta.tags(layer_shell.ShellEdge)) |edge| {
|
||||
if (anchored_edge) |anchored| {
|
||||
if (edge == anchored) {
|
||||
gtk4_layer_shell.setMargin(window, edge, 0);
|
||||
gtk4_layer_shell.setAnchor(window, edge, true);
|
||||
layer_shell.setMargin(window, edge, 0);
|
||||
layer_shell.setAnchor(window, edge, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary margin - could be made customizable?
|
||||
gtk4_layer_shell.setMargin(window, edge, 20);
|
||||
gtk4_layer_shell.setAnchor(window, edge, false);
|
||||
layer_shell.setMargin(window, edge, 20);
|
||||
layer_shell.setAnchor(window, edge, false);
|
||||
}
|
||||
|
||||
if (self.apprt_window.isQuickTerminal()) {
|
||||
if (self.slide) |slide| slide.release();
|
||||
if (self.slide) |slide| slide.release();
|
||||
|
||||
self.slide = if (anchored_edge) |anchored| slide: {
|
||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
||||
self.slide = if (anchored_edge) |anchored| slide: {
|
||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
||||
|
||||
const slide = mgr.create(self.surface) catch |err| {
|
||||
log.warn("could not create slide object={}", .{err});
|
||||
break :slide null;
|
||||
};
|
||||
const slide = mgr.create(self.surface) catch |err| {
|
||||
log.warn("could not create slide object={}", .{err});
|
||||
break :slide null;
|
||||
};
|
||||
|
||||
const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
};
|
||||
const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
};
|
||||
|
||||
slide.setLocation(@intCast(@intFromEnum(slide_location)));
|
||||
slide.commit();
|
||||
break :slide slide;
|
||||
} else null;
|
||||
}
|
||||
slide.setLocation(@intCast(@intFromEnum(slide_location)));
|
||||
slide.commit();
|
||||
break :slide slide;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the size of the quick terminal based on monitor dimensions.
|
||||
@@ -410,19 +459,43 @@ pub const Window = struct {
|
||||
_: *gdk.Surface,
|
||||
monitor: *gdk.Monitor,
|
||||
apprt_window: *ApprtWindow,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
const window = apprt_window.window.as(gtk.Window);
|
||||
const size = apprt_window.config.quick_terminal_size;
|
||||
const position = apprt_window.config.quick_terminal_position;
|
||||
const config = &apprt_window.config;
|
||||
|
||||
var monitor_size: gdk.Rectangle = undefined;
|
||||
monitor.getGeometry(&monitor_size);
|
||||
|
||||
const dims = size.calculate(position, .{
|
||||
.width = @intCast(monitor_size.f_width),
|
||||
.height = @intCast(monitor_size.f_height),
|
||||
});
|
||||
const dims = config.quick_terminal_size.calculate(
|
||||
config.quick_terminal_position,
|
||||
.{
|
||||
.width = @intCast(monitor_size.f_width),
|
||||
.height = @intCast(monitor_size.f_height),
|
||||
},
|
||||
);
|
||||
|
||||
window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
|
||||
}
|
||||
|
||||
fn onActivationTokenEvent(
|
||||
token: *xdg.ActivationTokenV1,
|
||||
event: xdg.ActivationTokenV1.Event,
|
||||
self: *Window,
|
||||
) void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
const current_token = self.activation_token orelse return;
|
||||
|
||||
if (token.getId() != current_token.getId()) {
|
||||
log.warn("received event for unknown activation token; ignoring", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
.done => |done| {
|
||||
activation.activate(done.token, self.surface);
|
||||
token.destroy();
|
||||
self.activation_token = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,16 +36,11 @@ pub const App = struct {
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
if (gobject.typeCheckInstanceIsA(
|
||||
gdk_display.as(gobject.TypeInstance),
|
||||
gdk_x11.X11Display.getGObjectType(),
|
||||
) == 0) return null;
|
||||
|
||||
// Get our X11 display
|
||||
const gdk_x11_display = gobject.ext.cast(
|
||||
gdk_x11.X11Display,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const xlib_display = gdk_x11_display.getXdisplay();
|
||||
|
||||
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
|
||||
@@ -109,7 +104,7 @@ pub const App = struct {
|
||||
return .{
|
||||
.display = xlib_display,
|
||||
.base_event_code = base_event_code,
|
||||
.atoms = Atoms.init(gdk_x11_display),
|
||||
.atoms = .init(gdk_x11_display),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,8 +171,8 @@ pub const App = struct {
|
||||
pub const Window = struct {
|
||||
app: *App,
|
||||
config: *const ApprtWindow.DerivedConfig,
|
||||
window: xlib.Window,
|
||||
gtk_window: *adw.ApplicationWindow,
|
||||
x11_surface: *gdk_x11.X11Surface,
|
||||
|
||||
blur_region: Region = .{},
|
||||
|
||||
@@ -192,13 +187,6 @@ pub const Window = struct {
|
||||
gtk.Native,
|
||||
).getSurface() orelse return error.NotX11Surface;
|
||||
|
||||
// Check if we're actually on X11
|
||||
if (gobject.typeCheckInstanceIsA(
|
||||
surface.as(gobject.TypeInstance),
|
||||
gdk_x11.X11Surface.getGObjectType(),
|
||||
) == 0)
|
||||
return error.NotX11Surface;
|
||||
|
||||
const x11_surface = gobject.ext.cast(
|
||||
gdk_x11.X11Surface,
|
||||
surface,
|
||||
@@ -207,8 +195,8 @@ pub const Window = struct {
|
||||
return .{
|
||||
.app = app,
|
||||
.config = &apprt_window.config,
|
||||
.window = x11_surface.getXid(),
|
||||
.gtk_window = apprt_window.window,
|
||||
.x11_surface = x11_surface,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,13 +207,12 @@ pub const Window = struct {
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
const gtk_widget = self.gtk_window.as(gtk.Widget);
|
||||
self.blur_region.width = gtk_widget.getWidth();
|
||||
self.blur_region.height = gtk_widget.getHeight();
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
// The user could have toggled between CSDs and SSDs,
|
||||
// therefore we need to recalculate the blur region offset.
|
||||
self.blur_region = blur: {
|
||||
// NOTE(pluiedev): CSDs are a f--king mistake.
|
||||
// Please, GNOME, stop this nonsense of making a window ~30% bigger
|
||||
@@ -236,6 +223,11 @@ pub const Window = struct {
|
||||
|
||||
self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor());
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
@@ -265,10 +257,17 @@ pub const Window = struct {
|
||||
// and I think it's not really noticeable enough to justify the effort.
|
||||
// (Wayland also has this visual artifact anyway...)
|
||||
|
||||
const gtk_widget = self.gtk_window.as(gtk.Widget);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = self.gtk_window.as(gtk.Widget).getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
|
||||
const blur = self.config.background_blur;
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.window,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
@@ -324,11 +323,19 @@ pub const Window = struct {
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const window_id = try std.fmt.bufPrint(&buf, "{}", .{self.window});
|
||||
const window_id = try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"{}",
|
||||
.{self.x11_surface.getXid()},
|
||||
);
|
||||
|
||||
try env.put("WINDOWID", window_id);
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
self.x11_surface.setUrgencyHint(@intFromBool(urgent));
|
||||
}
|
||||
|
||||
fn getWindowProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
@@ -352,7 +359,7 @@ pub const Window = struct {
|
||||
|
||||
const code = c.XGetWindowProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.window,
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
options.offset,
|
||||
options.length,
|
||||
@@ -390,7 +397,7 @@ pub const Window = struct {
|
||||
|
||||
const status = c.XChangeProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.window,
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
typ,
|
||||
@intFromEnum(format),
|
||||
@@ -408,7 +415,7 @@ pub const Window = struct {
|
||||
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
|
||||
const status = c.XDeleteProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.window,
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
);
|
||||
if (status == 0) return error.RequestFailed;
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
const internal_os = @import("../os/main.zig");
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
pub const App = struct {};
|
||||
pub const Surface = struct {};
|
||||
|
||||
@@ -43,8 +43,9 @@ pub const Message = union(enum) {
|
||||
close: void,
|
||||
|
||||
/// The child process running in the surface has exited. This may trigger
|
||||
/// a surface close, it may not.
|
||||
child_exited: void,
|
||||
/// a surface close, it may not. Additional details about the child
|
||||
/// command are given in the `ChildExited` struct.
|
||||
child_exited: ChildExited,
|
||||
|
||||
/// Show a desktop notification.
|
||||
desktop_notification: struct {
|
||||
@@ -74,18 +75,26 @@ pub const Message = union(enum) {
|
||||
|
||||
/// A terminal color was changed using OSC sequences.
|
||||
color_change: struct {
|
||||
kind: terminal.osc.Command.ColorKind,
|
||||
kind: terminal.osc.Command.ColorOperation.Kind,
|
||||
color: terminal.color.RGB,
|
||||
},
|
||||
|
||||
/// The terminal has reported a change in the working directory.
|
||||
pwd_change: WriteReq,
|
||||
|
||||
/// The terminal encountered a bell character.
|
||||
ring_bell,
|
||||
|
||||
pub const ReportTitleStyle = enum {
|
||||
csi_21_t,
|
||||
|
||||
// This enum is a placeholder for future title styles.
|
||||
};
|
||||
|
||||
pub const ChildExited = struct {
|
||||
exit_code: u32,
|
||||
runtime_ms: u64,
|
||||
};
|
||||
};
|
||||
|
||||
/// A surface mailbox.
|
||||
|
||||
@@ -68,7 +68,7 @@ pub fn main() !void {
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try std.process.argsWithAllocator(alloc);
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn main() !void {
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try std.process.argsWithAllocator(alloc);
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn main() !void {
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try std.process.argsWithAllocator(alloc);
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn main() !void {
|
||||
var args: Args = args: {
|
||||
var args: Args = .{};
|
||||
errdefer args.deinit();
|
||||
var iter = try std.process.argsWithAllocator(alloc);
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
break :args args;
|
||||
|
||||
@@ -12,9 +12,9 @@ const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const cli = @import("../cli.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const synthetic = @import("../synthetic/main.zig");
|
||||
|
||||
const Args = struct {
|
||||
mode: Mode = .noop,
|
||||
@@ -70,6 +70,14 @@ const Mode = enum {
|
||||
|
||||
// Generate an infinite stream of arbitrary random bytes.
|
||||
@"gen-rand",
|
||||
|
||||
// Generate an infinite stream of OSC requests. These will be mixed
|
||||
// with valid and invalid OSC requests by default, but the
|
||||
// `-valid` and `-invalid`-suffixed variants can be used to get only
|
||||
// a specific type of OSC request.
|
||||
@"gen-osc",
|
||||
@"gen-osc-valid",
|
||||
@"gen-osc-invalid",
|
||||
};
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
@@ -84,7 +92,7 @@ pub fn main() !void {
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try std.process.argsWithAllocator(alloc);
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
@@ -93,13 +101,57 @@ pub fn main() !void {
|
||||
const writer = std.io.getStdOut().writer();
|
||||
const buf = try alloc.alloc(u8, args.@"buffer-size");
|
||||
|
||||
// Build our RNG
|
||||
const seed: u64 = if (args.seed >= 0) @bitCast(args.seed) else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())));
|
||||
var prng = std.Random.DefaultPrng.init(seed);
|
||||
const rand = prng.random();
|
||||
|
||||
// Handle the modes that do not depend on terminal state first.
|
||||
switch (args.mode) {
|
||||
.@"gen-ascii" => try genAscii(writer, seed),
|
||||
.@"gen-utf8" => try genUtf8(writer, seed),
|
||||
.@"gen-rand" => try genRand(writer, seed),
|
||||
.@"gen-ascii" => {
|
||||
var gen: synthetic.Bytes = .{
|
||||
.rand = rand,
|
||||
.alphabet = synthetic.Bytes.Alphabet.ascii,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-utf8" => {
|
||||
var gen: synthetic.Utf8 = .{
|
||||
.rand = rand,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-rand" => {
|
||||
var gen: synthetic.Bytes = .{ .rand = rand };
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-osc" => {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = 0.5,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-osc-valid" => {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = 1.0,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-osc-invalid" => {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = 0.0,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.noop => try benchNoop(reader, buf),
|
||||
|
||||
// Handle the ones that depend on terminal state next
|
||||
@@ -133,61 +185,14 @@ pub fn main() !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an infinite stream of random printable ASCII characters.
|
||||
/// This has no control characters in it at all.
|
||||
fn genAscii(writer: anytype, seed: u64) !void {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~";
|
||||
try genData(writer, alphabet, seed);
|
||||
}
|
||||
|
||||
/// Generates an infinite stream of bytes from the given alphabet.
|
||||
fn genData(writer: anytype, alphabet: []const u8, seed: u64) !void {
|
||||
var prng = std.rand.DefaultPrng.init(seed);
|
||||
const rnd = prng.random();
|
||||
fn generate(
|
||||
writer: anytype,
|
||||
gen: synthetic.Generator,
|
||||
) !void {
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
for (&buf) |*c| {
|
||||
const idx = rnd.uintLessThanBiased(usize, alphabet.len);
|
||||
c.* = alphabet[idx];
|
||||
}
|
||||
|
||||
writer.writeAll(&buf) catch |err| switch (err) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn genUtf8(writer: anytype, seed: u64) !void {
|
||||
var prng = std.rand.DefaultPrng.init(seed);
|
||||
const rnd = prng.random();
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
var i: usize = 0;
|
||||
while (i <= buf.len - 4) {
|
||||
const cp: u18 = while (true) {
|
||||
const cp = rnd.int(u18);
|
||||
if (ziglyph.isPrint(cp)) break cp;
|
||||
};
|
||||
|
||||
i += try std.unicode.utf8Encode(cp, buf[i..]);
|
||||
}
|
||||
|
||||
writer.writeAll(buf[0..i]) catch |err| switch (err) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn genRand(writer: anytype, seed: u64) !void {
|
||||
var prng = std.rand.DefaultPrng.init(seed);
|
||||
const rnd = prng.random();
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
rnd.bytes(&buf);
|
||||
|
||||
writer.writeAll(&buf) catch |err| switch (err) {
|
||||
const data = try gen.next(&buf);
|
||||
writer.writeAll(data) catch |err| switch (err) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
else => return err,
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn init(b: *std.Build) !Config {
|
||||
// This is set to true when we're building a system package. For now
|
||||
// this is trivially detected using the "system_package_mode" bool
|
||||
// but we may want to make this more sophisticated in the future.
|
||||
const system_package: bool = b.graph.system_package_mode;
|
||||
const system_package = b.graph.system_package_mode;
|
||||
|
||||
// This specifies our target wasm runtime. For now only one semi-usable
|
||||
// one exists so this is hardcoded.
|
||||
@@ -361,7 +361,6 @@ pub fn init(b: *std.Build) !Config {
|
||||
"libpng",
|
||||
"zlib",
|
||||
"oniguruma",
|
||||
"gtk4-layer-shell",
|
||||
}) |dep| {
|
||||
_ = b.systemIntegrationOption(
|
||||
dep,
|
||||
@@ -387,6 +386,15 @@ pub fn init(b: *std.Build) !Config {
|
||||
}) |dep| {
|
||||
_ = b.systemIntegrationOption(dep, .{ .default = false });
|
||||
}
|
||||
|
||||
// These are dynamic libraries we default to true, preferring
|
||||
// to use system packages over building and installing libs
|
||||
// as they require additional ldconfig of library paths or
|
||||
// patching the rpath of the program to discover the dynamic library
|
||||
// at runtime
|
||||
for (&[_][]const u8{"gtk4-layer-shell"}) |dep| {
|
||||
_ = b.systemIntegrationOption(dep, .{ .default = true });
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -36,11 +36,13 @@ pub fn init(
|
||||
const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name});
|
||||
const c_exe = b.addExecutable(.{
|
||||
.name = bin_name,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = deps.config.target,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = deps.config.target,
|
||||
|
||||
// We always want our benchmarks to be in release mode.
|
||||
.optimize = .ReleaseFast,
|
||||
// We always want our benchmarks to be in release mode.
|
||||
.optimize = .ReleaseFast,
|
||||
}),
|
||||
});
|
||||
c_exe.linkLibC();
|
||||
|
||||
|
||||
@@ -36,6 +36,17 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
|
||||
"--format=tgz",
|
||||
});
|
||||
|
||||
// embed the Ghostty version in the tarball
|
||||
{
|
||||
const version = b.addWriteFiles().add("VERSION", b.fmt("{}", .{cfg.version}));
|
||||
// --add-file uses the most recent --prefix to determine the path
|
||||
// in the archive to copy the file (the directory only).
|
||||
git_archive.addArg(b.fmt("--prefix=ghostty-{}/", .{
|
||||
cfg.version,
|
||||
}));
|
||||
git_archive.addPrefixedFileArg("--add-file=", version);
|
||||
}
|
||||
|
||||
// Add all of our resources into the tarball.
|
||||
for (resources.items) |resource| {
|
||||
// Our dist path basename may not match our generated file basename,
|
||||
|
||||
@@ -26,8 +26,13 @@ pub fn init(
|
||||
inline for (manpages) |manpage| {
|
||||
const generate_markdown = b.addExecutable(.{
|
||||
.name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = b.graph.host,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = b.graph.host,
|
||||
.strip = false,
|
||||
.omit_frame_pointer = false,
|
||||
.unwind_tables = .sync,
|
||||
}),
|
||||
});
|
||||
deps.help_strings.addImport(generate_markdown);
|
||||
|
||||
|
||||
@@ -13,10 +13,14 @@ install_step: *std.Build.Step.InstallArtifact,
|
||||
pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty {
|
||||
const exe: *std.Build.Step.Compile = b.addExecutable(.{
|
||||
.name = "ghostty",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = cfg.target,
|
||||
.optimize = cfg.optimize,
|
||||
.strip = cfg.strip,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = cfg.target,
|
||||
.optimize = cfg.optimize,
|
||||
.strip = cfg.strip,
|
||||
.omit_frame_pointer = cfg.strip,
|
||||
.unwind_tables = if (cfg.strip) .none else .sync,
|
||||
}),
|
||||
});
|
||||
const install_step = b.addInstallArtifact(exe, .{});
|
||||
|
||||
|
||||
@@ -15,8 +15,13 @@ output: std.Build.LazyPath,
|
||||
pub fn init(b: *std.Build) !GhosttyFrameData {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "framegen",
|
||||
.root_source_file = b.path("src/build/framegen/main.zig"),
|
||||
.target = b.graph.host,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/build/framegen/main.zig"),
|
||||
.target = b.graph.host,
|
||||
.strip = false,
|
||||
.omit_frame_pointer = false,
|
||||
.unwind_tables = .sync,
|
||||
}),
|
||||
});
|
||||
|
||||
const run = b.addRunArtifact(exe);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const GhosttyI18n = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Config = @import("Config.zig");
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
@@ -21,6 +22,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
|
||||
defer steps.deinit();
|
||||
|
||||
inline for (internal_os.i18n.locales) |locale| {
|
||||
// There is no encoding suffix in the LC_MESSAGES path on FreeBSD,
|
||||
// so we need to remove it from `locale` to have a correct destination string.
|
||||
// (/usr/local/share/locale/en_AU/LC_MESSAGES)
|
||||
const target_locale = comptime if (builtin.target.os.tag == .freebsd)
|
||||
std.mem.trimRight(u8, locale, ".UTF-8")
|
||||
else
|
||||
locale;
|
||||
|
||||
const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" });
|
||||
msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));
|
||||
|
||||
@@ -28,7 +37,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
|
||||
msgfmt.captureStdOut(),
|
||||
std.fmt.comptimePrint(
|
||||
"share/locale/{s}/LC_MESSAGES/{s}.mo",
|
||||
.{ locale, domain },
|
||||
.{ target_locale, domain },
|
||||
),
|
||||
).step);
|
||||
}
|
||||
@@ -54,7 +63,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
|
||||
"--keyword=C_:1c,2",
|
||||
"--package-name=" ++ domain,
|
||||
"--msgid-bugs-address=m@mitchellh.com",
|
||||
"--copyright-holder=Mitchell Hashimoto",
|
||||
"--copyright-holder=\"Mitchell Hashimoto, Ghostty contributors\"",
|
||||
"-o",
|
||||
"-",
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const GhosttyResources = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const buildpkg = @import("main.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const config_vim = @import("../config/vim.zig");
|
||||
@@ -16,6 +18,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
|
||||
// Terminfo
|
||||
terminfo: {
|
||||
const os_tag = cfg.target.result.os.tag;
|
||||
const terminfo_share_dir = if (os_tag == .freebsd)
|
||||
"site-terminfo"
|
||||
else
|
||||
"terminfo";
|
||||
|
||||
// Encode our terminfo
|
||||
var str = std.ArrayList(u8).init(b.allocator);
|
||||
defer str.deinit();
|
||||
@@ -26,12 +34,19 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
const source = wf.add("ghostty.terminfo", str.items);
|
||||
|
||||
if (cfg.emit_terminfo) {
|
||||
const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
|
||||
const source_install = b.addInstallFile(
|
||||
source,
|
||||
if (os_tag == .freebsd)
|
||||
"share/site-terminfo/ghostty.terminfo"
|
||||
else
|
||||
"share/terminfo/ghostty.terminfo",
|
||||
);
|
||||
|
||||
try steps.append(&source_install.step);
|
||||
}
|
||||
|
||||
// Windows doesn't have the binaries below.
|
||||
if (cfg.target.result.os.tag == .windows) break :terminfo;
|
||||
if (os_tag == .windows) break :terminfo;
|
||||
|
||||
// Convert to termcap source format if thats helpful to people and
|
||||
// install it. The resulting value here is the termcap source in case
|
||||
@@ -43,7 +58,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
const out_source = run_step.captureStdOut();
|
||||
_ = run_step.captureStdErr(); // so we don't see stderr
|
||||
|
||||
const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap");
|
||||
const cap_install = b.addInstallFile(
|
||||
out_source,
|
||||
if (os_tag == .freebsd)
|
||||
"share/site-terminfo/ghostty.termcap"
|
||||
else
|
||||
"share/terminfo/ghostty.termcap",
|
||||
);
|
||||
|
||||
try steps.append(&cap_install.step);
|
||||
}
|
||||
|
||||
@@ -51,7 +73,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
{
|
||||
const run_step = RunStep.create(b, "tic");
|
||||
run_step.addArgs(&.{ "tic", "-x", "-o" });
|
||||
const path = run_step.addOutputFileArg("terminfo");
|
||||
const path = run_step.addOutputFileArg(terminfo_share_dir);
|
||||
|
||||
run_step.addFileArg(source);
|
||||
_ = run_step.captureStdErr(); // so we don't see stderr
|
||||
|
||||
@@ -63,7 +86,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
.windows => mkdir_step.addArgs(&.{"mkdir"}),
|
||||
else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
|
||||
}
|
||||
mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
|
||||
|
||||
mkdir_step.addArg(b.fmt(
|
||||
"{s}/share/{s}",
|
||||
.{ b.install_path, terminfo_share_dir },
|
||||
));
|
||||
|
||||
try steps.append(&mkdir_step.step);
|
||||
|
||||
// Use cp -R instead of Step.InstallDir because we need to preserve
|
||||
@@ -193,77 +221,178 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
}
|
||||
|
||||
// App (Linux)
|
||||
if (cfg.target.result.os.tag == .linux) {
|
||||
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
|
||||
if (cfg.target.result.os.tag == .linux) try addLinuxAppResources(
|
||||
b,
|
||||
cfg,
|
||||
&steps,
|
||||
);
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
}
|
||||
|
||||
/// Add the resource files needed to make Ghostty a proper
|
||||
/// Linux desktop application (for various desktop environments).
|
||||
fn addLinuxAppResources(
|
||||
b: *std.Build,
|
||||
cfg: *const Config,
|
||||
steps: *std.ArrayList(*std.Build.Step),
|
||||
) !void {
|
||||
assert(cfg.target.result.os.tag == .linux);
|
||||
|
||||
// Background:
|
||||
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
|
||||
|
||||
const name = b.fmt("Ghostty{s}", .{
|
||||
switch (cfg.optimize) {
|
||||
.Debug, .ReleaseSafe => " (Debug)",
|
||||
.ReleaseFast, .ReleaseSmall => "",
|
||||
},
|
||||
});
|
||||
|
||||
const app_id = b.fmt("com.mitchellh.ghostty{s}", .{
|
||||
switch (cfg.optimize) {
|
||||
.Debug, .ReleaseSafe => "-debug",
|
||||
.ReleaseFast, .ReleaseSmall => "",
|
||||
},
|
||||
});
|
||||
|
||||
const exe_abs_path = b.fmt(
|
||||
"{s}/bin/ghostty",
|
||||
.{b.install_prefix},
|
||||
);
|
||||
|
||||
// The templates that we will process. The templates are in
|
||||
// cmake format and will be processed and saved to the
|
||||
// second element of the tuple.
|
||||
const Template = struct { std.Build.LazyPath, []const u8 };
|
||||
const templates: []const Template = templates: {
|
||||
var ts: std.ArrayList(Template) = .init(b.allocator);
|
||||
|
||||
// Desktop file so that we have an icon and other metadata
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/app.desktop"),
|
||||
"share/applications/com.mitchellh.ghostty.desktop",
|
||||
).step);
|
||||
try ts.append(.{
|
||||
b.path("dist/linux/app.desktop.in"),
|
||||
b.fmt("share/applications/{s}.desktop", .{app_id}),
|
||||
});
|
||||
|
||||
// Right click menu action for Plasma desktop
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_dolphin.desktop"),
|
||||
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
|
||||
).step);
|
||||
// Service for DBus activation.
|
||||
try ts.append(.{
|
||||
if (cfg.flatpak)
|
||||
b.path("dist/linux/dbus.service.flatpak.in")
|
||||
else
|
||||
b.path("dist/linux/dbus.service.in"),
|
||||
b.fmt("share/dbus-1/services/{s}.service", .{app_id}),
|
||||
});
|
||||
|
||||
// Right click menu action for Nautilus. Note that this _must_ be named
|
||||
// `ghostty.py`. Using the full app id causes problems (see #5468).
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_nautilus.py"),
|
||||
"share/nautilus-python/extensions/ghostty.py",
|
||||
).step);
|
||||
// systemd user service. This is kind of nasty but systemd
|
||||
// looks for user services in different paths depending on
|
||||
// if we are installed as a system package or not (lib vs.
|
||||
// share) so we have to handle that here. We might be able
|
||||
// to get away with always installing to both because it
|
||||
// only ever searches in one... but I don't want to do that hack
|
||||
// until we have to.
|
||||
if (!cfg.flatpak) try ts.append(.{
|
||||
b.path("dist/linux/systemd.service.in"),
|
||||
b.fmt(
|
||||
"{s}/systemd/user/{s}.service",
|
||||
.{
|
||||
if (b.graph.system_package_mode) "lib" else "share",
|
||||
app_id,
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
// Various icons that our application can use, including the icon
|
||||
// that will be used for the desktop.
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16.png"),
|
||||
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32.png"),
|
||||
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128.png"),
|
||||
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256.png"),
|
||||
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_512.png"),
|
||||
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
// Flatpaks only support icons up to 512x512.
|
||||
if (!cfg.flatpak) {
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_1024.png"),
|
||||
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
}
|
||||
// AppStream metainfo so that application has rich metadata
|
||||
// within app stores
|
||||
try ts.append(.{
|
||||
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"),
|
||||
b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}),
|
||||
});
|
||||
|
||||
break :templates ts.items;
|
||||
};
|
||||
|
||||
// Process all our templates
|
||||
for (templates) |template| {
|
||||
const tpl = b.addConfigHeader(.{
|
||||
.style = .{ .cmake = template[0] },
|
||||
}, .{
|
||||
.NAME = name,
|
||||
.APPID = app_id,
|
||||
.GHOSTTY = exe_abs_path,
|
||||
});
|
||||
|
||||
// Template output has a single header line we want to remove.
|
||||
// We use `tail` to do it since its part of the POSIX standard.
|
||||
const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" });
|
||||
tail.setStdIn(.{ .lazy_path = tpl.getOutput() });
|
||||
|
||||
const copy = b.addInstallFile(
|
||||
tail.captureStdOut(),
|
||||
template[1],
|
||||
);
|
||||
|
||||
try steps.append(©.step);
|
||||
}
|
||||
|
||||
// Right click menu action for Plasma desktop
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_dolphin.desktop"),
|
||||
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
|
||||
).step);
|
||||
|
||||
// Right click menu action for Nautilus. Note that this _must_ be named
|
||||
// `ghostty.py`. Using the full app id causes problems (see #5468).
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_nautilus.py"),
|
||||
"share/nautilus-python/extensions/ghostty.py",
|
||||
).step);
|
||||
|
||||
// Various icons that our application can use, including the icon
|
||||
// that will be used for the desktop.
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16.png"),
|
||||
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32.png"),
|
||||
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128.png"),
|
||||
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256.png"),
|
||||
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_512.png"),
|
||||
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
// Flatpaks only support icons up to 512x512.
|
||||
if (!cfg.flatpak) {
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16@2x.png"),
|
||||
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32@2x.png"),
|
||||
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128@2x.png"),
|
||||
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256@2x.png"),
|
||||
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
|
||||
b.path("images/icons/icon_1024.png"),
|
||||
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
}
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16@2x.png"),
|
||||
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32@2x.png"),
|
||||
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128@2x.png"),
|
||||
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256@2x.png"),
|
||||
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
}
|
||||
|
||||
pub fn install(self: *const GhosttyResources) void {
|
||||
|
||||
@@ -18,8 +18,13 @@ pub fn init(
|
||||
{
|
||||
const webgen_config = b.addExecutable(.{
|
||||
.name = "webgen_config",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = b.graph.host,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = b.graph.host,
|
||||
.strip = false,
|
||||
.omit_frame_pointer = false,
|
||||
.unwind_tables = .sync,
|
||||
}),
|
||||
});
|
||||
deps.help_strings.addImport(webgen_config);
|
||||
|
||||
|
||||
@@ -12,8 +12,13 @@ output: std.Build.LazyPath,
|
||||
pub fn init(b: *std.Build, cfg: *const Config) !HelpStrings {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "helpgen",
|
||||
.root_source_file = b.path("src/helpgen.zig"),
|
||||
.target = b.graph.host,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/helpgen.zig"),
|
||||
.target = b.graph.host,
|
||||
.strip = false,
|
||||
.omit_frame_pointer = false,
|
||||
.unwind_tables = .sync,
|
||||
}),
|
||||
});
|
||||
|
||||
const help_config = config: {
|
||||
|
||||
@@ -22,13 +22,26 @@ step: *Step,
|
||||
output: LazyPath,
|
||||
|
||||
pub fn create(b: *std.Build, opts: Options) ?*MetallibStep {
|
||||
const self = b.allocator.create(MetallibStep) catch @panic("OOM");
|
||||
|
||||
const sdk = switch (opts.target.result.os.tag) {
|
||||
.macos => "macosx",
|
||||
.ios => "iphoneos",
|
||||
.ios => switch (opts.target.result.abi) {
|
||||
// The iOS simulator uses the same SDK for Metal as the device,
|
||||
// but the minimum version tag causes different behaviors.
|
||||
.simulator => "iphoneos",
|
||||
else => "iphoneos",
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
const platform_version_arg = switch (opts.target.result.os.tag) {
|
||||
.macos => "-mmacos-version-min",
|
||||
.ios => switch (opts.target.result.abi) {
|
||||
.simulator => "-mios-simulator-version-min",
|
||||
else => "-mios-version-min",
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
|
||||
const self = b.allocator.create(MetallibStep) catch @panic("OOM");
|
||||
|
||||
const min_version = if (opts.target.query.os_version_min) |v|
|
||||
b.fmt("{}", .{v.semver})
|
||||
@@ -46,16 +59,11 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep {
|
||||
const output_ir = run_ir.addOutputFileArg(b.fmt("{s}.ir", .{opts.name}));
|
||||
run_ir.addArgs(&.{"-c"});
|
||||
for (opts.sources) |source| run_ir.addFileArg(source);
|
||||
switch (opts.target.result.os.tag) {
|
||||
.ios => run_ir.addArgs(&.{b.fmt(
|
||||
"-mios-version-min={s}",
|
||||
.{min_version},
|
||||
)}),
|
||||
.macos => run_ir.addArgs(&.{b.fmt(
|
||||
"-mmacos-version-min={s}",
|
||||
.{min_version},
|
||||
)}),
|
||||
else => {},
|
||||
if (platform_version_arg) |arg| {
|
||||
run_ir.addArgs(&.{b.fmt(
|
||||
"{s}={s}",
|
||||
.{ arg, min_version },
|
||||
)});
|
||||
}
|
||||
|
||||
const run_lib = RunStep.create(
|
||||
|
||||
@@ -24,9 +24,9 @@ pub const LazyPathList = std.ArrayList(std.Build.LazyPath);
|
||||
pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps {
|
||||
var result: SharedDeps = .{
|
||||
.config = cfg,
|
||||
.help_strings = try HelpStrings.init(b, cfg),
|
||||
.unicode_tables = try UnicodeTables.init(b),
|
||||
.framedata = try GhosttyFrameData.init(b),
|
||||
.help_strings = try .init(b, cfg),
|
||||
.unicode_tables = try .init(b),
|
||||
.framedata = try .init(b),
|
||||
|
||||
// Setup by retarget
|
||||
.options = undefined,
|
||||
@@ -60,6 +60,9 @@ pub fn changeEntrypoint(
|
||||
|
||||
var result = self.*;
|
||||
result.config = config;
|
||||
result.options = b.addOptions();
|
||||
try config.addOptions(result.options);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -69,10 +72,10 @@ fn initTarget(
|
||||
target: std.Build.ResolvedTarget,
|
||||
) !void {
|
||||
// Update our metallib
|
||||
self.metallib = MetallibStep.create(b, .{
|
||||
self.metallib = .create(b, .{
|
||||
.name = "Ghostty",
|
||||
.target = target,
|
||||
.sources = &.{b.path("src/renderer/shaders/cell.metal")},
|
||||
.sources = &.{b.path("src/renderer/shaders/shaders.metal")},
|
||||
});
|
||||
|
||||
// Change our config
|
||||
@@ -374,7 +377,7 @@ pub fn add(
|
||||
// We always require the system SDK so that our system headers are available.
|
||||
// This makes things like `os/log.h` available for cross-compiling.
|
||||
if (step.rootModuleTarget().os.tag.isDarwin()) {
|
||||
try @import("apple_sdk").addPaths(b, step.root_module);
|
||||
try @import("apple_sdk").addPaths(b, step);
|
||||
|
||||
const metallib = self.metallib.?;
|
||||
metallib.output.addStepDependencies(&step.step);
|
||||
@@ -606,21 +609,23 @@ fn addGTK(
|
||||
.wayland_protocols = wayland_protocols_dep.path(""),
|
||||
});
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/blur.xml"),
|
||||
);
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"),
|
||||
);
|
||||
scanner.addCustomProtocol(
|
||||
plasma_wayland_protocols_dep.path("src/protocols/slide.xml"),
|
||||
);
|
||||
scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml");
|
||||
|
||||
scanner.generate("wl_compositor", 1);
|
||||
scanner.generate("org_kde_kwin_blur_manager", 1);
|
||||
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
|
||||
scanner.generate("org_kde_kwin_slide_manager", 1);
|
||||
scanner.generate("xdg_activation_v1", 1);
|
||||
|
||||
step.root_module.addImport("wayland", b.createModule(.{
|
||||
.root_source_file = scanner.result,
|
||||
@@ -647,14 +652,13 @@ fn addGTK(
|
||||
// IMPORTANT: gtk4-layer-shell must be linked BEFORE
|
||||
// wayland-client, as it relies on shimming libwayland's APIs.
|
||||
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
|
||||
step.linkSystemLibrary2(
|
||||
"gtk4-layer-shell-0",
|
||||
dynamic_link_opts,
|
||||
);
|
||||
step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
|
||||
} else {
|
||||
// gtk4-layer-shell *must* be dynamically linked,
|
||||
// so we don't add it as a static library
|
||||
step.linkLibrary(gtk4_layer_shell.artifact("gtk4-layer-shell"));
|
||||
const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
|
||||
b.installArtifact(shared_lib);
|
||||
step.linkLibrary(shared_lib);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,34 +666,6 @@ fn addGTK(
|
||||
}
|
||||
|
||||
{
|
||||
// For our actual build, we validate our GTK builder files if we can.
|
||||
{
|
||||
const gtk_builder_check = b.addExecutable(.{
|
||||
.name = "gtk_builder_check",
|
||||
.root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
|
||||
.target = b.graph.host,
|
||||
});
|
||||
gtk_builder_check.root_module.addOptions("build_options", self.options);
|
||||
if (gobject_) |gobject| {
|
||||
gtk_builder_check.root_module.addImport(
|
||||
"gtk",
|
||||
gobject.module("gtk4"),
|
||||
);
|
||||
gtk_builder_check.root_module.addImport(
|
||||
"adw",
|
||||
gobject.module("adw1"),
|
||||
);
|
||||
}
|
||||
|
||||
for (gresource.dependencies) |pathname| {
|
||||
const extension = std.fs.path.extension(pathname);
|
||||
if (!std.mem.eql(u8, extension, ".ui")) continue;
|
||||
const check = b.addRunArtifact(gtk_builder_check);
|
||||
check.addFileArg(b.path(pathname));
|
||||
step.step.dependOn(&check.step);
|
||||
}
|
||||
}
|
||||
|
||||
// Get our gresource c/h files and add them to our build.
|
||||
const dist = gtkDistResources(b);
|
||||
step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
|
||||
|
||||
@@ -12,8 +12,13 @@ output: std.Build.LazyPath,
|
||||
pub fn init(b: *std.Build) !UnicodeTables {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "unigen",
|
||||
.root_source_file = b.path("src/unicode/props.zig"),
|
||||
.target = b.graph.host,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/unicode/props.zig"),
|
||||
.target = b.graph.host,
|
||||
.strip = false,
|
||||
.omit_frame_pointer = false,
|
||||
.unwind_tables = .sync,
|
||||
}),
|
||||
});
|
||||
|
||||
if (b.lazyDependency("ziglyph", .{
|
||||
|
||||
@@ -44,6 +44,7 @@ See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
|
||||
# AUTHOR
|
||||
|
||||
Mitchell Hashimoto <m@mitchellh.com>
|
||||
Ghostty contributors <https://github.com/ghostty-org/ghostty/graphs/contributors>
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
|
||||
# AUTHOR
|
||||
|
||||
Mitchell Hashimoto <m@mitchellh.com>
|
||||
Ghostty contributors <https://github.com/ghostty-org/ghostty/graphs/contributors>
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
|
||||
\\
|
||||
);
|
||||
|
||||
@setEvalBranchQuota(3000);
|
||||
@setEvalBranchQuota(5000);
|
||||
inline for (@typeInfo(Config).@"struct".fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
|
||||
@@ -94,6 +94,7 @@ pub fn genKeybindActions(writer: anytype) !void {
|
||||
const info = @typeInfo(KeybindAction);
|
||||
std.debug.assert(info == .@"union");
|
||||
|
||||
@setEvalBranchQuota(5000);
|
||||
inline for (info.@"union".fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const list_keybinds = @import("list_keybinds.zig");
|
||||
const list_themes = @import("list_themes.zig");
|
||||
const list_colors = @import("list_colors.zig");
|
||||
const list_actions = @import("list_actions.zig");
|
||||
const edit_config = @import("edit_config.zig");
|
||||
const show_config = @import("show_config.zig");
|
||||
const validate_config = @import("validate_config.zig");
|
||||
const crash_report = @import("crash_report.zig");
|
||||
@@ -40,6 +41,9 @@ pub const Action = enum {
|
||||
/// List keybind actions
|
||||
@"list-actions",
|
||||
|
||||
/// Edit the config file in the configured terminal editor.
|
||||
@"edit-config",
|
||||
|
||||
/// Dump the config to stdout
|
||||
@"show-config",
|
||||
|
||||
@@ -151,6 +155,7 @@ pub const Action = enum {
|
||||
.@"list-themes" => try list_themes.run(alloc),
|
||||
.@"list-colors" => try list_colors.run(alloc),
|
||||
.@"list-actions" => try list_actions.run(alloc),
|
||||
.@"edit-config" => try edit_config.run(alloc),
|
||||
.@"show-config" => try show_config.run(alloc),
|
||||
.@"validate-config" => try validate_config.run(alloc),
|
||||
.@"crash-report" => try crash_report.run(alloc),
|
||||
@@ -187,6 +192,7 @@ pub const Action = enum {
|
||||
.@"list-themes" => list_themes.Options,
|
||||
.@"list-colors" => list_colors.Options,
|
||||
.@"list-actions" => list_actions.Options,
|
||||
.@"edit-config" => edit_config.Options,
|
||||
.@"show-config" => show_config.Options,
|
||||
.@"validate-config" => validate_config.Options,
|
||||
.@"crash-report" => crash_report.Options,
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn parse(
|
||||
// If the arena is unset, we create it. We mark that we own it
|
||||
// only so that we can clean it up on error.
|
||||
if (dst._arena == null) {
|
||||
dst._arena = ArenaAllocator.init(alloc);
|
||||
dst._arena = .init(alloc);
|
||||
arena_owned = true;
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ pub fn parseIntoField(
|
||||
return error.InvalidField;
|
||||
}
|
||||
|
||||
fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
const info = @typeInfo(T).@"union";
|
||||
assert(@typeInfo(info.tag_type.?) == .@"enum");
|
||||
|
||||
@@ -481,7 +481,7 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
// Keep track of which fields were set so we can error if a required
|
||||
// field was not set.
|
||||
const FieldSet = std.StaticBitSet(info.fields.len);
|
||||
var fields_set: FieldSet = FieldSet.initEmpty();
|
||||
var fields_set: FieldSet = .initEmpty();
|
||||
|
||||
// We split each value by ","
|
||||
var iter = std.mem.splitSequence(u8, v, ",");
|
||||
@@ -1090,6 +1090,7 @@ test "parseIntoField: tagged union" {
|
||||
b: u8,
|
||||
c: void,
|
||||
d: []const u8,
|
||||
e: [:0]const u8,
|
||||
} = undefined,
|
||||
} = .{};
|
||||
|
||||
@@ -1108,6 +1109,10 @@ test "parseIntoField: tagged union" {
|
||||
// Set string field
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
|
||||
try testing.expectEqualStrings("hello", data.value.d);
|
||||
|
||||
// Set sentinel string field
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "value", "e:hello");
|
||||
try testing.expectEqualStrings("hello", data.value.e);
|
||||
}
|
||||
|
||||
test "parseIntoField: tagged union unknown filed" {
|
||||
|
||||
@@ -176,7 +176,7 @@ const Boo = struct {
|
||||
pub fn run(gpa: Allocator) !u8 {
|
||||
// Disable on non-desktop systems.
|
||||
switch (builtin.os.tag) {
|
||||
.windows, .macos, .linux => {},
|
||||
.windows, .macos, .linux, .freebsd => {},
|
||||
else => return 1,
|
||||
}
|
||||
|
||||
|
||||
159
src/cli/edit_config.zig
Normal file
159
src/cli/edit_config.zig
Normal file
@@ -0,0 +1,159 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const args = @import("args.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("action.zig").Action;
|
||||
const configpkg = @import("../config.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
||||
pub const Options = struct {
|
||||
pub fn deinit(self: Options) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// Enables `-h` and `--help` to work.
|
||||
pub fn help(self: Options) !void {
|
||||
_ = self;
|
||||
return Action.help_error;
|
||||
}
|
||||
};
|
||||
|
||||
/// The `edit-config` command opens the Ghostty configuration file in the
|
||||
/// editor specified by the `$VISUAL` or `$EDITOR` environment variables.
|
||||
///
|
||||
/// IMPORTANT: This command will not reload the configuration after
|
||||
/// editing. You will need to manually reload the configuration using the
|
||||
/// application menu, configured keybind, or by restarting Ghostty. We
|
||||
/// plan to auto-reload in the future, but Ghostty isn't capable of
|
||||
/// this yet.
|
||||
///
|
||||
/// The filepath opened is the default user-specific configuration
|
||||
/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`.
|
||||
/// On macOS, this may also be located at
|
||||
/// `~/Library/Application Support/com.mitchellh.ghostty/config`.
|
||||
/// On macOS, whichever path exists and is non-empty will be prioritized,
|
||||
/// prioritizing the Application Support directory if neither are
|
||||
/// non-empty.
|
||||
///
|
||||
/// This command prefers the `$VISUAL` environment variable over `$EDITOR`,
|
||||
/// if both are set. If neither are set, it will print an error
|
||||
/// and exit.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
// Implementation note (by @mitchellh): I do proper memory cleanup
|
||||
// throughout this command, even though we plan on doing `exec`.
|
||||
// I do this out of good hygiene in case we ever change this to
|
||||
// not using `exec` anymore and because this command isn't performance
|
||||
// critical where setting up the defer cleanup is a problem.
|
||||
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
|
||||
{
|
||||
var iter = try args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try args.parse(Options, alloc, &opts, &iter);
|
||||
}
|
||||
|
||||
// We load the configuration once because that will write our
|
||||
// default configuration files to disk. We don't use the config.
|
||||
var config = try Config.load(alloc);
|
||||
defer config.deinit();
|
||||
|
||||
// Find the preferred path.
|
||||
const path = try Config.preferredDefaultFilePath(alloc);
|
||||
defer alloc.free(path);
|
||||
|
||||
// We don't currently support Windows because we use the exec syscall.
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
try stderr.print(
|
||||
\\The `ghostty +edit-config` command is not supported on Windows.
|
||||
\\Please edit the configuration file manually at the following path:
|
||||
\\
|
||||
\\{s}
|
||||
\\
|
||||
,
|
||||
.{path},
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get our editor
|
||||
const get_env_: ?internal_os.GetEnvResult = env: {
|
||||
// VISUAL vs. EDITOR: https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference
|
||||
if (try internal_os.getenv(alloc, "VISUAL")) |v| {
|
||||
if (v.value.len > 0) break :env v;
|
||||
v.deinit(alloc);
|
||||
}
|
||||
|
||||
if (try internal_os.getenv(alloc, "EDITOR")) |v| {
|
||||
if (v.value.len > 0) break :env v;
|
||||
v.deinit(alloc);
|
||||
}
|
||||
|
||||
break :env null;
|
||||
};
|
||||
defer if (get_env_) |v| v.deinit(alloc);
|
||||
const editor: []const u8 = if (get_env_) |v| v.value else "";
|
||||
|
||||
// If we don't have `$EDITOR` set then we can't do anything
|
||||
// but we can still print a helpful message.
|
||||
if (editor.len == 0) {
|
||||
try stderr.print(
|
||||
\\The $EDITOR or $VISUAL environment variable is not set or is empty.
|
||||
\\This environment variable is required to edit the Ghostty configuration
|
||||
\\via this CLI command.
|
||||
\\
|
||||
\\Please set the environment variable to your preferred terminal
|
||||
\\text editor and try again.
|
||||
\\
|
||||
\\If you prefer to edit the configuration file another way,
|
||||
\\you can find the configuration file at the following path:
|
||||
\\
|
||||
\\
|
||||
,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Output the path using the OSC8 sequence so that it is linked.
|
||||
try stderr.print(
|
||||
"\x1b]8;;file://{s}\x1b\\{s}\x1b]8;;\x1b\\\n",
|
||||
.{ path, path },
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// We require libc because we want to use std.c.environ for envp
|
||||
// and not have to build that ourselves. We can remove this
|
||||
// limitation later but Ghostty already heavily requires libc
|
||||
// so this is not a big deal.
|
||||
comptime assert(builtin.link_libc);
|
||||
|
||||
const editorZ = try alloc.dupeZ(u8, editor);
|
||||
defer alloc.free(editorZ);
|
||||
const pathZ = try alloc.dupeZ(u8, path);
|
||||
defer alloc.free(pathZ);
|
||||
const err = std.posix.execvpeZ(
|
||||
editorZ,
|
||||
&.{ editorZ, pathZ },
|
||||
std.c.environ,
|
||||
);
|
||||
|
||||
// If we reached this point then exec failed.
|
||||
try stderr.print(
|
||||
\\Failed to execute the editor. Error code={}.
|
||||
\\
|
||||
\\This is usually due to the executable path not existing, invalid
|
||||
\\permissions, or the shell environment not being set up
|
||||
\\correctly.
|
||||
\\
|
||||
\\Editor: {s}
|
||||
\\Path: {s}
|
||||
\\
|
||||
, .{ err, editor, path });
|
||||
return 1;
|
||||
}
|
||||
@@ -155,14 +155,12 @@ const ChordBinding = struct {
|
||||
while (l_trigger != null and r_trigger != null) {
|
||||
const lhs_key: c_int = blk: {
|
||||
switch (l_trigger.?.data.key) {
|
||||
.translated => |key| break :blk @intFromEnum(key),
|
||||
.physical => |key| break :blk @intFromEnum(key),
|
||||
.unicode => |key| break :blk @intCast(key),
|
||||
}
|
||||
};
|
||||
const rhs_key: c_int = blk: {
|
||||
switch (r_trigger.?.data.key) {
|
||||
.translated => |key| break :blk @intFromEnum(key),
|
||||
.physical => |key| break :blk @intFromEnum(key),
|
||||
.unicode => |key| break :blk @intCast(key),
|
||||
}
|
||||
@@ -254,8 +252,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||
}
|
||||
const key = switch (trigger.data.key) {
|
||||
.translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
|
||||
.unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
|
||||
};
|
||||
result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
|
||||
@@ -297,8 +294,7 @@ fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !s
|
||||
if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{});
|
||||
|
||||
switch (t.key) {
|
||||
.translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}),
|
||||
.physical => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
|
||||
.unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}),
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ pub const Options = struct {
|
||||
/// If true, force a plain list of themes.
|
||||
plain: bool = false,
|
||||
|
||||
/// Specifies the color scheme of the themes to include in the list.
|
||||
color: enum { all, dark, light } = .all,
|
||||
|
||||
pub fn deinit(self: Options) void {
|
||||
_ = self;
|
||||
}
|
||||
@@ -74,7 +77,7 @@ const ThemeListElement = struct {
|
||||
/// Two different directories will be searched for themes.
|
||||
///
|
||||
/// The first directory is the `themes` subdirectory of your Ghostty
|
||||
/// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or
|
||||
/// configuration directory. This is `$XDG_CONFIG_HOME/ghostty/themes` or
|
||||
/// `~/.config/ghostty/themes`.
|
||||
///
|
||||
/// The second directory is the `themes` subdirectory of the Ghostty resources
|
||||
@@ -93,6 +96,9 @@ const ThemeListElement = struct {
|
||||
/// * `--path`: Show the full path to the theme.
|
||||
///
|
||||
/// * `--plain`: Force a plain listing of themes.
|
||||
///
|
||||
/// * `--color`: Specify the color scheme of the themes included in the list.
|
||||
/// This can be `dark`, `light`, or `all`. The default is `all`.
|
||||
pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
@@ -109,7 +115,8 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
|
||||
if (global_state.resources_dir == null)
|
||||
const resources_dir = global_state.resources_dir.app();
|
||||
if (resources_dir == null)
|
||||
try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
|
||||
"that Ghostty is installed correctly.\n", .{});
|
||||
|
||||
@@ -137,11 +144,30 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
||||
if (std.mem.eql(u8, entry.name, ".DS_Store"))
|
||||
continue;
|
||||
count += 1;
|
||||
try themes.append(.{
|
||||
.location = loc.location,
|
||||
.path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }),
|
||||
.theme = try alloc.dupe(u8, entry.name),
|
||||
});
|
||||
|
||||
const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name });
|
||||
// if there is no need to filter just append the theme to the list
|
||||
if (opts.color == .all) {
|
||||
try themes.append(.{
|
||||
.path = path,
|
||||
.location = loc.location,
|
||||
.theme = try alloc.dupe(u8, entry.name),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise check if the theme should be included based on the provided options
|
||||
var config = try Config.default(alloc);
|
||||
defer config.deinit();
|
||||
try config.loadFile(config._arena.?.allocator(), path);
|
||||
|
||||
if (shouldIncludeTheme(opts, config)) {
|
||||
try themes.append(.{
|
||||
.path = path,
|
||||
.location = loc.location,
|
||||
.theme = try alloc.dupe(u8, entry.name),
|
||||
});
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -1594,3 +1620,13 @@ fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement) !void {
|
||||
defer app.deinit();
|
||||
try app.run();
|
||||
}
|
||||
|
||||
fn shouldIncludeTheme(opts: Options, theme_config: Config) bool {
|
||||
const rf = @as(f32, @floatFromInt(theme_config.background.r)) / 255.0;
|
||||
const gf = @as(f32, @floatFromInt(theme_config.background.g)) / 255.0;
|
||||
const bf = @as(f32, @floatFromInt(theme_config.background.b)) / 255.0;
|
||||
const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf;
|
||||
const is_dark = luminance < 0.5;
|
||||
|
||||
return (opts.color == .dark and is_dark) or (opts.color == .light and !is_dark);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
||||
const formatter = @import("config/formatter.zig");
|
||||
pub const Config = @import("config/Config.zig");
|
||||
pub const conditional = @import("config/conditional.zig");
|
||||
pub const io = @import("config/io.zig");
|
||||
pub const string = @import("config/string.zig");
|
||||
pub const edit = @import("config/edit.zig");
|
||||
pub const url = @import("config/url.zig");
|
||||
@@ -14,6 +15,7 @@ pub const formatEntry = formatter.formatEntry;
|
||||
|
||||
// Field types
|
||||
pub const ClipboardAccess = Config.ClipboardAccess;
|
||||
pub const Command = Config.Command;
|
||||
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
|
||||
pub const CopyOnSelect = Config.CopyOnSelect;
|
||||
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
||||
@@ -29,8 +31,11 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation;
|
||||
pub const RepeatableString = Config.RepeatableString;
|
||||
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
|
||||
pub const RepeatablePath = Config.RepeatablePath;
|
||||
pub const Path = Config.Path;
|
||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
|
||||
pub const BackgroundImageFit = Config.BackgroundImageFit;
|
||||
|
||||
// Alternate APIs
|
||||
pub const CAPI = @import("config/CAPI.zig");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
322
src/config/command.zig
Normal file
322
src/config/command.zig
Normal file
@@ -0,0 +1,322 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
|
||||
/// A command to execute (argv0 and args).
|
||||
///
|
||||
/// A command is specified as a simple string such as "nvim a b c".
|
||||
/// By default, we expect the downstream to do some sort of shell expansion
|
||||
/// on this string.
|
||||
///
|
||||
/// If a command is already expanded and the user does NOT want to do
|
||||
/// shell expansion (because this usually requires a round trip into
|
||||
/// /bin/sh or equivalent), specify a `direct:`-prefix. e.g.
|
||||
/// `direct:nvim a b c`.
|
||||
///
|
||||
/// The whitespace before or around the prefix is ignored. For example,
|
||||
/// ` direct:nvim a b c` and `direct: nvim a b c` are equivalent.
|
||||
///
|
||||
/// If the command is not absolute, it'll be looked up via the PATH.
|
||||
/// For the shell-expansion case, we let the shell do this. For the
|
||||
/// direct case, we do this directly.
|
||||
pub const Command = union(enum) {
|
||||
const Self = @This();
|
||||
|
||||
/// Execute a command directly, e.g. via `exec`. The format here
|
||||
/// is already structured to be ready to passed directly to `exec`
|
||||
/// with index zero being the command to execute.
|
||||
///
|
||||
/// Index zero is not guaranteed to be an absolute path, and may require
|
||||
/// PATH lookup. It is up to the downstream to do this, usually via
|
||||
/// delegation to something like `execvp`.
|
||||
direct: []const [:0]const u8,
|
||||
|
||||
/// Execute a command via shell expansion. This provides the command
|
||||
/// as a single string that is expected to be expanded in some way
|
||||
/// (up to the downstream). Usually `/bin/sh -c`.
|
||||
shell: [:0]const u8,
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
input_: ?[]const u8,
|
||||
) !void {
|
||||
// Input is required. Whitespace on the edges isn't needed.
|
||||
// Commands must be non-empty.
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
const trimmed = std.mem.trim(u8, input, " ");
|
||||
if (trimmed.len == 0) return error.ValueRequired;
|
||||
|
||||
// If we have a `:` then we MIGHT have a prefix to specify what
|
||||
// tag we should use.
|
||||
const tag: std.meta.Tag(Self), const str: []const u8 = tag: {
|
||||
if (std.mem.indexOfScalar(u8, trimmed, ':')) |idx| {
|
||||
const prefix = trimmed[0..idx];
|
||||
if (std.mem.eql(u8, prefix, "direct")) {
|
||||
break :tag .{ .direct, trimmed[idx + 1 ..] };
|
||||
} else if (std.mem.eql(u8, prefix, "shell")) {
|
||||
break :tag .{ .shell, trimmed[idx + 1 ..] };
|
||||
}
|
||||
}
|
||||
|
||||
break :tag .{ .shell, trimmed };
|
||||
};
|
||||
|
||||
switch (tag) {
|
||||
.shell => {
|
||||
// We have a shell command, so we can just dupe it.
|
||||
const copy = try alloc.dupeZ(u8, std.mem.trim(u8, str, " "));
|
||||
self.* = .{ .shell = copy };
|
||||
},
|
||||
|
||||
.direct => {
|
||||
// We're not shell expanding, so the arguments are naively
|
||||
// split on spaces.
|
||||
var builder: std.ArrayListUnmanaged([:0]const u8) = .empty;
|
||||
var args = std.mem.splitScalar(
|
||||
u8,
|
||||
std.mem.trim(u8, str, " "),
|
||||
' ',
|
||||
);
|
||||
while (args.next()) |arg| {
|
||||
const copy = try alloc.dupeZ(u8, arg);
|
||||
try builder.append(alloc, copy);
|
||||
}
|
||||
|
||||
self.* = .{ .direct = try builder.toOwnedSlice(alloc) };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a command as a single string, joining arguments as
|
||||
/// necessary with spaces. Its not guaranteed that this is a valid
|
||||
/// command; it is only meant to be human readable.
|
||||
pub fn string(
|
||||
self: *const Self,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error![:0]const u8 {
|
||||
return switch (self.*) {
|
||||
.shell => |v| try alloc.dupeZ(u8, v),
|
||||
.direct => |v| try std.mem.joinZ(alloc, " ", v),
|
||||
};
|
||||
}
|
||||
|
||||
/// Get an iterator over the arguments array. This may allocate
|
||||
/// depending on the active tag of the command.
|
||||
///
|
||||
/// For direct commands, this is very cheap and just iterates over
|
||||
/// the array. There is no allocation.
|
||||
///
|
||||
/// For shell commands, this will use Zig's ArgIteratorGeneral as
|
||||
/// a best effort shell string parser. This is not guaranteed to be
|
||||
/// 100% accurate, but it works for common cases. This requires allocation.
|
||||
pub fn argIterator(
|
||||
self: *const Self,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!ArgIterator {
|
||||
return switch (self.*) {
|
||||
.direct => |v| .{ .direct = .{ .args = v } },
|
||||
.shell => |v| .{ .shell = try .init(alloc, v) },
|
||||
};
|
||||
}
|
||||
|
||||
/// Iterates over each argument in the command.
|
||||
pub const ArgIterator = union(enum) {
|
||||
shell: std.process.ArgIteratorGeneral(.{}),
|
||||
direct: struct {
|
||||
i: usize = 0,
|
||||
args: []const [:0]const u8,
|
||||
},
|
||||
|
||||
/// Return the next argument. This may or may not be a copy
|
||||
/// depending on the active tag. If you want to ensure that every
|
||||
/// argument is a copy, use the `clone` method first.
|
||||
pub fn next(self: *ArgIterator) ?[:0]const u8 {
|
||||
return switch (self.*) {
|
||||
.shell => |*v| v.next(),
|
||||
.direct => |*v| {
|
||||
if (v.i >= v.args.len) return null;
|
||||
defer v.i += 1;
|
||||
return v.args[v.i];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ArgIterator) void {
|
||||
switch (self.*) {
|
||||
.shell => |*v| v.deinit(),
|
||||
.direct => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn clone(
|
||||
self: *const Self,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!Self {
|
||||
return switch (self.*) {
|
||||
.shell => |v| .{ .shell = try alloc.dupeZ(u8, v) },
|
||||
.direct => |v| direct: {
|
||||
const copy = try alloc.alloc([:0]const u8, v.len);
|
||||
for (v, 0..) |arg, i| copy[i] = try alloc.dupeZ(u8, arg);
|
||||
break :direct .{ .direct = copy };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||
switch (self) {
|
||||
.shell => |v| try formatter.formatEntry([]const u8, v),
|
||||
|
||||
.direct => |v| {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
writer.writeAll("direct:") catch return error.OutOfMemory;
|
||||
for (v) |arg| {
|
||||
writer.writeAll(arg) catch return error.OutOfMemory;
|
||||
writer.writeByte(' ') catch return error.OutOfMemory;
|
||||
}
|
||||
|
||||
const written = fbs.getWritten();
|
||||
try formatter.formatEntry(
|
||||
[]const u8,
|
||||
written[0..@intCast(written.len - 1)],
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test "Command: parseCLI errors" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var v: Self = undefined;
|
||||
try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null));
|
||||
try testing.expectError(error.ValueRequired, v.parseCLI(alloc, ""));
|
||||
try testing.expectError(error.ValueRequired, v.parseCLI(alloc, " "));
|
||||
}
|
||||
|
||||
test "Command: parseCLI shell expanded" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var v: Self = undefined;
|
||||
try v.parseCLI(alloc, "echo hello");
|
||||
try testing.expect(v == .shell);
|
||||
try testing.expectEqualStrings(v.shell, "echo hello");
|
||||
|
||||
// Spaces are stripped
|
||||
try v.parseCLI(alloc, " echo hello ");
|
||||
try testing.expect(v == .shell);
|
||||
try testing.expectEqualStrings(v.shell, "echo hello");
|
||||
}
|
||||
|
||||
test "Command: parseCLI direct" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var v: Self = undefined;
|
||||
try v.parseCLI(alloc, "direct:echo hello");
|
||||
try testing.expect(v == .direct);
|
||||
try testing.expectEqual(v.direct.len, 2);
|
||||
try testing.expectEqualStrings(v.direct[0], "echo");
|
||||
try testing.expectEqualStrings(v.direct[1], "hello");
|
||||
|
||||
// Spaces around the prefix
|
||||
try v.parseCLI(alloc, " direct: echo hello");
|
||||
try testing.expect(v == .direct);
|
||||
try testing.expectEqual(v.direct.len, 2);
|
||||
try testing.expectEqualStrings(v.direct[0], "echo");
|
||||
try testing.expectEqualStrings(v.direct[1], "hello");
|
||||
}
|
||||
|
||||
test "Command: argIterator shell" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v: Self = .{ .shell = "echo hello world" };
|
||||
var it = try v.argIterator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
try testing.expectEqualStrings(it.next().?, "echo");
|
||||
try testing.expectEqualStrings(it.next().?, "hello");
|
||||
try testing.expectEqualStrings(it.next().?, "world");
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
test "Command: argIterator direct" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v: Self = .{ .direct = &.{ "echo", "hello world" } };
|
||||
var it = try v.argIterator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
try testing.expectEqualStrings(it.next().?, "echo");
|
||||
try testing.expectEqualStrings(it.next().?, "hello world");
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
test "Command: string shell" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v: Self = .{ .shell = "echo hello world" };
|
||||
const str = try v.string(alloc);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str, "echo hello world");
|
||||
}
|
||||
|
||||
test "Command: string direct" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var v: Self = .{ .direct = &.{ "echo", "hello world" } };
|
||||
const str = try v.string(alloc);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings(str, "echo hello world");
|
||||
}
|
||||
|
||||
test "Command: formatConfig shell" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
var v: Self = undefined;
|
||||
try v.parseCLI(alloc, "echo hello");
|
||||
try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.items);
|
||||
}
|
||||
|
||||
test "Command: formatConfig direct" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
var v: Self = undefined;
|
||||
try v.parseCLI(alloc, "direct: echo hello");
|
||||
try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Command;
|
||||
}
|
||||
@@ -20,10 +20,10 @@ pub fn open(alloc_gpa: Allocator) !void {
|
||||
// Use an arena to make memory management easier in here.
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const alloc_arena = arena.allocator();
|
||||
|
||||
// Get the path we should open
|
||||
const config_path = try configPath(alloc);
|
||||
const config_path = try configPath(alloc_arena);
|
||||
|
||||
// Create config directory recursively.
|
||||
if (std.fs.path.dirname(config_path)) |config_dir| {
|
||||
@@ -41,7 +41,7 @@ pub fn open(alloc_gpa: Allocator) !void {
|
||||
}
|
||||
};
|
||||
|
||||
try internal_os.open(alloc, .text, config_path);
|
||||
try internal_os.open(alloc_gpa, .text, config_path);
|
||||
}
|
||||
|
||||
/// Returns the config path to use for open for the current OS.
|
||||
|
||||
@@ -153,7 +153,7 @@ pub const FileFormatter = struct {
|
||||
// If we're change-tracking then we need the default config to
|
||||
// compare against.
|
||||
var default: ?Config = if (self.changed)
|
||||
try Config.default(self.alloc)
|
||||
try .default(self.alloc)
|
||||
else
|
||||
null;
|
||||
defer if (default) |*v| v.deinit();
|
||||
|
||||
256
src/config/io.zig
Normal file
256
src/config/io.zig
Normal file
@@ -0,0 +1,256 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const string = @import("string.zig");
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
/// ReadableIO is some kind of IO source that is readable.
|
||||
///
|
||||
/// It can be either a direct string or a filepath. The filepath will
|
||||
/// be deferred and read later, so it won't be checked for existence
|
||||
/// or readability at configuration time. This allows using a path that
|
||||
/// might be produced in an intermediate state.
|
||||
pub const ReadableIO = union(enum) {
|
||||
const Self = @This();
|
||||
|
||||
raw: [:0]const u8,
|
||||
path: [:0]const u8,
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
input_: ?[]const u8,
|
||||
) !void {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
if (input.len == 0) return error.ValueRequired;
|
||||
|
||||
// We create a buffer only to do string parsing and validate
|
||||
// it works. We store the value as raw so that our formatting
|
||||
// can recreate it.
|
||||
{
|
||||
const buf = try alloc.alloc(u8, input.len);
|
||||
defer alloc.free(buf);
|
||||
_ = try string.parse(buf, input);
|
||||
}
|
||||
|
||||
// Next, parse the tagged union using normal rules.
|
||||
self.* = cli.args.parseTaggedUnion(
|
||||
Self,
|
||||
alloc,
|
||||
input,
|
||||
) catch |err| switch (err) {
|
||||
// Invalid values in the tagged union are interpreted as
|
||||
// raw values. This lets users pass in simple string values
|
||||
// without needing to tag them.
|
||||
error.InvalidValue => .{ .raw = try alloc.dupeZ(u8, input) },
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self {
|
||||
return switch (self) {
|
||||
.raw => |v| .{ .raw = try alloc.dupeZ(u8, v) },
|
||||
.path => |v| .{ .path = try alloc.dupeZ(u8, v) },
|
||||
};
|
||||
}
|
||||
|
||||
/// Same as clone but also parses the values as Zig strings in
|
||||
/// the final resulting value all at once so we can avoid extra
|
||||
/// allocations.
|
||||
pub fn cloneParsed(
|
||||
self: Self,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!Self {
|
||||
switch (self) {
|
||||
inline else => |v, tag| {
|
||||
// Parsing can't fail because we validate it in parseCLI
|
||||
const copied = try alloc.dupeZ(u8, v);
|
||||
const parsed = string.parse(copied, v) catch unreachable;
|
||||
assert(copied.ptr == parsed.ptr);
|
||||
|
||||
// If we parsed less than our original length we need
|
||||
// to keep it null-terminated.
|
||||
if (parsed.len < copied.len) copied[parsed.len] = 0;
|
||||
|
||||
return @unionInit(
|
||||
Self,
|
||||
@tagName(tag),
|
||||
copied[0..parsed.len :0],
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
if (std.meta.activeTag(self) != std.meta.activeTag(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return switch (self) {
|
||||
.raw => |v| std.mem.eql(u8, v, other.raw),
|
||||
.path => |v| std.mem.eql(u8, v, other.path),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
switch (self) {
|
||||
inline else => |v, tag| {
|
||||
writer.writeAll(@tagName(tag)) catch return error.OutOfMemory;
|
||||
writer.writeByte(':') catch return error.OutOfMemory;
|
||||
writer.writeAll(v) catch return error.OutOfMemory;
|
||||
},
|
||||
}
|
||||
|
||||
const written = fbs.getWritten();
|
||||
try formatter.formatEntry(
|
||||
[]const u8,
|
||||
written,
|
||||
);
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
{
|
||||
var io: Self = undefined;
|
||||
try Self.parseCLI(&io, alloc, "foo");
|
||||
try testing.expect(io == .raw);
|
||||
try testing.expectEqualStrings("foo", io.raw);
|
||||
}
|
||||
{
|
||||
var io: Self = undefined;
|
||||
try Self.parseCLI(&io, alloc, "raw:foo");
|
||||
try testing.expect(io == .raw);
|
||||
try testing.expectEqualStrings("foo", io.raw);
|
||||
}
|
||||
{
|
||||
var io: Self = undefined;
|
||||
try Self.parseCLI(&io, alloc, "path:foo");
|
||||
try testing.expect(io == .path);
|
||||
try testing.expectEqualStrings("foo", io.path);
|
||||
}
|
||||
}
|
||||
|
||||
test "formatEntry" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
var v: Self = undefined;
|
||||
try v.parseCLI(alloc, "raw:foo");
|
||||
try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
pub const RepeatableReadableIO = struct {
|
||||
const Self = @This();
|
||||
|
||||
// Allocator for the list is the arena for the parent config.
|
||||
list: std.ArrayListUnmanaged(ReadableIO) = .{},
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
input: ?[]const u8,
|
||||
) !void {
|
||||
const value = input orelse return error.ValueRequired;
|
||||
|
||||
// Empty value resets the list
|
||||
if (value.len == 0) {
|
||||
self.list.clearRetainingCapacity();
|
||||
return;
|
||||
}
|
||||
|
||||
var io: ReadableIO = undefined;
|
||||
try ReadableIO.parseCLI(&io, alloc, value);
|
||||
try self.list.append(alloc, io);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||
var list = try std.ArrayListUnmanaged(ReadableIO).initCapacity(
|
||||
alloc,
|
||||
self.list.items.len,
|
||||
);
|
||||
for (self.list.items) |item| {
|
||||
const copy = try item.clone(alloc);
|
||||
list.appendAssumeCapacity(copy);
|
||||
}
|
||||
|
||||
return .{ .list = list };
|
||||
}
|
||||
|
||||
/// See ReadableIO.cloneParsed
|
||||
pub fn cloneParsed(
|
||||
self: *const Self,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!Self {
|
||||
var list = try std.ArrayListUnmanaged(ReadableIO).initCapacity(
|
||||
alloc,
|
||||
self.list.items.len,
|
||||
);
|
||||
for (self.list.items) |item| {
|
||||
const copy = try item.cloneParsed(alloc);
|
||||
list.appendAssumeCapacity(copy);
|
||||
}
|
||||
|
||||
return .{ .list = list };
|
||||
}
|
||||
|
||||
/// Compare if two of our value are requal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
const itemsA = self.list.items;
|
||||
const itemsB = other.list.items;
|
||||
if (itemsA.len != itemsB.len) return false;
|
||||
for (itemsA, itemsB) |a, b| {
|
||||
if (!a.equal(b)) return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(
|
||||
self: Self,
|
||||
formatter: anytype,
|
||||
) !void {
|
||||
if (self.list.items.len == 0) {
|
||||
try formatter.formatEntry(void, {});
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.list.items) |value| {
|
||||
try formatter.formatEntry(ReadableIO, value);
|
||||
}
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: Self = .{};
|
||||
try list.parseCLI(alloc, "raw:A");
|
||||
try list.parseCLI(alloc, "path:B");
|
||||
try testing.expectEqual(@as(usize, 2), list.list.items.len);
|
||||
|
||||
try list.parseCLI(alloc, "");
|
||||
try testing.expectEqual(@as(usize, 0), list.list.items.len);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = ReadableIO;
|
||||
_ = RepeatableReadableIO;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ const std = @import("std");
|
||||
/// Parse a string literal into a byte array. The string can contain
|
||||
/// any valid Zig string literal escape sequences.
|
||||
///
|
||||
/// The output buffer never needs sto be larger than the input buffer.
|
||||
/// The output buffer never needs to be larger than the input buffer.
|
||||
/// The buffers may alias.
|
||||
pub fn parse(out: []u8, bytes: []const u8) ![]u8 {
|
||||
var dst_i: usize = 0;
|
||||
|
||||
@@ -56,7 +56,7 @@ pub const Location = enum {
|
||||
},
|
||||
|
||||
.resources => try std.fs.path.join(arena_alloc, &.{
|
||||
global_state.resources_dir orelse return null,
|
||||
global_state.resources_dir.app() orelse return null,
|
||||
"themes",
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ pub const regex =
|
||||
"(?:" ++ url_schemes ++
|
||||
\\)(?:
|
||||
++ ipv6_url_pattern ++
|
||||
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/*|\/)[\w\-.~:\/?#@!$&*+,;=%]+(?:\/[\w\-.~:\/?#@!$&*+,;=%]*)*
|
||||
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|\/)[\w\-.~:\/?#@!$&*+,;=%]+(?:\/[\w\-.~:\/?#@!$&*+,;=%]*)*
|
||||
;
|
||||
const url_schemes =
|
||||
\\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:
|
||||
@@ -112,6 +112,10 @@ test "url regex" {
|
||||
.input = "url with dashes [mode 2027](https://github.com/contour-terminal/terminal-unicode-core) for better unicode support",
|
||||
.expect = "https://github.com/contour-terminal/terminal-unicode-core",
|
||||
},
|
||||
.{
|
||||
.input = "dot.http://example.com",
|
||||
.expect = "http://example.com",
|
||||
},
|
||||
// weird characters in URL
|
||||
.{
|
||||
.input = "weird characters https://example.com/~user/?query=1&other=2#hash and more",
|
||||
|
||||
@@ -88,6 +88,7 @@ fn writeSyntax(writer: anytype) !void {
|
||||
\\let s:cpo_save = &cpo
|
||||
\\set cpo&vim
|
||||
\\
|
||||
\\syn iskeyword @,48-57,-
|
||||
\\syn keyword ghosttyConfigKeyword
|
||||
);
|
||||
|
||||
|
||||
@@ -81,6 +81,13 @@ pub fn init(gpa: Allocator) !void {
|
||||
fn initThread(gpa: Allocator) !void {
|
||||
if (comptime !build_options.sentry) return;
|
||||
|
||||
// Right now, on Darwin, `std.Thread.setName` can only name the current
|
||||
// thread, and we have no way to get the current thread from within it,
|
||||
// so instead we use this code to name the thread instead.
|
||||
if (builtin.os.tag.isDarwin()) {
|
||||
internal_os.macos.pthread_setname_np(&"sentry-init".*);
|
||||
}
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
@@ -166,7 +173,7 @@ fn beforeSend(
|
||||
event_val: sentry.c.sentry_value_t,
|
||||
_: ?*anyopaque,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.C) sentry.c.sentry_value_t {
|
||||
) callconv(.c) sentry.c.sentry_value_t {
|
||||
// The native SDK at the time of writing doesn't support thread-local
|
||||
// scopes. The full SDK has one global scope. So we use the beforeSend
|
||||
// handler to set thread-specific data such as window size, grid size,
|
||||
@@ -237,7 +244,7 @@ fn beforeSend(
|
||||
}
|
||||
|
||||
pub const Transport = struct {
|
||||
pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.C) void {
|
||||
pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.c) void {
|
||||
_ = ud;
|
||||
defer envelope.deinit();
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ pub const Item = union(enum) {
|
||||
|
||||
// Decode the item.
|
||||
self.* = switch (encoded.type) {
|
||||
.attachment => .{ .attachment = try Attachment.decode(
|
||||
.attachment => .{ .attachment = try .decode(
|
||||
alloc,
|
||||
encoded,
|
||||
) },
|
||||
|
||||
44
src/datastruct/array_list_collection.zig
Normal file
44
src/datastruct/array_list_collection.zig
Normal file
@@ -0,0 +1,44 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// A collection of ArrayLists with methods for bulk operations.
|
||||
pub fn ArrayListCollection(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = ArrayListCollection(T);
|
||||
const ArrayListT = std.ArrayListUnmanaged(T);
|
||||
|
||||
// An array containing the lists that belong to this collection.
|
||||
lists: []ArrayListT,
|
||||
|
||||
// The collection will be initialized with empty ArrayLists.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
list_count: usize,
|
||||
initial_capacity: usize,
|
||||
) Allocator.Error!Self {
|
||||
const self: Self = .{
|
||||
.lists = try alloc.alloc(ArrayListT, list_count),
|
||||
};
|
||||
|
||||
for (self.lists) |*list| {
|
||||
list.* = try .initCapacity(alloc, initial_capacity);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, alloc: Allocator) void {
|
||||
for (self.lists) |*list| {
|
||||
list.deinit(alloc);
|
||||
}
|
||||
alloc.free(self.lists);
|
||||
}
|
||||
|
||||
/// Clear all lists in the collection, retaining capacity.
|
||||
pub fn reset(self: *Self) void {
|
||||
for (self.lists) |*list| {
|
||||
list.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -70,7 +70,7 @@ pub fn CacheTable(
|
||||
/// become a pointless check, but hopefully branch prediction picks
|
||||
/// up on it at that point. The memory cost isn't too bad since it's
|
||||
/// just bytes, so should be a fraction the size of the main table.
|
||||
lengths: [bucket_count]u8 = [_]u8{0} ** bucket_count,
|
||||
lengths: [bucket_count]u8 = @splat(0),
|
||||
|
||||
/// An instance of the context structure.
|
||||
/// Must be initialized before calling any operations.
|
||||
|
||||
@@ -152,7 +152,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
/// If larger, new values will be set to the default value.
|
||||
pub fn resize(self: *Self, alloc: Allocator, size: usize) Allocator.Error!void {
|
||||
// Rotate to zero so it is aligned.
|
||||
try self.rotateToZero(alloc);
|
||||
try self.rotateToZero();
|
||||
|
||||
// Reallocate, this adds to the end so we're ready to go.
|
||||
const prev_len = self.len();
|
||||
@@ -173,29 +173,16 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
}
|
||||
|
||||
/// Rotate the data so that it is zero-aligned.
|
||||
fn rotateToZero(self: *Self, alloc: Allocator) Allocator.Error!void {
|
||||
// TODO: this does this in the worst possible way by allocating.
|
||||
// rewrite to not allocate, its possible, I'm just lazy right now.
|
||||
|
||||
fn rotateToZero(self: *Self) Allocator.Error!void {
|
||||
// If we're already at zero then do nothing.
|
||||
if (self.tail == 0) return;
|
||||
|
||||
var buf = try alloc.alloc(T, self.storage.len);
|
||||
defer {
|
||||
self.head = if (self.full) 0 else self.len();
|
||||
self.tail = 0;
|
||||
alloc.free(self.storage);
|
||||
self.storage = buf;
|
||||
}
|
||||
// We use std.mem.rotate to rotate our storage in-place.
|
||||
std.mem.rotate(T, self.storage, self.tail);
|
||||
|
||||
if (!self.full and self.head >= self.tail) {
|
||||
fastmem.copy(T, buf, self.storage[self.tail..self.head]);
|
||||
return;
|
||||
}
|
||||
|
||||
const middle = self.storage.len - self.tail;
|
||||
fastmem.copy(T, buf, self.storage[self.tail..]);
|
||||
fastmem.copy(T, buf[middle..], self.storage[0..self.head]);
|
||||
// Then fix up our head and tail.
|
||||
self.head = self.len() % self.storage.len;
|
||||
self.tail = 0;
|
||||
}
|
||||
|
||||
/// Returns if the buffer is currently empty. To check if its
|
||||
@@ -589,7 +576,7 @@ test "CircBuf rotateToZero" {
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
_ = buf.getPtrSlice(0, 11);
|
||||
try buf.rotateToZero(alloc);
|
||||
try buf.rotateToZero();
|
||||
}
|
||||
|
||||
test "CircBuf rotateToZero offset" {
|
||||
@@ -611,7 +598,7 @@ test "CircBuf rotateToZero offset" {
|
||||
try testing.expect(buf.tail > 0 and buf.head >= buf.tail);
|
||||
|
||||
// Rotate to zero
|
||||
try buf.rotateToZero(alloc);
|
||||
try buf.rotateToZero();
|
||||
try testing.expectEqual(@as(usize, 0), buf.tail);
|
||||
try testing.expectEqual(@as(usize, 1), buf.head);
|
||||
}
|
||||
@@ -645,7 +632,7 @@ test "CircBuf rotateToZero wraps" {
|
||||
}
|
||||
|
||||
// Rotate to zero
|
||||
try buf.rotateToZero(alloc);
|
||||
try buf.rotateToZero();
|
||||
try testing.expectEqual(@as(usize, 0), buf.tail);
|
||||
try testing.expectEqual(@as(usize, 3), buf.head);
|
||||
{
|
||||
@@ -681,7 +668,7 @@ test "CircBuf rotateToZero full no wrap" {
|
||||
}
|
||||
|
||||
// Rotate to zero
|
||||
try buf.rotateToZero(alloc);
|
||||
try buf.rotateToZero();
|
||||
try testing.expect(buf.full);
|
||||
try testing.expectEqual(@as(usize, 0), buf.tail);
|
||||
try testing.expectEqual(@as(usize, 0), buf.head);
|
||||
|
||||
102
src/file_type.zig
Normal file
102
src/file_type.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
const std = @import("std");
|
||||
|
||||
const type_details: []const struct {
|
||||
typ: FileType,
|
||||
sigs: []const []const ?u8,
|
||||
exts: []const []const u8,
|
||||
} = &.{
|
||||
.{
|
||||
.typ = .jpeg,
|
||||
.sigs = &.{
|
||||
&.{ 0xFF, 0xD8, 0xFF, 0xDB },
|
||||
&.{ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01 },
|
||||
&.{ 0xFF, 0xD8, 0xFF, 0xEE },
|
||||
&.{ 0xFF, 0xD8, 0xFF, 0xE1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 },
|
||||
&.{ 0xFF, 0xD8, 0xFF, 0xE0 },
|
||||
},
|
||||
.exts = &.{ ".jpg", ".jpeg", ".jfif" },
|
||||
},
|
||||
.{
|
||||
.typ = .png,
|
||||
.sigs = &.{&.{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }},
|
||||
.exts = &.{".png"},
|
||||
},
|
||||
.{
|
||||
.typ = .gif,
|
||||
.sigs = &.{
|
||||
&.{ 'G', 'I', 'F', '8', '7', 'a' },
|
||||
&.{ 'G', 'I', 'F', '8', '9', 'a' },
|
||||
},
|
||||
.exts = &.{".gif"},
|
||||
},
|
||||
.{
|
||||
.typ = .bmp,
|
||||
.sigs = &.{&.{ 'B', 'M' }},
|
||||
.exts = &.{".bmp"},
|
||||
},
|
||||
.{
|
||||
.typ = .qoi,
|
||||
.sigs = &.{&.{ 'q', 'o', 'i', 'f' }},
|
||||
.exts = &.{".qoi"},
|
||||
},
|
||||
.{
|
||||
.typ = .webp,
|
||||
.sigs = &.{
|
||||
&.{ 0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50 },
|
||||
},
|
||||
.exts = &.{".webp"},
|
||||
},
|
||||
};
|
||||
|
||||
/// This is a helper for detecting file types based on magic bytes.
|
||||
///
|
||||
/// Ref: https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
pub const FileType = enum {
|
||||
/// JPEG image file.
|
||||
jpeg,
|
||||
|
||||
/// PNG image file.
|
||||
png,
|
||||
|
||||
/// GIF image file.
|
||||
gif,
|
||||
|
||||
/// BMP image file.
|
||||
bmp,
|
||||
|
||||
/// QOI image file.
|
||||
qoi,
|
||||
|
||||
/// WebP image file.
|
||||
webp,
|
||||
|
||||
/// Unknown file format.
|
||||
unknown,
|
||||
|
||||
/// Detect file type based on the magic bytes
|
||||
/// at the start of the provided file contents.
|
||||
pub fn detect(contents: []const u8) FileType {
|
||||
inline for (type_details) |typ| {
|
||||
inline for (typ.sigs) |signature| {
|
||||
if (contents.len >= signature.len) {
|
||||
for (contents[0..signature.len], signature) |f, sig| {
|
||||
if (sig) |s| if (f != s) break;
|
||||
} else {
|
||||
return typ.typ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
/// Guess file type from its extension.
|
||||
pub fn guessFromExtension(extension: []const u8) FileType {
|
||||
inline for (type_details) |typ| {
|
||||
inline for (typ.exts) |ext| {
|
||||
if (std.ascii.eqlIgnoreCase(extension, ext)) return typ.typ;
|
||||
}
|
||||
}
|
||||
return .unknown;
|
||||
}
|
||||
};
|
||||
@@ -50,15 +50,18 @@ modified: std.atomic.Value(usize) = .{ .raw = 0 },
|
||||
resized: std.atomic.Value(usize) = .{ .raw = 0 },
|
||||
|
||||
pub const Format = enum(u8) {
|
||||
/// 1 byte per pixel grayscale.
|
||||
grayscale = 0,
|
||||
rgb = 1,
|
||||
rgba = 2,
|
||||
/// 3 bytes per pixel BGR.
|
||||
bgr = 1,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
bgra = 2,
|
||||
|
||||
pub fn depth(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.grayscale => 1,
|
||||
.rgb => 3,
|
||||
.rgba => 4,
|
||||
.bgr => 3,
|
||||
.bgra => 4,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -303,7 +306,12 @@ pub fn clear(self: *Atlas) void {
|
||||
}
|
||||
|
||||
/// Dump the atlas as a PPM to a writer, for debug purposes.
|
||||
/// Only supports grayscale and rgb atlases.
|
||||
/// Only supports grayscale and bgr atlases.
|
||||
///
|
||||
/// NOTE: BGR atlases will have the red and blue channels
|
||||
/// swapped because PPM expects RGB. This would be
|
||||
/// easy enough to fix so next time someone needs
|
||||
/// to debug a color atlas they should fix it.
|
||||
pub fn dump(self: Atlas, writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\P{c}
|
||||
@@ -313,7 +321,7 @@ pub fn dump(self: Atlas, writer: anytype) !void {
|
||||
, .{
|
||||
@as(u8, switch (self.format) {
|
||||
.grayscale => '5',
|
||||
.rgb => '6',
|
||||
.bgr => '6',
|
||||
else => {
|
||||
log.err("Unsupported format for dump: {}", .{self.format});
|
||||
@panic("Cannot dump this atlas format.");
|
||||
@@ -418,8 +426,16 @@ pub const Wasm = struct {
|
||||
|
||||
// We need to draw pixels so this is format dependent.
|
||||
const buf: []u8 = switch (self.format) {
|
||||
// RGBA is the native ImageData format
|
||||
.rgba => self.data,
|
||||
.bgra => buf: {
|
||||
// Convert from BGRA to RGBA by swapping every R and B.
|
||||
var buf: []u8 = try alloc.dupe(u8, self.data);
|
||||
errdefer alloc.free(buf);
|
||||
var i: usize = 0;
|
||||
while (i < self.data.len) : (i += 4) {
|
||||
std.mem.swap(u8, &buf[i], &buf[i + 2]);
|
||||
}
|
||||
break :buf buf;
|
||||
},
|
||||
|
||||
.grayscale => buf: {
|
||||
// Convert from A8 to RGBA so every 4th byte is set to a value.
|
||||
@@ -572,12 +588,12 @@ test "grow" {
|
||||
try testing.expectEqual(@as(u8, 4), atlas.data[atlas.size * 2 + 2]);
|
||||
}
|
||||
|
||||
test "writing RGB data" {
|
||||
test "writing BGR data" {
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try init(alloc, 32, .rgb);
|
||||
var atlas = try init(alloc, 32, .bgr);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
// This is RGB so its 3 bpp
|
||||
// This is BGR so its 3 bpp
|
||||
const reg = try atlas.reserve(alloc, 1, 2);
|
||||
atlas.set(reg, &[_]u8{
|
||||
1, 2, 3,
|
||||
@@ -594,18 +610,18 @@ test "writing RGB data" {
|
||||
try testing.expectEqual(@as(u8, 6), atlas.data[65 * depth + 2]);
|
||||
}
|
||||
|
||||
test "grow RGB" {
|
||||
test "grow BGR" {
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Atlas is 4x4 so its a 1px border meaning we only have 2x2 available
|
||||
var atlas = try init(alloc, 4, .rgb);
|
||||
var atlas = try init(alloc, 4, .bgr);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
// Get our 2x2, which should be ALL our usable space
|
||||
const reg = try atlas.reserve(alloc, 2, 2);
|
||||
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
|
||||
|
||||
// This is RGB so its 3 bpp
|
||||
// This is BGR so its 3 bpp
|
||||
atlas.set(reg, &[_]u8{
|
||||
10, 11, 12, // (0, 0) (x, y) from top-left
|
||||
13, 14, 15, // (1, 0)
|
||||
|
||||
@@ -37,7 +37,7 @@ collection: Collection,
|
||||
|
||||
/// The set of statuses and whether they're enabled or not. This defaults
|
||||
/// to true. This can be changed at runtime with no ill effect.
|
||||
styles: StyleStatus = StyleStatus.initFill(true),
|
||||
styles: StyleStatus = .initFill(true),
|
||||
|
||||
/// If discovery is available, we'll look up fonts where we can't find
|
||||
/// the codepoint. This can be set after initialization.
|
||||
@@ -140,7 +140,7 @@ pub fn getIndex(
|
||||
// handle this.
|
||||
if (self.sprite) |sprite| {
|
||||
if (sprite.hasCodepoint(cp, p)) {
|
||||
return Collection.Index.initSpecial(.sprite);
|
||||
return .initSpecial(.sprite);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ test getIndex {
|
||||
const testEmoji = font.embedded.emoji;
|
||||
const testEmojiText = font.embedded.emoji_text;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = Collection.init();
|
||||
@@ -388,7 +388,7 @@ test getIndex {
|
||||
|
||||
{
|
||||
errdefer c.deinit(alloc);
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -398,7 +398,7 @@ test getIndex {
|
||||
_ = try c.add(
|
||||
alloc,
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(
|
||||
.{ .loaded = try .init(
|
||||
lib,
|
||||
testEmoji,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
@@ -408,7 +408,7 @@ test getIndex {
|
||||
_ = try c.add(
|
||||
alloc,
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(
|
||||
.{ .loaded = try .init(
|
||||
lib,
|
||||
testEmojiText,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
@@ -461,23 +461,23 @@ test "getIndex disabled font style" {
|
||||
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas_grayscale.deinit(alloc);
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = Collection.init();
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
) });
|
||||
_ = try c.add(alloc, .bold, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .bold, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
) });
|
||||
_ = try c.add(alloc, .italic, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .italic, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -513,7 +513,7 @@ test "getIndex box glyph" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
const c = Collection.init();
|
||||
|
||||
@@ -55,7 +55,7 @@ load_options: ?LoadOptions = null,
|
||||
pub fn init() Collection {
|
||||
// Initialize our styles array, preallocating some space that is
|
||||
// likely to be used.
|
||||
return .{ .faces = StyleArray.initFill(.{}) };
|
||||
return .{ .faces = .initFill(.{}) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Collection, alloc: Allocator) void {
|
||||
@@ -78,8 +78,8 @@ pub const AddError = Allocator.Error || error{
|
||||
/// next in priority if others exist already, i.e. it'll be the _last_ to be
|
||||
/// searched for a glyph in that list.
|
||||
///
|
||||
/// The collection takes ownership of the face. The face will be deallocated
|
||||
/// when the collection is deallocated.
|
||||
/// If no error is encountered then the collection takes ownership of the face,
|
||||
/// in which case face will be deallocated when the collection is deallocated.
|
||||
///
|
||||
/// If a loaded face is added to the collection, it should be the same
|
||||
/// size as all the other faces in the collection. This function will not
|
||||
@@ -700,29 +700,32 @@ test "add full" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.regular;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
|
||||
for (0..Index.Special.start - 1) |_| {
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
) });
|
||||
}
|
||||
|
||||
try testing.expectError(error.CollectionFull, c.add(
|
||||
alloc,
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
) },
|
||||
));
|
||||
var face = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
);
|
||||
// We have to deinit it manually since the
|
||||
// collection doesn't do it if adding fails.
|
||||
defer face.deinit();
|
||||
try testing.expectError(
|
||||
error.CollectionFull,
|
||||
c.add(alloc, .regular, .{ .loaded = face }),
|
||||
);
|
||||
}
|
||||
|
||||
test "add deferred without loading options" {
|
||||
@@ -746,13 +749,13 @@ test getFace {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.regular;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
|
||||
const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
const idx = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -770,13 +773,13 @@ test getIndex {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.regular;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -801,14 +804,14 @@ test completeStyles {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.regular;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -828,14 +831,14 @@ test setSize {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.regular;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -851,14 +854,14 @@ test hasCodepoint {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.regular;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
const idx = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -875,14 +878,14 @@ test "hasCodepoint emoji default graphical" {
|
||||
const alloc = testing.allocator;
|
||||
const testEmoji = font.embedded.emoji;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
const idx = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testEmoji,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -898,14 +901,14 @@ test "metrics" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.inconsolata;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
c.load_options = .{ .library = lib };
|
||||
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
|
||||
@@ -254,7 +254,7 @@ fn loadWebCanvas(
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
const wc = self.wc.?;
|
||||
return try Face.initNamed(wc.alloc, wc.font_str, opts, wc.presentation);
|
||||
return try .initNamed(wc.alloc, wc.font_str, opts, wc.presentation);
|
||||
}
|
||||
|
||||
/// Returns true if this face can satisfy the given codepoint and
|
||||
@@ -407,7 +407,7 @@ test "fontconfig" {
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Load freetype
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
// Get a deferred face from fontconfig
|
||||
@@ -425,7 +425,8 @@ test "fontconfig" {
|
||||
try testing.expect(n.len > 0);
|
||||
|
||||
// Load it and verify it works
|
||||
const face = try def.load(lib, .{ .size = .{ .points = 12 } });
|
||||
var face = try def.load(lib, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
try testing.expect(face.glyphIndex(' ') != null);
|
||||
}
|
||||
|
||||
@@ -437,7 +438,7 @@ test "coretext" {
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Load freetype
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
// Get a deferred face from fontconfig
|
||||
@@ -456,6 +457,7 @@ test "coretext" {
|
||||
try testing.expect(n.len > 0);
|
||||
|
||||
// Load it and verify it works
|
||||
const face = try def.load(lib, .{ .size = .{ .points = 12 } });
|
||||
var face = try def.load(lib, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
try testing.expect(face.glyphIndex(' ') != null);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const log = std.log.scoped(.font_shared_grid);
|
||||
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{},
|
||||
|
||||
/// Cache for glyph renders into the atlas.
|
||||
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Render) = .{},
|
||||
glyphs: std.HashMapUnmanaged(GlyphKey, Render, GlyphKey.Context, 80) = .{},
|
||||
|
||||
/// The texture atlas to store renders in. The Glyph data in the glyphs
|
||||
/// cache is dependent on the atlas matching.
|
||||
@@ -79,7 +79,7 @@ pub fn init(
|
||||
|
||||
var atlas_grayscale = try Atlas.init(alloc, 512, .grayscale);
|
||||
errdefer atlas_grayscale.deinit(alloc);
|
||||
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||
var atlas_color = try Atlas.init(alloc, 512, .bgra);
|
||||
errdefer atlas_color.deinit(alloc);
|
||||
|
||||
var result: SharedGrid = .{
|
||||
@@ -307,6 +307,39 @@ const GlyphKey = struct {
|
||||
index: Collection.Index,
|
||||
glyph: u32,
|
||||
opts: RenderOptions,
|
||||
|
||||
const Context = struct {
|
||||
pub fn hash(_: Context, key: GlyphKey) u64 {
|
||||
return @bitCast(Packed.from(key));
|
||||
}
|
||||
|
||||
pub fn eql(_: Context, a: GlyphKey, b: GlyphKey) bool {
|
||||
return Packed.from(a) == Packed.from(b);
|
||||
}
|
||||
};
|
||||
|
||||
const Packed = packed struct(u64) {
|
||||
index: Collection.Index,
|
||||
glyph: u32,
|
||||
opts: packed struct(u16) {
|
||||
cell_width: u2,
|
||||
thicken: bool,
|
||||
thicken_strength: u8,
|
||||
_padding: u5 = 0,
|
||||
},
|
||||
|
||||
inline fn from(key: GlyphKey) Packed {
|
||||
return .{
|
||||
.index = key.index,
|
||||
.glyph = key.glyph,
|
||||
.opts = .{
|
||||
.cell_width = key.opts.cell_width orelse 0,
|
||||
.thicken = key.opts.thicken,
|
||||
.thicken_strength = key.opts.thicken_strength,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const TestMode = enum { normal };
|
||||
@@ -319,7 +352,7 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid {
|
||||
|
||||
switch (mode) {
|
||||
.normal => {
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
@@ -338,7 +371,7 @@ test getIndex {
|
||||
const alloc = testing.allocator;
|
||||
// const testEmoji = @import("test.zig").fontEmoji;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var grid = try testGrid(.normal, alloc, lib);
|
||||
|
||||
@@ -50,7 +50,7 @@ pub const InitError = Library.InitError;
|
||||
|
||||
/// Initialize a new SharedGridSet.
|
||||
pub fn init(alloc: Allocator) InitError!SharedGridSet {
|
||||
var font_lib = try Library.init();
|
||||
var font_lib = try Library.init(alloc);
|
||||
errdefer font_lib.deinit();
|
||||
|
||||
return .{
|
||||
@@ -126,7 +126,7 @@ pub fn ref(
|
||||
.ref = 1,
|
||||
};
|
||||
|
||||
grid.* = try SharedGrid.init(self.alloc, resolver: {
|
||||
grid.* = try .init(self.alloc, resolver: {
|
||||
// Build our collection. This is the expensive operation that
|
||||
// involves finding fonts, loading them (maybe, some are deferred),
|
||||
// etc.
|
||||
@@ -258,7 +258,7 @@ fn collection(
|
||||
_ = try c.add(
|
||||
self.alloc,
|
||||
.regular,
|
||||
.{ .fallback_loaded = try Face.init(
|
||||
.{ .fallback_loaded = try .init(
|
||||
self.font_lib,
|
||||
font.embedded.regular,
|
||||
load_options.faceOptions(),
|
||||
@@ -267,7 +267,7 @@ fn collection(
|
||||
_ = try c.add(
|
||||
self.alloc,
|
||||
.bold,
|
||||
.{ .fallback_loaded = try Face.init(
|
||||
.{ .fallback_loaded = try .init(
|
||||
self.font_lib,
|
||||
font.embedded.bold,
|
||||
load_options.faceOptions(),
|
||||
@@ -276,7 +276,7 @@ fn collection(
|
||||
_ = try c.add(
|
||||
self.alloc,
|
||||
.italic,
|
||||
.{ .fallback_loaded = try Face.init(
|
||||
.{ .fallback_loaded = try .init(
|
||||
self.font_lib,
|
||||
font.embedded.italic,
|
||||
load_options.faceOptions(),
|
||||
@@ -285,7 +285,7 @@ fn collection(
|
||||
_ = try c.add(
|
||||
self.alloc,
|
||||
.bold_italic,
|
||||
.{ .fallback_loaded = try Face.init(
|
||||
.{ .fallback_loaded = try .init(
|
||||
self.font_lib,
|
||||
font.embedded.bold_italic,
|
||||
load_options.faceOptions(),
|
||||
@@ -318,7 +318,7 @@ fn collection(
|
||||
_ = try c.add(
|
||||
self.alloc,
|
||||
.regular,
|
||||
.{ .fallback_loaded = try Face.init(
|
||||
.{ .fallback_loaded = try .init(
|
||||
self.font_lib,
|
||||
font.embedded.emoji,
|
||||
load_options.faceOptions(),
|
||||
@@ -327,7 +327,7 @@ fn collection(
|
||||
_ = try c.add(
|
||||
self.alloc,
|
||||
.regular,
|
||||
.{ .fallback_loaded = try Face.init(
|
||||
.{ .fallback_loaded = try .init(
|
||||
self.font_lib,
|
||||
font.embedded.emoji_text,
|
||||
load_options.faceOptions(),
|
||||
@@ -391,7 +391,7 @@ fn discover(self: *SharedGridSet) !?*Discover {
|
||||
// If we initialized, use it
|
||||
if (self.font_discover) |*v| return v;
|
||||
|
||||
self.font_discover = Discover.init();
|
||||
self.font_discover = .init();
|
||||
return &self.font_discover.?;
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ pub const Key = struct {
|
||||
/// each style. For example, bold is from
|
||||
/// offsets[@intFromEnum(.bold) - 1] to
|
||||
/// offsets[@intFromEnum(.bold)].
|
||||
style_offsets: StyleOffsets = .{0} ** style_offsets_len,
|
||||
style_offsets: StyleOffsets = @splat(0),
|
||||
|
||||
/// The codepoint map configuration.
|
||||
codepoint_map: CodepointMap = .{},
|
||||
|
||||
@@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const fontconfig = @import("fontconfig");
|
||||
const macos = @import("macos");
|
||||
const opentype = @import("opentype.zig");
|
||||
const options = @import("main.zig").options;
|
||||
const Collection = @import("main.zig").Collection;
|
||||
const DeferredFace = @import("main.zig").DeferredFace;
|
||||
@@ -562,149 +563,266 @@ pub const CoreText = struct {
|
||||
desc: *const Descriptor,
|
||||
list: []*macos.text.FontDescriptor,
|
||||
) void {
|
||||
var desc_mut = desc.*;
|
||||
if (desc_mut.style == null) {
|
||||
// If there is no explicit style set, we set a preferred
|
||||
// based on the style bool attributes.
|
||||
//
|
||||
// TODO: doesn't handle i18n font names well, we should have
|
||||
// another mechanism that uses the weight attribute if it exists.
|
||||
// Wait for this to be a real problem.
|
||||
desc_mut.style = if (desc_mut.bold and desc_mut.italic)
|
||||
"Bold Italic"
|
||||
else if (desc_mut.bold)
|
||||
"Bold"
|
||||
else if (desc_mut.italic)
|
||||
"Italic"
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
std.mem.sortUnstable(*macos.text.FontDescriptor, list, &desc_mut, struct {
|
||||
std.mem.sortUnstable(*macos.text.FontDescriptor, list, desc, struct {
|
||||
fn lessThan(
|
||||
desc_inner: *const Descriptor,
|
||||
lhs: *macos.text.FontDescriptor,
|
||||
rhs: *macos.text.FontDescriptor,
|
||||
) bool {
|
||||
const lhs_score = score(desc_inner, lhs);
|
||||
const rhs_score = score(desc_inner, rhs);
|
||||
const lhs_score: Score = .score(desc_inner, lhs);
|
||||
const rhs_score: Score = .score(desc_inner, rhs);
|
||||
// Higher score is "less" (earlier)
|
||||
return lhs_score.int() > rhs_score.int();
|
||||
}
|
||||
}.lessThan);
|
||||
}
|
||||
|
||||
/// We represent our sorting score as a packed struct so that we can
|
||||
/// compare scores numerically but build scores symbolically.
|
||||
/// We represent our sorting score as a packed struct so that we
|
||||
/// can compare scores numerically but build scores symbolically.
|
||||
///
|
||||
/// Note that packed structs store their fields from least to most
|
||||
/// significant, so the fields here are defined in increasing order
|
||||
/// of precedence.
|
||||
const Score = packed struct {
|
||||
const Backing = @typeInfo(@This()).@"struct".backing_integer.?;
|
||||
|
||||
glyph_count: u16 = 0, // clamped if > intmax
|
||||
traits: Traits = .unmatched,
|
||||
style: Style = .unmatched,
|
||||
/// Number of glyphs in the font, if two fonts have identical
|
||||
/// scores otherwise then we prefer the one with more glyphs.
|
||||
///
|
||||
/// (Number of glyphs clamped at u16 intmax)
|
||||
glyph_count: u16 = 0,
|
||||
/// A fuzzy match on the style string, less important than
|
||||
/// an exact match, and less important than trait matches.
|
||||
fuzzy_style: u8 = 0,
|
||||
/// Whether the bold-ness of the font matches the descriptor.
|
||||
/// This is less important than italic because a font that's italic
|
||||
/// when it shouldn't be or not italic when it should be is a bigger
|
||||
/// problem (subjectively) than being the wrong weight.
|
||||
bold: bool = false,
|
||||
/// Whether the italic-ness of the font matches the descriptor.
|
||||
/// This is less important than an exact match on the style string
|
||||
/// because we want users to be allowed to override trait matching
|
||||
/// for the bold/italic/bold italic styles if they want.
|
||||
italic: bool = false,
|
||||
/// An exact (case-insensitive) match on the style string.
|
||||
exact_style: bool = false,
|
||||
/// Whether the font is monospace, this is more important than any of
|
||||
/// the other fields unless we're looking for a specific codepoint,
|
||||
/// in which case that is the most important thing.
|
||||
monospace: bool = false,
|
||||
/// If we're looking for a codepoint, whether this font has it.
|
||||
codepoint: bool = false,
|
||||
|
||||
const Traits = enum(u8) { unmatched = 0, _ };
|
||||
const Style = enum(u8) { unmatched = 0, match = 0xFF, _ };
|
||||
|
||||
pub fn int(self: Score) Backing {
|
||||
return @bitCast(self);
|
||||
}
|
||||
};
|
||||
|
||||
fn score(desc: *const Descriptor, ct_desc: *const macos.text.FontDescriptor) Score {
|
||||
var score_acc: Score = .{};
|
||||
fn score(desc: *const Descriptor, ct_desc: *const macos.text.FontDescriptor) Score {
|
||||
var self: Score = .{};
|
||||
|
||||
// We always load the font if we can since some things can only be
|
||||
// inspected on the font itself.
|
||||
const font_: ?*macos.text.Font = macos.text.Font.createWithFontDescriptor(
|
||||
ct_desc,
|
||||
12,
|
||||
) catch null;
|
||||
defer if (font_) |font| font.release();
|
||||
// We always load the font if we can since some things can only be
|
||||
// inspected on the font itself. Fonts that can't be loaded score
|
||||
// 0 automatically because we don't want a font we can't load.
|
||||
const font: *macos.text.Font = macos.text.Font.createWithFontDescriptor(
|
||||
ct_desc,
|
||||
12,
|
||||
) catch return self;
|
||||
defer font.release();
|
||||
|
||||
// If we have a font, prefer the font with more glyphs.
|
||||
if (font_) |font| {
|
||||
const Type = @TypeOf(score_acc.glyph_count);
|
||||
score_acc.glyph_count = std.math.cast(
|
||||
Type,
|
||||
font.getGlyphCount(),
|
||||
) orelse std.math.maxInt(Type);
|
||||
}
|
||||
|
||||
// If we're searching for a codepoint, prioritize fonts that
|
||||
// have that codepoint.
|
||||
if (desc.codepoint > 0) codepoint: {
|
||||
const font = font_ orelse break :codepoint;
|
||||
|
||||
// Turn UTF-32 into UTF-16 for CT API
|
||||
var unichars: [2]u16 = undefined;
|
||||
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
|
||||
desc.codepoint,
|
||||
&unichars,
|
||||
);
|
||||
const len: usize = if (pair) 2 else 1;
|
||||
|
||||
// Get our glyphs
|
||||
var glyphs = [2]macos.graphics.Glyph{ 0, 0 };
|
||||
score_acc.codepoint = font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len]);
|
||||
}
|
||||
|
||||
// Get our symbolic traits for the descriptor so we can compare
|
||||
// boolean attributes like bold, monospace, etc.
|
||||
const symbolic_traits: macos.text.FontSymbolicTraits = traits: {
|
||||
const traits = ct_desc.copyAttribute(.traits) orelse break :traits .{};
|
||||
defer traits.release();
|
||||
|
||||
const key = macos.text.FontTraitKey.symbolic.key();
|
||||
const symbolic = traits.getValue(macos.foundation.Number, key) orelse
|
||||
break :traits .{};
|
||||
|
||||
break :traits macos.text.FontSymbolicTraits.init(symbolic);
|
||||
};
|
||||
|
||||
score_acc.monospace = symbolic_traits.monospace;
|
||||
|
||||
score_acc.style = style: {
|
||||
const style = ct_desc.copyAttribute(.style_name) orelse
|
||||
break :style .unmatched;
|
||||
defer style.release();
|
||||
|
||||
// Get our style string
|
||||
var buf: [128]u8 = undefined;
|
||||
const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched;
|
||||
|
||||
// If we have a specific desired style, attempt to search for that.
|
||||
if (desc.style) |desired_style| {
|
||||
// Matching style string gets highest score
|
||||
if (std.mem.eql(u8, desired_style, style_str)) break :style .match;
|
||||
} else if (!desc.bold and !desc.italic) {
|
||||
// If we do not, and we have no symbolic traits, then we try
|
||||
// to find "regular" (or no style). If we have symbolic traits
|
||||
// we do nothing but we can improve scoring by taking that into
|
||||
// account, too.
|
||||
if (std.mem.eql(u8, "Regular", style_str)) {
|
||||
break :style .match;
|
||||
}
|
||||
// We prefer fonts with more glyphs, all else being equal.
|
||||
{
|
||||
const Type = @TypeOf(self.glyph_count);
|
||||
self.glyph_count = std.math.cast(
|
||||
Type,
|
||||
font.getGlyphCount(),
|
||||
) orelse std.math.maxInt(Type);
|
||||
}
|
||||
|
||||
// Otherwise the score is based on the length of the style string.
|
||||
// Shorter styles are scored higher. This is a heuristic that
|
||||
// if we don't have a desired style then shorter tends to be
|
||||
// more often the "regular" style.
|
||||
break :style @enumFromInt(100 -| style_str.len);
|
||||
};
|
||||
// If we're searching for a codepoint, then we
|
||||
// prioritize fonts that have that codepoint.
|
||||
if (desc.codepoint > 0) {
|
||||
// Turn UTF-32 into UTF-16 for CT API
|
||||
var unichars: [2]u16 = undefined;
|
||||
const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
|
||||
desc.codepoint,
|
||||
&unichars,
|
||||
);
|
||||
const len: usize = if (pair) 2 else 1;
|
||||
|
||||
score_acc.traits = traits: {
|
||||
var count: u8 = 0;
|
||||
if (desc.bold == symbolic_traits.bold) count += 1;
|
||||
if (desc.italic == symbolic_traits.italic) count += 1;
|
||||
break :traits @enumFromInt(count);
|
||||
};
|
||||
// Get our glyphs
|
||||
var glyphs = [2]macos.graphics.Glyph{ 0, 0 };
|
||||
self.codepoint = font.getGlyphsForCharacters(
|
||||
unichars[0..len],
|
||||
glyphs[0..len],
|
||||
);
|
||||
}
|
||||
|
||||
return score_acc;
|
||||
}
|
||||
// Get our symbolic traits for the descriptor so we can
|
||||
// compare boolean attributes like bold, monospace, etc.
|
||||
const symbolic_traits: macos.text.FontSymbolicTraits = traits: {
|
||||
const traits = ct_desc.copyAttribute(.traits) orelse break :traits .{};
|
||||
defer traits.release();
|
||||
|
||||
const key = macos.text.FontTraitKey.symbolic.key();
|
||||
const symbolic = traits.getValue(macos.foundation.Number, key) orelse
|
||||
break :traits .{};
|
||||
|
||||
break :traits macos.text.FontSymbolicTraits.init(symbolic);
|
||||
};
|
||||
|
||||
self.monospace = symbolic_traits.monospace;
|
||||
|
||||
// We try to derived data from the font itself, which is generally
|
||||
// more reliable than only using the symbolic traits for this.
|
||||
const is_bold: bool, const is_italic: bool = derived: {
|
||||
// We start with initial guesses based on the symbolic traits,
|
||||
// but refine these with more information if we can get it.
|
||||
var is_italic = symbolic_traits.italic;
|
||||
var is_bold = symbolic_traits.bold;
|
||||
|
||||
// Read the 'head' table out of the font data if it's available.
|
||||
if (head: {
|
||||
const tag = macos.text.FontTableTag.init("head");
|
||||
const data = font.copyTable(tag) orelse break :head null;
|
||||
defer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :head opentype.Head.init(ptr[0..len]) catch |err| {
|
||||
log.warn("error parsing head table: {}", .{err});
|
||||
break :head null;
|
||||
};
|
||||
}) |head_| {
|
||||
const head: opentype.Head = head_;
|
||||
is_bold = is_bold or (head.macStyle & 1 == 1);
|
||||
is_italic = is_italic or (head.macStyle & 2 == 2);
|
||||
}
|
||||
|
||||
// Read the 'OS/2' table out of the font data if it's available.
|
||||
if (os2: {
|
||||
const tag = macos.text.FontTableTag.init("OS/2");
|
||||
const data = font.copyTable(tag) orelse break :os2 null;
|
||||
defer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :os2 opentype.OS2.init(ptr[0..len]) catch |err| {
|
||||
log.warn("error parsing OS/2 table: {}", .{err});
|
||||
break :os2 null;
|
||||
};
|
||||
}) |os2| {
|
||||
is_bold = is_bold or os2.fsSelection.bold;
|
||||
is_italic = is_italic or os2.fsSelection.italic;
|
||||
}
|
||||
|
||||
// Check if we have variation axes in our descriptor, if we
|
||||
// do then we can derive weight italic-ness or both from them.
|
||||
if (font.copyAttribute(.variation_axes)) |axes| variations: {
|
||||
defer axes.release();
|
||||
|
||||
// Copy the variation values for this instance of the font.
|
||||
// if there are none then we just break out immediately.
|
||||
const values: *macos.foundation.Dictionary =
|
||||
font.copyAttribute(.variation) orelse break :variations;
|
||||
defer values.release();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
|
||||
// If we see the 'ital' value then we ignore 'slnt'.
|
||||
var ital_seen = false;
|
||||
|
||||
const len = axes.getCount();
|
||||
for (0..len) |i| {
|
||||
const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i);
|
||||
const Key = macos.text.FontVariationAxisKey;
|
||||
const cf_id = dict.getValue(Key.identifier.Value(), Key.identifier.key()).?;
|
||||
const cf_name = dict.getValue(Key.name.Value(), Key.name.key()).?;
|
||||
const cf_def = dict.getValue(Key.default_value.Value(), Key.default_value.key()).?;
|
||||
|
||||
const name_str = cf_name.cstring(&buf, .utf8) orelse "";
|
||||
|
||||
// Default value
|
||||
var def: f64 = 0;
|
||||
_ = cf_def.getValue(.double, &def);
|
||||
// Value in this font
|
||||
var val: f64 = def;
|
||||
if (values.getValue(
|
||||
macos.foundation.Number,
|
||||
cf_id,
|
||||
)) |cf_val| _ = cf_val.getValue(.double, &val);
|
||||
|
||||
if (std.mem.eql(u8, "wght", name_str)) {
|
||||
// Somewhat subjective threshold, we consider fonts
|
||||
// bold if they have a 'wght' set greater than 600.
|
||||
is_bold = val > 600;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, "ital", name_str)) {
|
||||
is_italic = val > 0.5;
|
||||
ital_seen = true;
|
||||
continue;
|
||||
}
|
||||
if (!ital_seen and std.mem.eql(u8, "slnt", name_str)) {
|
||||
// Arbitrary threshold of anything more than a 5
|
||||
// degree clockwise slant is considered italic.
|
||||
is_italic = val <= -5.0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :derived .{ is_bold, is_italic };
|
||||
};
|
||||
|
||||
self.bold = desc.bold == is_bold;
|
||||
self.italic = desc.italic == is_italic;
|
||||
|
||||
// Get the style string from the font.
|
||||
var style_str_buf: [128]u8 = undefined;
|
||||
const style_str: []const u8 = style_str: {
|
||||
const style = ct_desc.copyAttribute(.style_name) orelse
|
||||
break :style_str "";
|
||||
defer style.release();
|
||||
|
||||
break :style_str style.cstring(&style_str_buf, .utf8) orelse "";
|
||||
};
|
||||
|
||||
// The first string in this slice will be used for the exact match,
|
||||
// and for the fuzzy match, all matching substrings will increase
|
||||
// the rank.
|
||||
const desired_styles: []const [:0]const u8 = desired: {
|
||||
if (desc.style) |s| break :desired &.{s};
|
||||
|
||||
// If we don't have an explicitly desired style name, we base
|
||||
// it on the bold and italic properties, this isn't ideal since
|
||||
// fonts may use style names other than these, but it helps in
|
||||
// some edge cases.
|
||||
if (desc.bold) {
|
||||
if (desc.italic) break :desired &.{ "bold italic", "bold", "italic", "oblique" };
|
||||
break :desired &.{ "bold", "upright" };
|
||||
} else if (desc.italic) {
|
||||
break :desired &.{ "italic", "regular", "oblique" };
|
||||
}
|
||||
break :desired &.{ "regular", "upright" };
|
||||
};
|
||||
|
||||
self.exact_style = std.ascii.eqlIgnoreCase(
|
||||
style_str,
|
||||
desired_styles[0],
|
||||
);
|
||||
// Our "fuzzy match" score is 0 if the desired style isn't present
|
||||
// in the string, otherwise we give higher priority for styles that
|
||||
// have fewer characters not in the desired_styles list.
|
||||
const fuzzy_type = @TypeOf(self.fuzzy_style);
|
||||
self.fuzzy_style = @intCast(style_str.len);
|
||||
for (desired_styles) |s| {
|
||||
if (std.ascii.indexOfIgnoreCase(style_str, s) != null) {
|
||||
self.fuzzy_style -|= @intCast(s.len);
|
||||
}
|
||||
}
|
||||
self.fuzzy_style = std.math.maxInt(fuzzy_type) -| self.fuzzy_style;
|
||||
|
||||
return self;
|
||||
}
|
||||
};
|
||||
|
||||
pub const DiscoverIterator = struct {
|
||||
alloc: Allocator,
|
||||
@@ -837,3 +955,85 @@ test "coretext codepoint" {
|
||||
// Should have other codepoints too
|
||||
try testing.expect(face.hasCodepoint('B', null));
|
||||
}
|
||||
|
||||
test "coretext sorting" {
|
||||
if (options.backend != .coretext and options.backend != .coretext_freetype)
|
||||
return error.SkipZigTest;
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!//
|
||||
// FIXME: Disabled for now because SF Pro is not available in CI
|
||||
// The solution likely involves directly testing that the
|
||||
// `sortMatchingDescriptors` function sorts a bundled test
|
||||
// font correctly, instead of relying on the system fonts.
|
||||
if (true) return error.SkipZigTest;
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!//
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var ct = CoreText.init();
|
||||
defer ct.deinit();
|
||||
|
||||
// We try to get a Regular, Italic, Bold, & Bold Italic version of SF Pro,
|
||||
// which should be installed on all Macs, and has many styles which makes
|
||||
// it a good test, since there will be many results for each discovery.
|
||||
|
||||
// Regular
|
||||
{
|
||||
var it = try ct.discover(alloc, .{
|
||||
.family = "SF Pro",
|
||||
.size = 12,
|
||||
});
|
||||
defer it.deinit();
|
||||
const res = (try it.next()).?;
|
||||
var buf: [1024]u8 = undefined;
|
||||
const name = try res.name(&buf);
|
||||
try testing.expectEqualStrings("SF Pro Regular", name);
|
||||
}
|
||||
|
||||
// Regular Italic
|
||||
//
|
||||
// NOTE: This makes sure that we don't accidentally prefer "Thin Italic",
|
||||
// which we previously did, because it has a shorter name.
|
||||
{
|
||||
var it = try ct.discover(alloc, .{
|
||||
.family = "SF Pro",
|
||||
.size = 12,
|
||||
.italic = true,
|
||||
});
|
||||
defer it.deinit();
|
||||
const res = (try it.next()).?;
|
||||
var buf: [1024]u8 = undefined;
|
||||
const name = try res.name(&buf);
|
||||
try testing.expectEqualStrings("SF Pro Regular Italic", name);
|
||||
}
|
||||
|
||||
// Bold
|
||||
{
|
||||
var it = try ct.discover(alloc, .{
|
||||
.family = "SF Pro",
|
||||
.size = 12,
|
||||
.bold = true,
|
||||
});
|
||||
defer it.deinit();
|
||||
const res = (try it.next()).?;
|
||||
var buf: [1024]u8 = undefined;
|
||||
const name = try res.name(&buf);
|
||||
try testing.expectEqualStrings("SF Pro Bold", name);
|
||||
}
|
||||
|
||||
// Bold Italic
|
||||
{
|
||||
var it = try ct.discover(alloc, .{
|
||||
.family = "SF Pro",
|
||||
.size = 12,
|
||||
.bold = true,
|
||||
.italic = true,
|
||||
});
|
||||
defer it.deinit();
|
||||
const res = (try it.next()).?;
|
||||
var buf: [1024]u8 = undefined;
|
||||
const name = try res.name(&buf);
|
||||
try testing.expectEqualStrings("SF Pro Bold Italic", name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,11 @@ pub const Face = struct {
|
||||
};
|
||||
|
||||
/// Initialize a CoreText-based font from a TTF/TTC in memory.
|
||||
pub fn init(lib: font.Library, source: [:0]const u8, opts: font.face.Options) !Face {
|
||||
pub fn init(
|
||||
lib: font.Library,
|
||||
source: [:0]const u8,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
_ = lib;
|
||||
|
||||
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
|
||||
@@ -93,7 +97,7 @@ pub const Face = struct {
|
||||
errdefer if (comptime harfbuzz_shaper) hb_font.destroy();
|
||||
|
||||
const color: ?ColorState = if (traits.color_glyphs)
|
||||
try ColorState.init(ct_font)
|
||||
try .init(ct_font)
|
||||
else
|
||||
null;
|
||||
errdefer if (color) |v| v.deinit();
|
||||
@@ -914,7 +918,7 @@ test "in-memory" {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var lib = try font.Library.init();
|
||||
var lib = try font.Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
@@ -941,7 +945,7 @@ test "variable" {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var lib = try font.Library.init();
|
||||
var lib = try font.Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
@@ -968,7 +972,7 @@ test "variable set variation" {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var lib = try font.Library.init();
|
||||
var lib = try font.Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
@@ -996,7 +1000,7 @@ test "svg font table" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.julia_mono;
|
||||
|
||||
var lib = try font.Library.init();
|
||||
var lib = try font.Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
@@ -1010,9 +1014,10 @@ test "svg font table" {
|
||||
|
||||
test "glyphIndex colored vs text" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.julia_mono;
|
||||
|
||||
var lib = try font.Library.init();
|
||||
var lib = try font.Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
|
||||
@@ -29,12 +29,20 @@ pub const Face = struct {
|
||||
assert(font.face.FreetypeLoadFlags != void);
|
||||
}
|
||||
|
||||
/// Our freetype library
|
||||
lib: freetype.Library,
|
||||
/// Our Library
|
||||
lib: Library,
|
||||
|
||||
/// Our font face.
|
||||
face: freetype.Face,
|
||||
|
||||
/// This mutex MUST be held while doing anything with the
|
||||
/// glyph slot on the freetype face, because this struct
|
||||
/// may be shared across multiple surfaces.
|
||||
///
|
||||
/// This means that anywhere where `self.face.loadGlyph`
|
||||
/// is called, this mutex must be held.
|
||||
ft_mutex: *std.Thread.Mutex,
|
||||
|
||||
/// Harfbuzz font corresponding to this face.
|
||||
hb_font: harfbuzz.Font,
|
||||
|
||||
@@ -59,30 +67,52 @@ pub const Face = struct {
|
||||
};
|
||||
|
||||
/// Initialize a new font face with the given source in-memory.
|
||||
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, opts: font.face.Options) !Face {
|
||||
pub fn initFile(
|
||||
lib: Library,
|
||||
path: [:0]const u8,
|
||||
index: i32,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
lib.mutex.lock();
|
||||
defer lib.mutex.unlock();
|
||||
const face = try lib.lib.initFace(path, index);
|
||||
errdefer face.deinit();
|
||||
return try initFace(lib, face, opts);
|
||||
}
|
||||
|
||||
/// Initialize a new font face with the given source in-memory.
|
||||
pub fn init(lib: Library, source: [:0]const u8, opts: font.face.Options) !Face {
|
||||
pub fn init(
|
||||
lib: Library,
|
||||
source: [:0]const u8,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
lib.mutex.lock();
|
||||
defer lib.mutex.unlock();
|
||||
const face = try lib.lib.initMemoryFace(source, 0);
|
||||
errdefer face.deinit();
|
||||
return try initFace(lib, face, opts);
|
||||
}
|
||||
|
||||
fn initFace(lib: Library, face: freetype.Face, opts: font.face.Options) !Face {
|
||||
fn initFace(
|
||||
lib: Library,
|
||||
face: freetype.Face,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
try face.selectCharmap(.unicode);
|
||||
try setSize_(face, opts.size);
|
||||
|
||||
var hb_font = try harfbuzz.freetype.createFont(face.handle);
|
||||
errdefer hb_font.destroy();
|
||||
|
||||
const ft_mutex = try lib.alloc.create(std.Thread.Mutex);
|
||||
errdefer lib.alloc.destroy(ft_mutex);
|
||||
ft_mutex.* = .{};
|
||||
|
||||
var result: Face = .{
|
||||
.lib = lib.lib,
|
||||
.lib = lib,
|
||||
.face = face,
|
||||
.hb_font = hb_font,
|
||||
.ft_mutex = ft_mutex,
|
||||
.load_flags = opts.freetype_load_flags,
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
@@ -114,7 +144,13 @@ pub const Face = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Face) void {
|
||||
self.face.deinit();
|
||||
self.lib.alloc.destroy(self.ft_mutex);
|
||||
{
|
||||
self.lib.mutex.lock();
|
||||
defer self.lib.mutex.unlock();
|
||||
|
||||
self.face.deinit();
|
||||
}
|
||||
self.hb_font.destroy();
|
||||
self.* = undefined;
|
||||
}
|
||||
@@ -147,11 +183,7 @@ pub const Face = struct {
|
||||
self.face.ref();
|
||||
errdefer self.face.deinit();
|
||||
|
||||
var f = try initFace(
|
||||
.{ .lib = self.lib },
|
||||
self.face,
|
||||
opts,
|
||||
);
|
||||
var f = try initFace(self.lib, self.face, opts);
|
||||
errdefer f.deinit();
|
||||
f.synthetic = self.synthetic;
|
||||
f.synthetic.bold = true;
|
||||
@@ -166,11 +198,7 @@ pub const Face = struct {
|
||||
self.face.ref();
|
||||
errdefer self.face.deinit();
|
||||
|
||||
var f = try initFace(
|
||||
.{ .lib = self.lib },
|
||||
self.face,
|
||||
opts,
|
||||
);
|
||||
var f = try initFace(self.lib, self.face, opts);
|
||||
errdefer f.deinit();
|
||||
f.synthetic = self.synthetic;
|
||||
f.synthetic.italic = true;
|
||||
@@ -228,7 +256,7 @@ pub const Face = struct {
|
||||
// first thing we have to do is get all the vars and put them into
|
||||
// an array.
|
||||
const mm = try self.face.getMMVar();
|
||||
defer self.lib.doneMMVar(mm);
|
||||
defer self.lib.lib.doneMMVar(mm);
|
||||
|
||||
// To avoid allocations, we cap the number of variation axes we can
|
||||
// support. This is arbitrary but Firefox caps this at 16 so I
|
||||
@@ -270,6 +298,9 @@ pub const Face = struct {
|
||||
|
||||
/// Returns true if the given glyph ID is colorized.
|
||||
pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
|
||||
// Load the glyph and see what pixel mode it renders with.
|
||||
// All modes other than BGRA are non-color.
|
||||
// If the glyph fails to load, just return false.
|
||||
@@ -296,6 +327,9 @@ pub const Face = struct {
|
||||
glyph_index: u32,
|
||||
opts: font.face.RenderOptions,
|
||||
) !Glyph {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
|
||||
const metrics = opts.grid_metrics;
|
||||
|
||||
// If we have synthetic italic, then we apply a transformation matrix.
|
||||
@@ -357,7 +391,7 @@ pub const Face = struct {
|
||||
const format: ?font.Atlas.Format = switch (bitmap_ft.pixel_mode) {
|
||||
freetype.c.FT_PIXEL_MODE_MONO => null,
|
||||
freetype.c.FT_PIXEL_MODE_GRAY => .grayscale,
|
||||
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
|
||||
freetype.c.FT_PIXEL_MODE_BGRA => .bgra,
|
||||
else => {
|
||||
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
|
||||
@panic("unsupported pixel mode");
|
||||
@@ -741,6 +775,9 @@ pub const Face = struct {
|
||||
// If we fail to load any visible ASCII we just use max_advance from
|
||||
// the metrics provided by FreeType.
|
||||
const cell_width: f64 = cell_width: {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
|
||||
var max: f64 = 0.0;
|
||||
var c: u8 = ' ';
|
||||
while (c < 127) : (c += 1) {
|
||||
@@ -780,6 +817,8 @@ pub const Face = struct {
|
||||
|
||||
break :heights .{
|
||||
cap: {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
if (face.getCharIndex('H')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{
|
||||
.render = true,
|
||||
@@ -791,6 +830,8 @@ pub const Face = struct {
|
||||
break :cap null;
|
||||
},
|
||||
ex: {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{
|
||||
.render = true,
|
||||
@@ -832,7 +873,7 @@ test {
|
||||
const testFont = font.embedded.inconsolata;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
@@ -881,10 +922,10 @@ test "color emoji" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.emoji;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .rgba);
|
||||
var atlas = try font.Atlas.init(alloc, 512, .bgra);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(
|
||||
@@ -932,14 +973,14 @@ test "color emoji" {
|
||||
}
|
||||
}
|
||||
|
||||
test "mono to rgba" {
|
||||
test "mono to bgra" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.emoji;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .rgba);
|
||||
var atlas = try font.Atlas.init(alloc, 512, .bgra);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });
|
||||
@@ -958,7 +999,7 @@ test "svg font table" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.julia_mono;
|
||||
|
||||
var lib = try font.Library.init();
|
||||
var lib = try font.Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });
|
||||
@@ -995,7 +1036,7 @@ test "bitmap glyph" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.terminus_ttf;
|
||||
|
||||
var lib = try Library.init();
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
|
||||
@@ -30,7 +30,7 @@ fn genMap() Map {
|
||||
// Initialize to no converter
|
||||
var i: usize = 0;
|
||||
while (i < freetype.c.FT_PIXEL_MODE_MAX) : (i += 1) {
|
||||
result[i] = AtlasArray.initFill(null);
|
||||
result[i] = .initFill(null);
|
||||
}
|
||||
|
||||
// Map our converters
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! A library represents the shared state that the underlying font
|
||||
//! library implementation(s) require per-process.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
const freetype = @import("freetype");
|
||||
@@ -24,13 +26,26 @@ pub const Library = switch (options.backend) {
|
||||
pub const FreetypeLibrary = struct {
|
||||
lib: freetype.Library,
|
||||
|
||||
pub const InitError = freetype.Error;
|
||||
alloc: Allocator,
|
||||
|
||||
pub fn init() InitError!Library {
|
||||
return Library{ .lib = try freetype.Library.init() };
|
||||
/// Mutex to be held any time the library is
|
||||
/// being used to create or destroy a face.
|
||||
mutex: *std.Thread.Mutex,
|
||||
|
||||
pub const InitError = freetype.Error || Allocator.Error;
|
||||
|
||||
pub fn init(alloc: Allocator) InitError!Library {
|
||||
const lib = try freetype.Library.init();
|
||||
errdefer lib.deinit();
|
||||
|
||||
const mutex = try alloc.create(std.Thread.Mutex);
|
||||
mutex.* = .{};
|
||||
|
||||
return Library{ .lib = lib, .alloc = alloc, .mutex = mutex };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Library) void {
|
||||
self.alloc.destroy(self.mutex);
|
||||
self.lib.deinit();
|
||||
}
|
||||
};
|
||||
@@ -38,7 +53,8 @@ pub const FreetypeLibrary = struct {
|
||||
pub const NoopLibrary = struct {
|
||||
pub const InitError = error{};
|
||||
|
||||
pub fn init() InitError!Library {
|
||||
pub fn init(alloc: Allocator) InitError!Library {
|
||||
_ = alloc;
|
||||
return Library{};
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user