mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-27 11:06:31 +00:00
Merge branch 'ghostty-org:main' into feat-list-themes-write-config
This commit is contained in:
@@ -5,21 +5,16 @@ const App = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("build_config.zig");
|
||||
const apprt = @import("apprt.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const tracy = @import("tracy");
|
||||
const input = @import("input.zig");
|
||||
const configpkg = @import("config.zig");
|
||||
const Config = configpkg.Config;
|
||||
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
|
||||
const renderer = @import("renderer.zig");
|
||||
const font = @import("font/main.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const macos = @import("macos");
|
||||
const objc = @import("objc");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
|
||||
1070
src/Surface.zig
1070
src/Surface.zig
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,6 @@
|
||||
//! The goal is to have different implementations share as much of the core
|
||||
//! logic as possible, and to only reach out to platform-specific implementation
|
||||
//! code when absolutely necessary.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_config = @import("build_config.zig");
|
||||
|
||||
const structs = @import("apprt/structs.zig");
|
||||
@@ -28,6 +26,7 @@ pub const Target = action.Target;
|
||||
|
||||
pub const ContentScale = structs.ContentScale;
|
||||
pub const Clipboard = structs.Clipboard;
|
||||
pub const ClipboardContent = structs.ClipboardContent;
|
||||
pub const ClipboardRequest = structs.ClipboardRequest;
|
||||
pub const ClipboardRequestType = structs.ClipboardRequestType;
|
||||
pub const ColorScheme = structs.ColorScheme;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const input = @import("../input.zig");
|
||||
@@ -129,6 +129,9 @@ pub const Action = union(Key) {
|
||||
/// Jump to a specific split.
|
||||
goto_split: GotoSplit,
|
||||
|
||||
/// Jump to next/previous window.
|
||||
goto_window: GotoWindow,
|
||||
|
||||
/// Resize the split in the given direction.
|
||||
resize_split: ResizeSplit,
|
||||
|
||||
@@ -164,6 +167,9 @@ pub const Action = union(Key) {
|
||||
/// The cell size has changed to the given dimensions in pixels.
|
||||
cell_size: CellSize,
|
||||
|
||||
/// The scrollbar is updating.
|
||||
scrollbar: terminal.Scrollbar,
|
||||
|
||||
/// The target should be re-rendered. This usually has a specific
|
||||
/// surface target but if the app is targeted then all active
|
||||
/// surfaces should be redrawn.
|
||||
@@ -186,8 +192,9 @@ pub const Action = union(Key) {
|
||||
set_title: SetTitle,
|
||||
|
||||
/// Set the title of the target to a prompted value. It is up to
|
||||
/// the apprt to prompt.
|
||||
prompt_title,
|
||||
/// the apprt to prompt. The value specifies whether to prompt for the
|
||||
/// surface title or the tab title.
|
||||
prompt_title: PromptTitle,
|
||||
|
||||
/// The current working directory has changed for the target terminal.
|
||||
pwd: Pwd,
|
||||
@@ -298,6 +305,21 @@ pub const Action = union(Key) {
|
||||
/// A command has finished,
|
||||
command_finished: CommandFinished,
|
||||
|
||||
/// Start the search overlay with an optional initial needle.
|
||||
start_search: StartSearch,
|
||||
|
||||
/// End the search overlay, clearing the search state and hiding it.
|
||||
end_search,
|
||||
|
||||
/// The total number of matches found by the search.
|
||||
search_total: SearchTotal,
|
||||
|
||||
/// The currently selected search match index (1-based).
|
||||
search_selected: SearchSelected,
|
||||
|
||||
/// The readonly state of the surface has changed.
|
||||
readonly: Readonly,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
@@ -316,6 +338,7 @@ pub const Action = union(Key) {
|
||||
move_tab,
|
||||
goto_tab,
|
||||
goto_split,
|
||||
goto_window,
|
||||
resize_split,
|
||||
equalize_splits,
|
||||
toggle_split_zoom,
|
||||
@@ -324,6 +347,7 @@ pub const Action = union(Key) {
|
||||
reset_window_size,
|
||||
initial_size,
|
||||
cell_size,
|
||||
scrollbar,
|
||||
render,
|
||||
inspector,
|
||||
show_gtk_inspector,
|
||||
@@ -354,6 +378,11 @@ pub const Action = union(Key) {
|
||||
progress_report,
|
||||
show_on_screen_keyboard,
|
||||
command_finished,
|
||||
start_search,
|
||||
end_search,
|
||||
search_total,
|
||||
search_selected,
|
||||
readonly,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
@@ -449,6 +478,13 @@ pub const GotoSplit = enum(c_int) {
|
||||
right,
|
||||
};
|
||||
|
||||
// This is made extern (c_int) to make interop easier with our embedded
|
||||
// runtime. The small size cost doesn't make a difference in our union.
|
||||
pub const GotoWindow = enum(c_int) {
|
||||
previous,
|
||||
next,
|
||||
};
|
||||
|
||||
/// The amount to resize the split by and the direction to resize it in.
|
||||
pub const ResizeSplit = extern struct {
|
||||
amount: u16,
|
||||
@@ -511,11 +547,22 @@ pub const QuitTimer = enum(c_int) {
|
||||
stop,
|
||||
};
|
||||
|
||||
pub const Readonly = enum(c_int) {
|
||||
off,
|
||||
on,
|
||||
};
|
||||
|
||||
pub const MouseVisibility = enum(c_int) {
|
||||
visible,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// Whether to prompt for the surface title or tab title.
|
||||
pub const PromptTitle = enum(c_int) {
|
||||
surface,
|
||||
tab,
|
||||
};
|
||||
|
||||
pub const MouseOverLink = struct {
|
||||
url: [:0]const u8,
|
||||
|
||||
@@ -720,6 +767,9 @@ pub const OpenUrl = struct {
|
||||
/// should try to open the URL in a text editor or viewer or
|
||||
/// some equivalent, if possible.
|
||||
text,
|
||||
|
||||
/// The URL is known to contain HTML content.
|
||||
html,
|
||||
};
|
||||
|
||||
// Sync with: ghostty_action_open_url_s
|
||||
@@ -744,6 +794,8 @@ pub const CloseTabMode = enum(c_int) {
|
||||
this,
|
||||
/// Close all other tabs.
|
||||
other,
|
||||
/// Close all tabs to the right of the current tab.
|
||||
right,
|
||||
};
|
||||
|
||||
pub const CommandFinished = struct {
|
||||
@@ -763,3 +815,48 @@ pub const CommandFinished = struct {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const StartSearch = struct {
|
||||
needle: [:0]const u8,
|
||||
|
||||
// Sync with: ghostty_action_start_search_s
|
||||
pub const C = extern struct {
|
||||
needle: [*:0]const u8,
|
||||
};
|
||||
|
||||
pub fn cval(self: StartSearch) C {
|
||||
return .{
|
||||
.needle = self.needle.ptr,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const SearchTotal = struct {
|
||||
total: ?usize,
|
||||
|
||||
// Sync with: ghostty_action_search_total_s
|
||||
pub const C = extern struct {
|
||||
total: isize,
|
||||
};
|
||||
|
||||
pub fn cval(self: SearchTotal) C {
|
||||
return .{
|
||||
.total = if (self.total) |t| @intCast(t) else -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const SearchSelected = struct {
|
||||
selected: ?usize,
|
||||
|
||||
// Sync with: ghostty_action_search_selected_s
|
||||
pub const C = extern struct {
|
||||
selected: isize,
|
||||
};
|
||||
|
||||
pub fn cval(self: SearchSelected) C {
|
||||
return .{
|
||||
.selected = if (self.selected) |s| @intCast(s) else -1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const objc = @import("objc");
|
||||
const apprt = @import("../apprt.zig");
|
||||
@@ -66,7 +66,13 @@ pub const App = struct {
|
||||
) callconv(.c) void,
|
||||
|
||||
/// Write the clipboard value.
|
||||
write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.c) void,
|
||||
write_clipboard: *const fn (
|
||||
SurfaceUD,
|
||||
c_int,
|
||||
[*]const CAPI.ClipboardContent,
|
||||
usize,
|
||||
bool,
|
||||
) callconv(.c) void,
|
||||
|
||||
/// Close the current surface given by this function.
|
||||
close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null,
|
||||
@@ -699,16 +705,27 @@ pub const Surface = struct {
|
||||
alloc.destroy(state);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
pub fn setClipboard(
|
||||
self: *const Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const array = try alloc.alloc(CAPI.ClipboardContent, contents.len);
|
||||
defer alloc.free(array);
|
||||
for (contents, 0..) |content, i| {
|
||||
array[i] = .{
|
||||
.mime = content.mime,
|
||||
.data = content.data,
|
||||
};
|
||||
}
|
||||
|
||||
self.app.opts.write_clipboard(
|
||||
self.userdata,
|
||||
val.ptr,
|
||||
@intCast(@intFromEnum(clipboard_type)),
|
||||
array.ptr,
|
||||
array.len,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
@@ -1211,6 +1228,12 @@ pub const CAPI = struct {
|
||||
cell_height_px: u32,
|
||||
};
|
||||
|
||||
// ghostty_clipboard_content_s
|
||||
const ClipboardContent = extern struct {
|
||||
mime: [*:0]const u8,
|
||||
data: [*:0]const u8,
|
||||
};
|
||||
|
||||
// ghostty_text_s
|
||||
const Text = extern struct {
|
||||
tl_px_x: f64,
|
||||
@@ -1535,7 +1558,7 @@ pub const CAPI = struct {
|
||||
defer core_surface.renderer_state.mutex.unlock();
|
||||
|
||||
// If we don't have a selection, do nothing.
|
||||
const core_sel = core_surface.io.terminal.screen.selection orelse return false;
|
||||
const core_sel = core_surface.io.terminal.screens.active.selection orelse return false;
|
||||
|
||||
// Read the text from the selection.
|
||||
return readTextLocked(surface, core_sel, result);
|
||||
@@ -1555,7 +1578,7 @@ pub const CAPI = struct {
|
||||
defer surface.core_surface.renderer_state.mutex.unlock();
|
||||
|
||||
const core_sel = sel.core(
|
||||
&surface.core_surface.renderer_state.terminal.screen,
|
||||
surface.core_surface.renderer_state.terminal.screens.active,
|
||||
) orelse return false;
|
||||
|
||||
return readTextLocked(surface, core_sel, result);
|
||||
@@ -2114,7 +2137,7 @@ pub const CAPI = struct {
|
||||
|
||||
// Get our word selection
|
||||
const sel = sel: {
|
||||
const screen = &surface.renderer_state.terminal.screen;
|
||||
const screen: *terminal.Screen = surface.renderer_state.terminal.screens.active;
|
||||
const pos = try ptr.getCursorPos();
|
||||
const pt_viewport = surface.posToViewport(pos.x, pos.y);
|
||||
const pin = screen.pages.pin(.{
|
||||
@@ -2126,7 +2149,7 @@ pub const CAPI = struct {
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return false;
|
||||
};
|
||||
break :sel surface.io.terminal.screen.selectWord(pin) orelse return false;
|
||||
break :sel surface.io.terminal.screens.active.selectWord(pin) orelse return false;
|
||||
};
|
||||
|
||||
// Read the selection
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
// The required comptime API for any apprt.
|
||||
pub const App = @import("gtk/App.zig");
|
||||
pub const Surface = @import("gtk/Surface.zig");
|
||||
|
||||
@@ -5,18 +5,13 @@ const App = @This();
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
const CoreApp = @import("../../App.zig");
|
||||
|
||||
const Application = @import("class/application.zig").Application;
|
||||
const Surface = @import("Surface.zig");
|
||||
const gtk_version = @import("gtk_version.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@@ -80,15 +80,15 @@ pub fn clipboardRequest(
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
pub fn setClipboard(
|
||||
self: *Self,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) !void {
|
||||
self.surface.setClipboardString(
|
||||
val,
|
||||
self.surface.setClipboard(
|
||||
clipboard_type,
|
||||
contents,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,20 @@ pub const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
pub const blueprint_compiler_help =
|
||||
\\
|
||||
\\When building from a Git checkout, Ghostty requires
|
||||
\\version {f} or newer of `blueprint-compiler` as a
|
||||
\\build-time dependency. Please install it, ensure that it
|
||||
\\is available on your PATH, and then retry building Ghostty.
|
||||
\\See `HACKING.md` for more details.
|
||||
\\
|
||||
\\This message should *not* appear for normal users, who
|
||||
\\should build Ghostty from official release tarballs instead.
|
||||
\\Please consult https://ghostty.org/docs/install/build for
|
||||
\\more information on the recommended build instructions.
|
||||
;
|
||||
|
||||
const adwaita_version = std.SemanticVersion{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
@@ -79,13 +93,9 @@ pub fn main() !void {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {f} 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});
|
||||
++ blueprint_compiler_help,
|
||||
.{required_blueprint_version},
|
||||
);
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
@@ -103,13 +113,9 @@ pub fn main() !void {
|
||||
if (version.order(required_blueprint_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` is the wrong version.
|
||||
\\
|
||||
\\Ghostty requires version {f} 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});
|
||||
++ blueprint_compiler_help,
|
||||
.{required_blueprint_version},
|
||||
);
|
||||
std.posix.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -144,13 +150,9 @@ pub fn main() !void {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {f} 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});
|
||||
++ blueprint_compiler_help,
|
||||
.{required_blueprint_version},
|
||||
);
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
|
||||
@@ -43,9 +43,11 @@ pub const blueprints: []const Blueprint = &.{
|
||||
.{ .major = 1, .minor = 5, .name = "inspector-widget" },
|
||||
.{ .major = 1, .minor = 5, .name = "inspector-window" },
|
||||
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
|
||||
.{ .major = 1, .minor = 2, .name = "search-overlay" },
|
||||
.{ .major = 1, .minor = 5, .name = "split-tree" },
|
||||
.{ .major = 1, .minor = 5, .name = "split-tree-split" },
|
||||
.{ .major = 1, .minor = 2, .name = "surface" },
|
||||
.{ .major = 1, .minor = 5, .name = "surface-scrolled-window" },
|
||||
.{ .major = 1, .minor = 5, .name = "surface-title-dialog" },
|
||||
.{ .major = 1, .minor = 3, .name = "surface-child-exited" },
|
||||
.{ .major = 1, .minor = 5, .name = "tab" },
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/// Contains all the logic for putting the Ghostty process and
|
||||
/// each individual surface into its own cgroup.
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_systemd_cgroup);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gio = @import("gio");
|
||||
@@ -10,6 +9,7 @@ const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const state = &@import("../../../global.zig").state;
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const cgroup = @import("../cgroup.zig");
|
||||
@@ -394,6 +394,14 @@ pub const Application = extern struct {
|
||||
.{ .detail = "config" },
|
||||
);
|
||||
|
||||
_ = gtk.CssProvider.signals.parsing_error.connect(
|
||||
css_provider,
|
||||
*Self,
|
||||
signalCssParsingError,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Trigger initial config changes
|
||||
self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
|
||||
|
||||
@@ -530,12 +538,16 @@ pub const Application = extern struct {
|
||||
}
|
||||
|
||||
// If we have no windows attached to our app, also quit.
|
||||
if (priv.requested_window and @as(
|
||||
?*glib.List,
|
||||
self.as(gtk.Application).getWindows(),
|
||||
) == null) {
|
||||
log.debug("must_quit due to no app windows", .{});
|
||||
break :q true;
|
||||
// We only do this if we don't have the closed delay set,
|
||||
// because with the closed delay set we'll exit eventually.
|
||||
if (config.@"quit-after-last-window-closed-delay" == null) {
|
||||
if (priv.requested_window and @as(
|
||||
?*glib.List,
|
||||
self.as(gtk.Application).getWindows(),
|
||||
) == null) {
|
||||
log.debug("must_quit due to no app windows", .{});
|
||||
break :q true;
|
||||
}
|
||||
}
|
||||
|
||||
// No quit conditions met
|
||||
@@ -649,6 +661,8 @@ pub const Application = extern struct {
|
||||
|
||||
.goto_split => return Action.gotoSplit(target, value),
|
||||
|
||||
.goto_window => return Action.gotoWindow(value),
|
||||
|
||||
.goto_tab => return Action.gotoTab(target, value),
|
||||
|
||||
.initial_size => return Action.initialSize(target, value),
|
||||
@@ -683,7 +697,7 @@ pub const Application = extern struct {
|
||||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.prompt_title => return Action.promptTitle(target),
|
||||
.prompt_title => return Action.promptTitle(target, value),
|
||||
|
||||
.quit => self.quit(),
|
||||
|
||||
@@ -697,6 +711,8 @@ pub const Application = extern struct {
|
||||
|
||||
.ring_bell => Action.ringBell(target),
|
||||
|
||||
.scrollbar => Action.scrollbar(target, value),
|
||||
|
||||
.set_title => Action.setTitle(target, value),
|
||||
|
||||
.show_child_exited => return Action.showChildExited(target, value),
|
||||
@@ -715,6 +731,11 @@ pub const Application = extern struct {
|
||||
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
|
||||
.command_finished => return Action.commandFinished(target, value),
|
||||
|
||||
.start_search => Action.startSearch(target),
|
||||
.end_search => Action.endSearch(target),
|
||||
.search_total => Action.searchTotal(target, value),
|
||||
.search_selected => Action.searchSelected(target, value),
|
||||
|
||||
// Unimplemented
|
||||
.secure_input,
|
||||
.close_all_windows,
|
||||
@@ -729,6 +750,7 @@ pub const Application = extern struct {
|
||||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.readonly,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
@@ -806,19 +828,19 @@ pub const Application = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn loadRuntimeCss(self: *Self) Allocator.Error!void {
|
||||
fn loadRuntimeCss(self: *Self) (Allocator.Error || std.Io.Writer.Error)!void {
|
||||
const alloc = self.allocator();
|
||||
const priv: *Private = self.private();
|
||||
const config = priv.config.get();
|
||||
|
||||
const config = self.private().config.get();
|
||||
var buf: std.Io.Writer.Allocating = try .initCapacity(alloc, 2048);
|
||||
defer buf.deinit();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
const writer = buf.writer(alloc);
|
||||
const writer = &buf.writer;
|
||||
|
||||
// Load standard css first as it can override some of the user configured styling.
|
||||
try loadRuntimeCss414(config, &writer);
|
||||
try loadRuntimeCss416(config, &writer);
|
||||
try loadRuntimeCss414(config, writer);
|
||||
try loadRuntimeCss416(config, writer);
|
||||
|
||||
const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
|
||||
|
||||
@@ -858,25 +880,22 @@ pub const Application = extern struct {
|
||||
, .{ .font_family = font_family });
|
||||
}
|
||||
|
||||
// ensure that we have a sentinel
|
||||
try writer.writeByte(0);
|
||||
const contents = buf.written();
|
||||
|
||||
const data = buf.items[0 .. buf.items.len - 1 :0];
|
||||
log.debug("runtime CSS is {d} bytes", .{contents.len});
|
||||
|
||||
log.debug("runtime CSS is {d} bytes", .{data.len + 1});
|
||||
const bytes = glib.Bytes.new(contents.ptr, contents.len);
|
||||
defer bytes.unref();
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
loadCssProviderFromData(
|
||||
self.private().css_provider,
|
||||
data,
|
||||
);
|
||||
priv.css_provider.loadFromBytes(bytes);
|
||||
}
|
||||
|
||||
/// Load runtime CSS for older than GTK 4.16
|
||||
fn loadRuntimeCss414(
|
||||
config: *const CoreConfig,
|
||||
writer: *const std.ArrayListUnmanaged(u8).Writer,
|
||||
) Allocator.Error!void {
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
if (gtk_version.runtimeAtLeast(4, 16, 0)) return;
|
||||
|
||||
const window_theme = config.@"window-theme";
|
||||
@@ -911,8 +930,8 @@ pub const Application = extern struct {
|
||||
/// Load runtime for GTK 4.16 and newer
|
||||
fn loadRuntimeCss416(
|
||||
config: *const CoreConfig,
|
||||
writer: *const std.ArrayListUnmanaged(u8).Writer,
|
||||
) Allocator.Error!void {
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
if (gtk_version.runtimeUntil(4, 16, 0)) return;
|
||||
|
||||
const window_theme = config.@"window-theme";
|
||||
@@ -1008,8 +1027,8 @@ pub const Application = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn loadCustomCss(self: *Self) !void {
|
||||
const priv = self.private();
|
||||
fn loadCustomCss(self: *Self) (std.fs.File.ReadError || Allocator.Error)!void {
|
||||
const priv: *Private = self.private();
|
||||
const alloc = self.allocator();
|
||||
const display = gdk.Display.getDefault() orelse {
|
||||
log.warn("unable to get display", .{});
|
||||
@@ -1026,7 +1045,7 @@ pub const Application = extern struct {
|
||||
}
|
||||
priv.custom_css_providers.clearRetainingCapacity();
|
||||
|
||||
const config = priv.config.getMut();
|
||||
const config = priv.config.get();
|
||||
for (config.@"gtk-custom-css".value.items) |p| {
|
||||
const path, const optional = switch (p) {
|
||||
.optional => |path| .{ path, true },
|
||||
@@ -1043,25 +1062,42 @@ pub const Application = extern struct {
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const css_file_size_limit = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
log.info("loading gtk-custom-css path={s}", .{path});
|
||||
var buf: [4096]u8 = undefined;
|
||||
var reader = file.reader(&buf);
|
||||
const contents = try reader.interface.readAlloc(
|
||||
const contents = file.readToEndAlloc(
|
||||
alloc,
|
||||
5 * 1024 * 1024, // 5MB,
|
||||
);
|
||||
css_file_size_limit,
|
||||
) catch |err| switch (err) {
|
||||
error.FileTooBig => {
|
||||
log.warn("gtk-custom-css file {s} was larger than {Bi}", .{ path, css_file_size_limit });
|
||||
continue;
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
defer alloc.free(contents);
|
||||
|
||||
const data = try alloc.dupeZ(u8, contents);
|
||||
defer alloc.free(data);
|
||||
const bytes = glib.Bytes.new(contents.ptr, contents.len);
|
||||
defer bytes.unref();
|
||||
|
||||
const css_provider = gtk.CssProvider.new();
|
||||
errdefer css_provider.unref();
|
||||
|
||||
_ = gtk.CssProvider.signals.parsing_error.connect(
|
||||
css_provider,
|
||||
*Self,
|
||||
signalCssParsingError,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
try priv.custom_css_providers.append(alloc, css_provider);
|
||||
|
||||
css_provider.loadFromBytes(bytes);
|
||||
|
||||
const provider = gtk.CssProvider.new();
|
||||
errdefer provider.unref();
|
||||
try priv.custom_css_providers.append(alloc, provider);
|
||||
loadCssProviderFromData(provider, data);
|
||||
gtk.StyleContext.addProviderForDisplay(
|
||||
display,
|
||||
provider.as(gtk.StyleProvider),
|
||||
css_provider.as(gtk.StyleProvider),
|
||||
gtk.STYLE_PROVIDER_PRIORITY_USER,
|
||||
);
|
||||
}
|
||||
@@ -1083,7 +1119,7 @@ pub const Application = extern struct {
|
||||
self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
|
||||
self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
|
||||
self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
|
||||
self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
|
||||
self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = .mixed });
|
||||
self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
|
||||
self.syncActionAccelerator("win.reset", .{ .reset = {} });
|
||||
self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
|
||||
@@ -1164,7 +1200,7 @@ pub const Application = extern struct {
|
||||
// just stuck with the old CSS but we don't want to fail the entire
|
||||
// config change operation.
|
||||
self.loadRuntimeCss() catch |err| switch (err) {
|
||||
error.OutOfMemory => log.warn(
|
||||
error.WriteFailed, error.OutOfMemory => log.warn(
|
||||
"out of memory loading runtime CSS, no runtime CSS applied",
|
||||
.{},
|
||||
),
|
||||
@@ -1177,6 +1213,37 @@ pub const Application = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Log CSS parsing error
|
||||
fn signalCssParsingError(
|
||||
_: *gtk.CssProvider,
|
||||
css_section: *gtk.CssSection,
|
||||
err: *glib.Error,
|
||||
_: *Self,
|
||||
) callconv(.c) void {
|
||||
const location = css_section.toString();
|
||||
defer glib.free(location);
|
||||
if (comptime gtk_version.atLeast(4, 16, 0)) bytes: {
|
||||
const bytes = css_section.getBytes() orelse break :bytes;
|
||||
var len: usize = undefined;
|
||||
const ptr = bytes.getData(&len) orelse break :bytes;
|
||||
const data = ptr[0..len];
|
||||
log.warn("css parsing failed at {s}: {s} {d} {s}\n{s}", .{
|
||||
location,
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "«unknown»",
|
||||
data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
log.warn("css parsing failed at {s}: {s} {d} {s}", .{
|
||||
location,
|
||||
glib.quarkToString(err.f_domain),
|
||||
err.f_code,
|
||||
err.f_message orelse "«unknown»",
|
||||
});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
@@ -1521,7 +1588,7 @@ pub const Application = extern struct {
|
||||
.dark;
|
||||
log.debug("style manager changed scheme={}", .{scheme});
|
||||
|
||||
const priv = self.private();
|
||||
const priv: *Private = self.private();
|
||||
const core_app = priv.core_app;
|
||||
core_app.colorSchemeEvent(self.rt(), scheme) catch |err| {
|
||||
log.warn("error updating app color scheme err={}", .{err});
|
||||
@@ -1534,6 +1601,26 @@ pub const Application = extern struct {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (gtk_version.atLeast(4, 20, 0)) {
|
||||
const gtk_scheme: gtk.InterfaceColorScheme = switch (scheme) {
|
||||
.light => gtk.InterfaceColorScheme.light,
|
||||
.dark => gtk.InterfaceColorScheme.dark,
|
||||
};
|
||||
var value = gobject.ext.Value.newFrom(gtk_scheme);
|
||||
gobject.Object.setProperty(
|
||||
priv.css_provider.as(gobject.Object),
|
||||
"prefers-color-scheme",
|
||||
&value,
|
||||
);
|
||||
for (priv.custom_css_providers.items) |css_provider| {
|
||||
gobject.Object.setProperty(
|
||||
css_provider.as(gobject.Object),
|
||||
"prefers-color-scheme",
|
||||
&value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handleReloadConfig(
|
||||
@@ -1931,6 +2018,69 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gotoWindow(direction: apprt.action.GotoWindow) bool {
|
||||
const glist = gtk.Window.listToplevels();
|
||||
defer glist.free();
|
||||
|
||||
// The window we're starting from is typically our active window.
|
||||
const starting: *glib.List = @as(?*glib.List, glist.findCustom(
|
||||
null,
|
||||
findActiveWindow,
|
||||
)) orelse glist;
|
||||
|
||||
// Go forward or backwards in the list until we find a valid
|
||||
// window that is visible.
|
||||
var current_: ?*glib.List = starting;
|
||||
while (current_) |node| : (current_ = switch (direction) {
|
||||
.next => node.f_next,
|
||||
.previous => node.f_prev,
|
||||
}) {
|
||||
const data = node.f_data orelse continue;
|
||||
const gtk_window: *gtk.Window = @ptrCast(@alignCast(data));
|
||||
if (gotoWindowMaybe(gtk_window)) return true;
|
||||
}
|
||||
|
||||
// If we reached here, we didn't find a valid window to focus.
|
||||
// Wrap around.
|
||||
current_ = switch (direction) {
|
||||
.next => glist,
|
||||
.previous => last: {
|
||||
var end: *glib.List = glist;
|
||||
while (end.f_next) |next| end = next;
|
||||
break :last end;
|
||||
},
|
||||
};
|
||||
while (current_) |node| : (current_ = switch (direction) {
|
||||
.next => node.f_next,
|
||||
.previous => node.f_prev,
|
||||
}) {
|
||||
if (current_ == starting) break;
|
||||
const data = node.f_data orelse continue;
|
||||
const gtk_window: *gtk.Window = @ptrCast(@alignCast(data));
|
||||
if (gotoWindowMaybe(gtk_window)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn gotoWindowMaybe(gtk_window: *gtk.Window) bool {
|
||||
// If it is already active skip it.
|
||||
if (gtk_window.isActive() != 0) return false;
|
||||
// If it is hidden, skip it.
|
||||
if (gtk_window.as(gtk.Widget).isVisible() == 0) return false;
|
||||
// If it isn't a Ghostty window, skip it.
|
||||
const window = gobject.ext.cast(
|
||||
Window,
|
||||
gtk_window,
|
||||
) orelse return false;
|
||||
|
||||
// Focus our active surface
|
||||
const surface = window.getActiveSurface() orelse return false;
|
||||
gtk.Window.present(gtk_window);
|
||||
surface.grabFocus();
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initialSize(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.InitialSize,
|
||||
@@ -2168,12 +2318,18 @@ const Action = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn promptTitle(target: apprt.Target) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
pub fn promptTitle(target: apprt.Target, value: apprt.action.PromptTitle) bool {
|
||||
switch (value) {
|
||||
.surface => switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
.tab => {
|
||||
// GTK does not yet support tab title prompting
|
||||
return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2270,6 +2426,44 @@ const Action = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scrollbar(
|
||||
target: apprt.Target,
|
||||
value: apprt.Action.Value(.scrollbar),
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.setScrollbar(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn startSearch(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.setSearchActive(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endSearch(target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.setSearchActive(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn searchTotal(target: apprt.Target, value: apprt.action.SearchTotal) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.setSearchTotal(value.total),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn searchSelected(target: apprt.Target, value: apprt.action.SearchSelected) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.setSearchSelected(value.selected),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.SetTitle,
|
||||
@@ -2485,7 +2679,9 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
|
||||
/// disable it.
|
||||
@"vulkan-disable": bool = false,
|
||||
} = .{
|
||||
.opengl = config.@"gtk-opengl-debug",
|
||||
// `gtk-opengl-debug` dumps logs directly to stderr so both must be true
|
||||
// to enable OpenGL debugging.
|
||||
.opengl = state.logging.stderr and config.@"gtk-opengl-debug",
|
||||
};
|
||||
|
||||
var gdk_disable: struct {
|
||||
@@ -2580,8 +2776,3 @@ fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c)
|
||||
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||
return if (window.isActive() != 0) 0 else -1;
|
||||
}
|
||||
|
||||
fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void {
|
||||
assert(gtk_version.runtimeAtLeast(4, 12, 0));
|
||||
provider.loadFromString(data);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
@@ -3,10 +3,8 @@ const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_dialog);
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Binding = @import("../../../input.zig").Binding;
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const key = @import("../key.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
|
||||
const cimgui = @import("cimgui");
|
||||
const gl = @import("opengl");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
|
||||
@@ -2,15 +2,12 @@ const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
|
||||
const key = @import("../key.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
const DebugWarning = @import("debug_warning.zig").DebugWarning;
|
||||
const InspectorWidget = @import("inspector_widget.zig").InspectorWidget;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
486
src/apprt/gtk/class/search_overlay.zig
Normal file
486
src/apprt/gtk/class/search_overlay.zig
Normal file
@@ -0,0 +1,486 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_search_overlay);
|
||||
|
||||
/// The overlay that shows the current size while a surface is resizing.
|
||||
/// This can be used generically to show pretty much anything with a
|
||||
/// disappearing overlay, but we have no other use at this point so it
|
||||
/// is named specifically for what it does.
|
||||
///
|
||||
/// General usage:
|
||||
///
|
||||
/// 1. Add it to an overlay
|
||||
/// 2. Set the label with `setLabel`
|
||||
/// 3. Schedule to show it with `schedule`
|
||||
///
|
||||
/// Set any properties to change the behavior.
|
||||
pub const SearchOverlay = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = adw.Bin;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttySearchOverlay",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const active = struct {
|
||||
pub const name = "active";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.getter = getSearchActive,
|
||||
.setter = setSearchActive,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"search-total" = struct {
|
||||
pub const name = "search-total";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
u64,
|
||||
.{
|
||||
.default = 0,
|
||||
.minimum = 0,
|
||||
.maximum = std.math.maxInt(u64),
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
u64,
|
||||
.{ .getter = getSearchTotal },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"has-search-total" = struct {
|
||||
pub const name = "has-search-total";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{ .getter = getHasSearchTotal },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"search-selected" = struct {
|
||||
pub const name = "search-selected";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
u64,
|
||||
.{
|
||||
.default = 0,
|
||||
.minimum = 0,
|
||||
.maximum = std.math.maxInt(u64),
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
u64,
|
||||
.{ .getter = getSearchSelected },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"has-search-selected" = struct {
|
||||
pub const name = "has-search-selected";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{ .getter = getHasSearchSelected },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"halign-target" = struct {
|
||||
pub const name = "halign-target";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
gtk.Align,
|
||||
.{
|
||||
.default = .end,
|
||||
.accessor = C.privateShallowFieldAccessor("halign_target"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"valign-target" = struct {
|
||||
pub const name = "valign-target";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
gtk.Align,
|
||||
.{
|
||||
.default = .start,
|
||||
.accessor = C.privateShallowFieldAccessor("valign_target"),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
/// Emitted when the search is stopped (e.g., Escape pressed).
|
||||
pub const @"stop-search" = struct {
|
||||
pub const name = "stop-search";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when the search text changes (debounced).
|
||||
pub const @"search-changed" = struct {
|
||||
pub const name = "search-changed";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{?[*:0]const u8},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when navigating to the next match.
|
||||
pub const @"next-match" = struct {
|
||||
pub const name = "next-match";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when navigating to the previous match.
|
||||
pub const @"previous-match" = struct {
|
||||
pub const name = "previous-match";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The search entry widget.
|
||||
search_entry: *gtk.SearchEntry,
|
||||
|
||||
/// True when a search is active, meaning we should show the overlay.
|
||||
active: bool = false,
|
||||
|
||||
/// Total number of search matches (null means unknown/none).
|
||||
search_total: ?usize = null,
|
||||
|
||||
/// Currently selected match index (null means none selected).
|
||||
search_selected: ?usize = null,
|
||||
|
||||
/// Target horizontal alignment for the overlay.
|
||||
halign_target: gtk.Align = .end,
|
||||
|
||||
/// Target vertical alignment for the overlay.
|
||||
valign_target: gtk.Align = .start,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
/// Grab focus on the search entry and select all text.
|
||||
pub fn grabFocus(self: *Self) void {
|
||||
const priv = self.private();
|
||||
_ = priv.search_entry.as(gtk.Widget).grabFocus();
|
||||
|
||||
// Select all text in the search entry field. -1 is distance from
|
||||
// the end, causing the entire text to be selected.
|
||||
priv.search_entry.as(gtk.Editable).selectRegion(0, -1);
|
||||
}
|
||||
|
||||
// Set active status, and update search on activation
|
||||
fn setSearchActive(self: *Self, active: bool) void {
|
||||
const priv = self.private();
|
||||
if (!priv.active and active) {
|
||||
const text = priv.search_entry.as(gtk.Editable).getText();
|
||||
signals.@"search-changed".impl.emit(self, null, .{text}, null);
|
||||
}
|
||||
priv.active = active;
|
||||
}
|
||||
|
||||
/// Set the total number of search matches.
|
||||
pub fn setSearchTotal(self: *Self, total: ?usize) void {
|
||||
const priv = self.private();
|
||||
const had_total = priv.search_total != null;
|
||||
if (priv.search_total == total) return;
|
||||
priv.search_total = total;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"search-total".impl.param_spec);
|
||||
if (had_total != (total != null)) {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"has-search-total".impl.param_spec);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the currently selected match index.
|
||||
pub fn setSearchSelected(self: *Self, selected: ?usize) void {
|
||||
const priv = self.private();
|
||||
const had_selected = priv.search_selected != null;
|
||||
if (priv.search_selected == selected) return;
|
||||
priv.search_selected = selected;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"search-selected".impl.param_spec);
|
||||
if (had_selected != (selected != null)) {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"has-search-selected".impl.param_spec);
|
||||
}
|
||||
}
|
||||
|
||||
fn getSearchActive(self: *Self) bool {
|
||||
return self.private().active;
|
||||
}
|
||||
|
||||
fn getSearchTotal(self: *Self) u64 {
|
||||
return self.private().search_total orelse 0;
|
||||
}
|
||||
|
||||
fn getHasSearchTotal(self: *Self) bool {
|
||||
return self.private().search_total != null;
|
||||
}
|
||||
|
||||
fn getSearchSelected(self: *Self) u64 {
|
||||
return self.private().search_selected orelse 0;
|
||||
}
|
||||
|
||||
fn getHasSearchSelected(self: *Self) bool {
|
||||
return self.private().search_selected != null;
|
||||
}
|
||||
|
||||
fn closureMatchLabel(
|
||||
_: *Self,
|
||||
has_selected: bool,
|
||||
selected: u64,
|
||||
has_total: bool,
|
||||
total: u64,
|
||||
) callconv(.c) ?[*:0]const u8 {
|
||||
if (!has_total or total == 0) return glib.ext.dupeZ(u8, "0/0");
|
||||
var buf: [32]u8 = undefined;
|
||||
const label = std.fmt.bufPrintZ(&buf, "{}/{}", .{
|
||||
if (has_selected) selected + 1 else 0,
|
||||
total,
|
||||
}) catch return null;
|
||||
return glib.ext.dupeZ(u8, label);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Template callbacks
|
||||
|
||||
fn searchChanged(entry: *gtk.SearchEntry, self: *Self) callconv(.c) void {
|
||||
const text = entry.as(gtk.Editable).getText();
|
||||
signals.@"search-changed".impl.emit(self, null, .{text}, null);
|
||||
}
|
||||
|
||||
// NOTE: The callbacks below use anyopaque for the first parameter
|
||||
// because they're shared with multiple widgets in the template.
|
||||
|
||||
fn stopSearch(_: *anyopaque, self: *Self) callconv(.c) void {
|
||||
signals.@"stop-search".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
fn nextMatch(_: *anyopaque, self: *Self) callconv(.c) void {
|
||||
signals.@"next-match".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
fn previousMatch(_: *anyopaque, self: *Self) callconv(.c) void {
|
||||
signals.@"previous-match".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
fn searchEntryKeyPressed(
|
||||
_: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
_: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
if (keyval == gdk.KEY_Return or keyval == gdk.KEY_KP_Enter) {
|
||||
if (gtk_mods.shift_mask) {
|
||||
signals.@"previous-match".impl.emit(self, null, .{}, null);
|
||||
} else {
|
||||
signals.@"next-match".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onDragEnd(
|
||||
_: *gtk.GestureDrag,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// On drag end, we want to move our halign/valign if we crossed
|
||||
// the midpoint on either axis. This lets the search overlay be
|
||||
// moved to different corners of the parent container.
|
||||
|
||||
const priv = self.private();
|
||||
const widget = self.as(gtk.Widget);
|
||||
const parent = widget.getParent() orelse return;
|
||||
|
||||
const parent_width: f64 = @floatFromInt(parent.getAllocatedWidth());
|
||||
const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight());
|
||||
const self_width: f64 = @floatFromInt(widget.getAllocatedWidth());
|
||||
const self_height: f64 = @floatFromInt(widget.getAllocatedHeight());
|
||||
|
||||
const self_x: f64 = if (priv.halign_target == .start) 0 else parent_width - self_width;
|
||||
const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height;
|
||||
|
||||
const new_x = self_x + offset_x + (self_width / 2);
|
||||
const new_y = self_y + offset_y + (self_height / 2);
|
||||
|
||||
const new_halign: gtk.Align = if (new_x > parent_width / 2) .end else .start;
|
||||
const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start;
|
||||
|
||||
var changed = false;
|
||||
if (new_halign != priv.halign_target) {
|
||||
priv.halign_target = new_halign;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"halign-target".impl.param_spec);
|
||||
changed = true;
|
||||
}
|
||||
if (new_valign != priv.valign_target) {
|
||||
priv.valign_target = new_valign;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) self.as(gtk.Widget).queueResize();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
fn dispose(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
_ = priv;
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
_ = priv;
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 2,
|
||||
.name = "search-overlay",
|
||||
}),
|
||||
);
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("search_entry", .{});
|
||||
|
||||
// Template Callbacks
|
||||
class.bindTemplateCallback("stop_search", &stopSearch);
|
||||
class.bindTemplateCallback("search_changed", &searchChanged);
|
||||
class.bindTemplateCallback("match_label_closure", &closureMatchLabel);
|
||||
class.bindTemplateCallback("next_match", &nextMatch);
|
||||
class.bindTemplateCallback("previous_match", &previousMatch);
|
||||
class.bindTemplateCallback("search_entry_key_pressed", &searchEntryKeyPressed);
|
||||
class.bindTemplateCallback("on_drag_end", &onDragEnd);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.active.impl,
|
||||
properties.@"search-total".impl,
|
||||
properties.@"has-search-total".impl,
|
||||
properties.@"search-selected".impl,
|
||||
properties.@"has-search-selected".impl,
|
||||
properties.@"halign-target".impl,
|
||||
properties.@"valign-target".impl,
|
||||
});
|
||||
|
||||
// Signals
|
||||
signals.@"stop-search".impl.register(.{});
|
||||
signals.@"search-changed".impl.register(.{});
|
||||
signals.@"next-match".impl.register(.{});
|
||||
signals.@"previous-match".impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
@@ -8,20 +7,15 @@ const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Application = @import("application.zig").Application;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
const SurfaceScrolledWindow = @import("surface_scrolled_window.zig").SurfaceScrolledWindow;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_split_tree);
|
||||
|
||||
@@ -874,7 +868,9 @@ pub const SplitTree = extern struct {
|
||||
current: Surface.Tree.Node.Handle,
|
||||
) *gtk.Widget {
|
||||
return switch (tree.nodes[current.idx()]) {
|
||||
.leaf => |v| v.as(gtk.Widget),
|
||||
.leaf => |v| gobject.ext.newInstance(SurfaceScrolledWindow, .{
|
||||
.surface = v,
|
||||
}).as(gtk.Widget),
|
||||
.split => |s| SplitTreeSplit.new(
|
||||
current,
|
||||
&s,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
@@ -19,18 +19,17 @@ const terminal = @import("../../../terminal/main.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gtk_key = @import("../key.zig");
|
||||
const ApprtSurface = @import("../Surface.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
const Config = @import("config.zig").Config;
|
||||
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
|
||||
const SearchOverlay = @import("search_overlay.zig").SearchOverlay;
|
||||
const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
|
||||
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
|
||||
const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog;
|
||||
const Window = @import("window.zig").Window;
|
||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
||||
const i18n = @import("../../../os/i18n.zig");
|
||||
|
||||
@@ -40,12 +39,16 @@ pub const Surface = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = adw.Bin;
|
||||
pub const Implements = [_]type{gtk.Scrollable};
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttySurface",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
.implements = &.{
|
||||
gobject.ext.implement(gtk.Scrollable, .{}),
|
||||
},
|
||||
});
|
||||
|
||||
/// A SplitTree implementation that stores surfaces.
|
||||
@@ -301,6 +304,62 @@ pub const Surface = extern struct {
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const hadjustment = struct {
|
||||
pub const name = "hadjustment";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*gtk.Adjustment,
|
||||
.{
|
||||
.accessor = .{
|
||||
.getter = getHAdjustmentValue,
|
||||
.setter = setHAdjustmentValue,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const vadjustment = struct {
|
||||
pub const name = "vadjustment";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*gtk.Adjustment,
|
||||
.{
|
||||
.accessor = .{
|
||||
.getter = getVAdjustmentValue,
|
||||
.setter = setVAdjustmentValue,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"hscroll-policy" = struct {
|
||||
pub const name = "hscroll-policy";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
gtk.ScrollablePolicy,
|
||||
.{
|
||||
.default = .natural,
|
||||
.accessor = C.privateShallowFieldAccessor("hscroll_policy"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"vscroll-policy" = struct {
|
||||
pub const name = "vscroll-policy";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
gtk.ScrollablePolicy,
|
||||
.{
|
||||
.default = .natural,
|
||||
.accessor = C.privateShallowFieldAccessor("vscroll_policy"),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
@@ -491,6 +550,9 @@ pub const Surface = extern struct {
|
||||
/// The resize overlay
|
||||
resize_overlay: *ResizeOverlay,
|
||||
|
||||
/// The search overlay
|
||||
search_overlay: *SearchOverlay,
|
||||
|
||||
/// The apprt Surface.
|
||||
rt_surface: ApprtSurface = undefined,
|
||||
|
||||
@@ -548,6 +610,13 @@ pub const Surface = extern struct {
|
||||
|
||||
action_group: ?*gio.SimpleActionGroup = null,
|
||||
|
||||
// Gtk.Scrollable interface adjustments
|
||||
hadj: ?*gtk.Adjustment = null,
|
||||
vadj: ?*gtk.Adjustment = null,
|
||||
hscroll_policy: gtk.ScrollablePolicy = .natural,
|
||||
vscroll_policy: gtk.ScrollablePolicy = .natural,
|
||||
vadj_signal_group: ?*gobject.SignalGroup = null,
|
||||
|
||||
// Template binds
|
||||
child_exited_overlay: *ChildExited,
|
||||
context_menu: *gtk.PopoverMenu,
|
||||
@@ -714,6 +783,47 @@ pub const Surface = extern struct {
|
||||
return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0;
|
||||
}
|
||||
|
||||
/// Set the scrollbar state for this surface. This will setup the
|
||||
/// properties for our Gtk.Scrollable interface properly.
|
||||
pub fn setScrollbar(self: *Self, scrollbar: terminal.Scrollbar) void {
|
||||
// Update existing adjustment in-place. If we don't have an
|
||||
// adjustment then we do nothing because we're not part of a
|
||||
// scrolled window.
|
||||
const vadj = self.getVAdjustment() orelse return;
|
||||
|
||||
// Check if values match existing adjustment and skip update if so
|
||||
const value: f64 = @floatFromInt(scrollbar.offset);
|
||||
const upper: f64 = @floatFromInt(scrollbar.total);
|
||||
const page_size: f64 = @floatFromInt(scrollbar.len);
|
||||
|
||||
if (std.math.approxEqAbs(f64, vadj.getValue(), value, 0.001) and
|
||||
std.math.approxEqAbs(f64, vadj.getUpper(), upper, 0.001) and
|
||||
std.math.approxEqAbs(f64, vadj.getPageSize(), page_size, 0.001))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a vadjustment we MUST have the signal group since
|
||||
// it is setup in the prop handler.
|
||||
const priv = self.private();
|
||||
const group = priv.vadj_signal_group.?;
|
||||
|
||||
// During manual scrollbar changes from Ghostty core we don't
|
||||
// want to emit value-changed signals so we block them. This would
|
||||
// cause a waste of resources at best and infinite loops at worst.
|
||||
group.block();
|
||||
defer group.unblock();
|
||||
|
||||
vadj.configure(
|
||||
value, // value: current scroll position
|
||||
0, // lower: minimum value
|
||||
upper, // upper: maximum value (total scrollable area)
|
||||
1, // step_increment: amount to scroll on arrow click
|
||||
page_size, // page_increment: amount to scroll on page up/down
|
||||
page_size, // page_size: size of visible area
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the current progress report state.
|
||||
pub fn setProgressReport(
|
||||
self: *Self,
|
||||
@@ -838,6 +948,8 @@ pub const Surface = extern struct {
|
||||
if (cfg.@"notify-on-command-finish" == .unfocused and self.getFocused()) return true;
|
||||
}
|
||||
|
||||
if (value.duration.lte(cfg.@"notify-on-command-finish-after")) return true;
|
||||
|
||||
const action = cfg.@"notify-on-command-finish-action";
|
||||
|
||||
if (action.bell) self.setBellRinging(true);
|
||||
@@ -1027,13 +1139,14 @@ pub const Surface = extern struct {
|
||||
if (entry.native == keycode) break :w3c entry.key;
|
||||
} else .unidentified;
|
||||
|
||||
// If the key should be remappable, then consult the pre-remapped
|
||||
// XKB keyval/keysym to get the (possibly) remapped key.
|
||||
// Consult the pre-remapped XKB keyval/keysym to get the (possibly)
|
||||
// remapped key. If the W3C key or the remapped key
|
||||
// is eligible for remapping, we use it.
|
||||
//
|
||||
// See the docs for `shouldBeRemappable` for why we even have to
|
||||
// do this in the first place.
|
||||
if (w3c_key.shouldBeRemappable()) {
|
||||
if (gtk_key.keyFromKeyval(keyval)) |remapped|
|
||||
if (gtk_key.keyFromKeyval(keyval)) |remapped| {
|
||||
if (w3c_key.shouldBeRemappable() or remapped.shouldBeRemappable())
|
||||
break :keycode remapped;
|
||||
}
|
||||
|
||||
@@ -1355,6 +1468,10 @@ pub const Surface = extern struct {
|
||||
// EnvMap is a bit annoying so I'm punting it.
|
||||
if (ext.getAncestor(Window, self.as(gtk.Widget))) |window| {
|
||||
try window.winproto().addSubprocessEnv(&env);
|
||||
|
||||
if (window.isQuickTerminal()) {
|
||||
try env.put("GHOSTTY_QUICK_TERMINAL", "1");
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
@@ -1443,16 +1560,16 @@ pub const Surface = extern struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
pub fn setClipboard(
|
||||
self: *Self,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) void {
|
||||
Clipboard.set(
|
||||
self,
|
||||
val,
|
||||
clipboard_type,
|
||||
contents,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
@@ -1466,6 +1583,12 @@ pub const Surface = extern struct {
|
||||
|
||||
pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void {
|
||||
const app = Application.default();
|
||||
const priv: *Private = self.private();
|
||||
|
||||
const core_surface = priv.core_surface orelse {
|
||||
log.warn("can't send notification because there is no core surface", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const t = switch (title.len) {
|
||||
0 => "Ghostty",
|
||||
@@ -1480,7 +1603,7 @@ pub const Surface = extern struct {
|
||||
defer icon.unref();
|
||||
notification.setIcon(icon.as(gio.Icon));
|
||||
|
||||
const pointer = glib.Variant.newUint64(@intFromPtr(self));
|
||||
const pointer = glib.Variant.newUint64(@intFromPtr(core_surface));
|
||||
notification.setDefaultActionAndTargetValue(
|
||||
"app.present-surface",
|
||||
pointer,
|
||||
@@ -1511,6 +1634,7 @@ pub const Surface = extern struct {
|
||||
priv.mouse_hidden = false;
|
||||
priv.focused = true;
|
||||
priv.size = .{ .width = 0, .height = 0 };
|
||||
priv.vadj_signal_group = null;
|
||||
|
||||
// If our configuration is null then we get the configuration
|
||||
// from the application.
|
||||
@@ -1575,6 +1699,22 @@ pub const Surface = extern struct {
|
||||
priv.config = null;
|
||||
}
|
||||
|
||||
if (priv.vadj_signal_group) |group| {
|
||||
group.setTarget(null);
|
||||
group.as(gobject.Object).unref();
|
||||
priv.vadj_signal_group = null;
|
||||
}
|
||||
|
||||
if (priv.hadj) |v| {
|
||||
v.as(gobject.Object).unref();
|
||||
priv.hadj = null;
|
||||
}
|
||||
|
||||
if (priv.vadj) |v| {
|
||||
v.as(gobject.Object).unref();
|
||||
priv.vadj = null;
|
||||
}
|
||||
|
||||
if (priv.progress_bar_timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove progress bar timer", .{});
|
||||
@@ -1816,6 +1956,29 @@ pub const Surface = extern struct {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
|
||||
}
|
||||
|
||||
pub fn setSearchActive(self: *Self, active: bool) void {
|
||||
const priv = self.private();
|
||||
var value = gobject.ext.Value.newFrom(active);
|
||||
defer value.unset();
|
||||
gobject.Object.setProperty(
|
||||
priv.search_overlay.as(gobject.Object),
|
||||
SearchOverlay.properties.active.name,
|
||||
&value,
|
||||
);
|
||||
|
||||
if (active) {
|
||||
priv.search_overlay.grabFocus();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setSearchTotal(self: *Self, total: ?usize) void {
|
||||
self.private().search_overlay.setSearchTotal(total);
|
||||
}
|
||||
|
||||
pub fn setSearchSelected(self: *Self, selected: ?usize) void {
|
||||
self.private().search_overlay.setSearchSelected(selected);
|
||||
}
|
||||
|
||||
fn propConfig(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1988,6 +2151,43 @@ pub const Surface = extern struct {
|
||||
self.as(gtk.Widget).setCursorFromName(name.ptr);
|
||||
}
|
||||
|
||||
fn vadjValueChanged(adj: *gtk.Adjustment, self: *Self) callconv(.c) void {
|
||||
// This will trigger for every single pixel change in the adjustment,
|
||||
// but our core surface handles the noise from this so that identical
|
||||
// rows are cheap.
|
||||
const core_surface = self.core() orelse return;
|
||||
const row: usize = @intFromFloat(@round(adj.getValue()));
|
||||
_ = core_surface.performBindingAction(.{ .scroll_to_row = row }) catch |err| {
|
||||
log.err("error performing scroll_to_row action err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn propVAdjustment(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
|
||||
// When vadjustment is first set, we setup the signal group lazily.
|
||||
// This makes it so that if we don't use scrollbars, we never
|
||||
// pay the memory cost of this.
|
||||
const group: *gobject.SignalGroup = priv.vadj_signal_group orelse group: {
|
||||
const group = gobject.SignalGroup.new(gtk.Adjustment.getGObjectType());
|
||||
group.connect(
|
||||
"value-changed",
|
||||
@ptrCast(&vadjValueChanged),
|
||||
self,
|
||||
);
|
||||
|
||||
priv.vadj_signal_group = group;
|
||||
break :group group;
|
||||
};
|
||||
|
||||
// Setup our signal group target
|
||||
group.setTarget(if (priv.vadj) |v| v.as(gobject.Object) else null);
|
||||
}
|
||||
|
||||
/// Handle bell features that need to happen every time a BEL is received
|
||||
/// Currently this is audio and system but this could change in the future.
|
||||
fn ringBell(self: *Self) void {
|
||||
@@ -2052,6 +2252,66 @@ pub const Surface = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Gtk.Scrollable interface implementation
|
||||
|
||||
pub fn getHAdjustment(self: *Self) ?*gtk.Adjustment {
|
||||
return self.private().hadj;
|
||||
}
|
||||
|
||||
pub fn setHAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void {
|
||||
self.as(gobject.Object).freezeNotify();
|
||||
defer self.as(gobject.Object).thawNotify();
|
||||
self.as(gobject.Object).notifyByPspec(properties.hadjustment.impl.param_spec);
|
||||
|
||||
const priv = self.private();
|
||||
if (priv.hadj) |old| {
|
||||
old.as(gobject.Object).unref();
|
||||
priv.hadj = null;
|
||||
}
|
||||
|
||||
const adj = adj_ orelse return;
|
||||
_ = adj.as(gobject.Object).ref();
|
||||
priv.hadj = adj;
|
||||
}
|
||||
|
||||
fn getHAdjustmentValue(self: *Self, value: *gobject.Value) void {
|
||||
gobject.ext.Value.set(value, self.getHAdjustment());
|
||||
}
|
||||
|
||||
fn setHAdjustmentValue(self: *Self, value: *const gobject.Value) void {
|
||||
self.setHAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment));
|
||||
}
|
||||
|
||||
pub fn getVAdjustment(self: *Self) ?*gtk.Adjustment {
|
||||
return self.private().vadj;
|
||||
}
|
||||
|
||||
pub fn setVAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void {
|
||||
self.as(gobject.Object).freezeNotify();
|
||||
defer self.as(gobject.Object).thawNotify();
|
||||
self.as(gobject.Object).notifyByPspec(properties.vadjustment.impl.param_spec);
|
||||
|
||||
const priv = self.private();
|
||||
|
||||
if (priv.vadj) |old| {
|
||||
old.as(gobject.Object).unref();
|
||||
priv.vadj = null;
|
||||
}
|
||||
|
||||
const adj = adj_ orelse return;
|
||||
_ = adj.as(gobject.Object).ref();
|
||||
priv.vadj = adj;
|
||||
}
|
||||
|
||||
fn getVAdjustmentValue(self: *Self, value: *gobject.Value) void {
|
||||
gobject.ext.Value.set(value, self.getVAdjustment());
|
||||
}
|
||||
|
||||
fn setVAdjustmentValue(self: *Self, value: *const gobject.Value) void {
|
||||
self.setVAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Signal Handlers
|
||||
|
||||
@@ -2938,6 +3198,35 @@ pub const Surface = extern struct {
|
||||
self.setTitleOverride(if (title.len == 0) null else title);
|
||||
}
|
||||
|
||||
fn searchStop(_: *SearchOverlay, self: *Self) callconv(.c) void {
|
||||
const surface = self.core() orelse return;
|
||||
_ = surface.performBindingAction(.end_search) catch |err| {
|
||||
log.warn("unable to perform end_search action err={}", .{err});
|
||||
};
|
||||
_ = self.private().gl_area.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
fn searchChanged(_: *SearchOverlay, needle: ?[*:0]const u8, self: *Self) callconv(.c) void {
|
||||
const surface = self.core() orelse return;
|
||||
_ = surface.performBindingAction(.{ .search = std.mem.sliceTo(needle orelse "", 0) }) catch |err| {
|
||||
log.warn("unable to perform search action err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn searchNextMatch(_: *SearchOverlay, self: *Self) callconv(.c) void {
|
||||
const surface = self.core() orelse return;
|
||||
_ = surface.performBindingAction(.{ .navigate_search = .next }) catch |err| {
|
||||
log.warn("unable to perform navigate_search action err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn searchPreviousMatch(_: *SearchOverlay, self: *Self) callconv(.c) void {
|
||||
const surface = self.core() orelse return;
|
||||
_ = surface.performBindingAction(.{ .navigate_search = .previous }) catch |err| {
|
||||
log.warn("unable to perform navigate_search action err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
@@ -2952,6 +3241,7 @@ pub const Surface = extern struct {
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
gobject.ext.ensureType(ResizeOverlay);
|
||||
gobject.ext.ensureType(SearchOverlay);
|
||||
gobject.ext.ensureType(ChildExited);
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
@@ -2971,6 +3261,7 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateChildPrivate("error_page", .{});
|
||||
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
|
||||
class.bindTemplateChildPrivate("resize_overlay", .{});
|
||||
class.bindTemplateChildPrivate("search_overlay", .{});
|
||||
class.bindTemplateChildPrivate("terminal_page", .{});
|
||||
class.bindTemplateChildPrivate("drop_target", .{});
|
||||
class.bindTemplateChildPrivate("im_context", .{});
|
||||
@@ -3005,8 +3296,13 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
||||
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
||||
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
|
||||
class.bindTemplateCallback("notify_vadjustment", &propVAdjustment);
|
||||
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
|
||||
class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown);
|
||||
class.bindTemplateCallback("search_stop", &searchStop);
|
||||
class.bindTemplateCallback("search_changed", &searchChanged);
|
||||
class.bindTemplateCallback("search_next_match", &searchNextMatch);
|
||||
class.bindTemplateCallback("search_previous_match", &searchPreviousMatch);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
@@ -3026,6 +3322,12 @@ pub const Surface = extern struct {
|
||||
properties.@"title-override".impl,
|
||||
properties.zoom.impl,
|
||||
properties.@"is-split".impl,
|
||||
|
||||
// For Gtk.Scrollable
|
||||
properties.hadjustment.impl,
|
||||
properties.vadjustment.impl,
|
||||
properties.@"hscroll-policy".impl,
|
||||
properties.@"vscroll-policy".impl,
|
||||
});
|
||||
|
||||
// Signals
|
||||
@@ -3097,24 +3399,80 @@ const Clipboard = struct {
|
||||
/// Set the clipboard contents.
|
||||
pub fn set(
|
||||
self: *Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
contents: []const apprt.ClipboardContent,
|
||||
confirm: bool,
|
||||
) void {
|
||||
const priv = self.private();
|
||||
|
||||
// Grab our plaintext content for use in confirmation dialogs
|
||||
// and signals. We always expect one to exist.
|
||||
const text: [:0]const u8 = for (contents) |content| {
|
||||
if (std.mem.eql(u8, content.mime, "text/plain")) {
|
||||
break content.data;
|
||||
}
|
||||
} else return;
|
||||
|
||||
// If no confirmation is necessary, set the clipboard.
|
||||
if (!confirm) {
|
||||
const clipboard = get(
|
||||
priv.gl_area.as(gtk.Widget),
|
||||
clipboard_type,
|
||||
) orelse return;
|
||||
clipboard.setText(val);
|
||||
|
||||
const alloc = Application.default().allocator();
|
||||
if (alloc.alloc(*gdk.ContentProvider, contents.len)) |providers| {
|
||||
// Note: we don't need to unref the individual providers
|
||||
// because new_union takes ownership of them.
|
||||
defer alloc.free(providers);
|
||||
|
||||
for (contents, 0..) |content, i| {
|
||||
const bytes = glib.Bytes.new(content.data.ptr, content.data.len);
|
||||
defer bytes.unref();
|
||||
if (std.mem.eql(u8, content.mime, "text/plain")) {
|
||||
// Add an explicit UTF-8 encoding parameter to the
|
||||
// text/plain type. The default charset when there is
|
||||
// none is ASCII, and lots of things look for UTF-8
|
||||
// specifically.
|
||||
// The specs are not clear about the order here, but
|
||||
// some clients apparently pick the first match in the
|
||||
// order we set here then garble up bare 'text/plain'
|
||||
// with non-ASCII UTF-8 content, so offer UTF-8 first.
|
||||
//
|
||||
// Note that under X11, GTK automatically adds the
|
||||
// UTF8_STRING atom when this is present.
|
||||
const text_provider_atoms = [_][:0]const u8{
|
||||
"text/plain;charset=utf-8",
|
||||
"text/plain",
|
||||
};
|
||||
var text_providers: [text_provider_atoms.len]*gdk.ContentProvider = undefined;
|
||||
for (text_provider_atoms, 0..) |atom, j| {
|
||||
const provider = gdk.ContentProvider.newForBytes(atom, bytes);
|
||||
text_providers[j] = provider;
|
||||
}
|
||||
const text_union = gdk.ContentProvider.newUnion(
|
||||
&text_providers,
|
||||
text_providers.len,
|
||||
);
|
||||
providers[i] = text_union;
|
||||
} else {
|
||||
const provider = gdk.ContentProvider.newForBytes(content.mime, bytes);
|
||||
providers[i] = provider;
|
||||
}
|
||||
}
|
||||
|
||||
const all = gdk.ContentProvider.newUnion(providers.ptr, providers.len);
|
||||
defer all.unref();
|
||||
_ = clipboard.setContent(all);
|
||||
} else |_| {
|
||||
// If we fail to alloc, we can at least set the text content.
|
||||
clipboard.setText(text);
|
||||
}
|
||||
|
||||
Surface.signals.@"clipboard-write".impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{ clipboard_type, val.ptr },
|
||||
.{ clipboard_type, text.ptr },
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -3124,7 +3482,7 @@ const Clipboard = struct {
|
||||
showClipboardConfirmation(
|
||||
self,
|
||||
.{ .osc_52_write = clipboard_type },
|
||||
val,
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
208
src/apprt/gtk/class/surface_scrolled_window.zig
Normal file
208
src/apprt/gtk/class/surface_scrolled_window.zig
Normal file
@@ -0,0 +1,208 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_surface_scrolled_window);
|
||||
|
||||
/// A wrapper widget that embeds a Surface inside a GtkScrolledWindow.
|
||||
/// This provides scrollbar functionality for the terminal surface.
|
||||
/// The surface property can be set during initialization or changed
|
||||
/// dynamically via the surface property.
|
||||
pub const SurfaceScrolledWindow = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = adw.Bin;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhostttySurfaceScrolledWindow",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const config = struct {
|
||||
pub const name = "config";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*Config,
|
||||
.{
|
||||
.accessor = C.privateObjFieldAccessor("config"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const surface = struct {
|
||||
pub const name = "surface";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*Surface,
|
||||
.{
|
||||
.accessor = .{
|
||||
.getter = getSurfaceValue,
|
||||
.setter = setSurfaceValue,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
config: ?*Config = null,
|
||||
config_binding: ?*gobject.Binding = null,
|
||||
surface: ?*Surface = null,
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
|
||||
if (priv.config_binding) |binding| {
|
||||
binding.as(gobject.Object).unref();
|
||||
priv.config_binding = null;
|
||||
}
|
||||
|
||||
if (priv.config) |v| {
|
||||
v.unref();
|
||||
priv.config = null;
|
||||
}
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
|
||||
gobject.ext.Value.set(
|
||||
value,
|
||||
self.private().surface,
|
||||
);
|
||||
}
|
||||
|
||||
fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
|
||||
self.setSurface(gobject.ext.Value.get(
|
||||
value,
|
||||
?*Surface,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn getSurface(self: *Self) ?*Surface {
|
||||
return self.private().surface;
|
||||
}
|
||||
|
||||
pub fn setSurface(self: *Self, surface_: ?*Surface) void {
|
||||
const priv = self.private();
|
||||
|
||||
if (surface_ == priv.surface) return;
|
||||
|
||||
self.as(gobject.Object).freezeNotify();
|
||||
defer self.as(gobject.Object).thawNotify();
|
||||
self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
|
||||
|
||||
priv.surface = surface_;
|
||||
}
|
||||
|
||||
fn closureScrollbarPolicy(
|
||||
_: *Self,
|
||||
config_: ?*Config,
|
||||
) callconv(.c) gtk.PolicyType {
|
||||
const config = if (config_) |c| c.get() else return .automatic;
|
||||
return switch (config.scrollbar) {
|
||||
.never => .never,
|
||||
.system => .automatic,
|
||||
};
|
||||
}
|
||||
|
||||
fn propSurface(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
const child: *gtk.Widget = self.as(Parent).getChild().?;
|
||||
const scrolled_window = gobject.ext.cast(gtk.ScrolledWindow, child).?;
|
||||
scrolled_window.setChild(if (priv.surface) |s| s.as(gtk.Widget) else null);
|
||||
|
||||
// Unbind old config binding if it exists
|
||||
if (priv.config_binding) |binding| {
|
||||
binding.as(gobject.Object).unref();
|
||||
priv.config_binding = null;
|
||||
}
|
||||
|
||||
// Bind config from surface to our config property
|
||||
if (priv.surface) |surface| {
|
||||
priv.config_binding = surface.as(gobject.Object).bindProperty(
|
||||
properties.config.name,
|
||||
self.as(gobject.Object),
|
||||
properties.config.name,
|
||||
.{ .sync_create = true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 5,
|
||||
.name = "surface-scrolled-window",
|
||||
}),
|
||||
);
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateCallback("scrollbar_policy", &closureScrollbarPolicy);
|
||||
class.bindTemplateCallback("notify_surface", &propSurface);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.config.impl,
|
||||
properties.surface.impl,
|
||||
});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
};
|
||||
@@ -6,7 +6,6 @@ const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
@@ -353,6 +347,7 @@ pub const Tab = extern struct {
|
||||
switch (mode) {
|
||||
.this => tab_view.closePage(page),
|
||||
.other => tab_view.closeOtherPages(page),
|
||||
.right => tab_view.closePagesAfter(page),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,8 +384,14 @@ pub const Tab = extern struct {
|
||||
// the terminal title if it exists, otherwise a default string.
|
||||
const plain = plain: {
|
||||
const default = "Ghostty";
|
||||
const config_title: ?[*:0]const u8 = title: {
|
||||
const config = config_ orelse break :title null;
|
||||
break :title config.get().title orelse null;
|
||||
};
|
||||
|
||||
const plain = override_ orelse
|
||||
terminal_ orelse
|
||||
config_title orelse
|
||||
break :plain default;
|
||||
break :plain std.mem.span(plain);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gio = @import("gio");
|
||||
@@ -28,7 +28,6 @@ const Surface = @import("surface.zig").Surface;
|
||||
const Tab = @import("tab.zig").Tab;
|
||||
const DebugWarning = @import("debug_warning.zig").DebugWarning;
|
||||
const CommandPalette = @import("command_palette.zig").CommandPalette;
|
||||
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_window);
|
||||
@@ -794,7 +793,7 @@ pub const Window = extern struct {
|
||||
|
||||
/// Get the currently active surface. See the "active-surface" property.
|
||||
/// This does not ref the value.
|
||||
fn getActiveSurface(self: *Self) ?*Surface {
|
||||
pub fn getActiveSurface(self: *Self) ?*Surface {
|
||||
const tab = self.getSelectedTab() orelse return null;
|
||||
return tab.getActiveSurface();
|
||||
}
|
||||
@@ -1015,6 +1014,15 @@ pub const Window = extern struct {
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// Hide quick-terminal if set to autohide
|
||||
if (self.isQuickTerminal()) {
|
||||
if (self.getConfig()) |cfg| {
|
||||
if (cfg.get().@"quick-terminal-autohide" and self.as(gtk.Window).isActive() == 0) {
|
||||
self.toggleVisibility();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't change urgency if we're not the active window.
|
||||
if (self.as(gtk.Window).isActive() == 0) return;
|
||||
|
||||
@@ -1585,6 +1593,9 @@ pub const Window = extern struct {
|
||||
|
||||
// Grab focus
|
||||
surface.grabFocus();
|
||||
|
||||
// Bring the window to the front.
|
||||
self.as(gtk.Window).present();
|
||||
}
|
||||
|
||||
fn surfaceToggleFullscreen(
|
||||
@@ -1789,7 +1800,7 @@ pub const Window = extern struct {
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.copy_to_clipboard);
|
||||
self.performBindingAction(.{ .copy_to_clipboard = .mixed });
|
||||
}
|
||||
|
||||
fn actionPaste(
|
||||
|
||||
@@ -34,6 +34,18 @@ label.url-overlay.right {
|
||||
border-radius: 6px 0px 0px 0px;
|
||||
}
|
||||
|
||||
/*
|
||||
* GhosttySurface search overlay
|
||||
*/
|
||||
.search-overlay {
|
||||
padding: 6px 8px;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
outline-style: solid;
|
||||
outline-color: #555555;
|
||||
outline-width: 1px;
|
||||
}
|
||||
|
||||
/*
|
||||
* GhosttySurface resize overlay
|
||||
*/
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
//! helpers.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const testing = std.testing;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const testing = std.testing;
|
||||
|
||||
const gio = @import("gio");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const glib = @import("glib");
|
||||
|
||||
94
src/apprt/gtk/ui/1.2/search-overlay.blp
Normal file
94
src/apprt/gtk/ui/1.2/search-overlay.blp
Normal file
@@ -0,0 +1,94 @@
|
||||
using Gtk 4.0;
|
||||
using Gdk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttySearchOverlay: Adw.Bin {
|
||||
visible: bind template.active;
|
||||
halign-target: end;
|
||||
valign-target: start;
|
||||
halign: bind template.halign-target;
|
||||
valign: bind template.valign-target;
|
||||
|
||||
GestureDrag {
|
||||
button: 1;
|
||||
propagation-phase: capture;
|
||||
drag-end => $on_drag_end();
|
||||
}
|
||||
|
||||
Adw.Bin {
|
||||
Box container {
|
||||
styles [
|
||||
"background",
|
||||
"search-overlay",
|
||||
]
|
||||
|
||||
orientation: horizontal;
|
||||
spacing: 6;
|
||||
|
||||
SearchEntry search_entry {
|
||||
placeholder-text: _("Find…");
|
||||
width-chars: 20;
|
||||
hexpand: true;
|
||||
stop-search => $stop_search();
|
||||
search-changed => $search_changed();
|
||||
next-match => $next_match();
|
||||
previous-match => $previous_match();
|
||||
|
||||
EventControllerKey {
|
||||
// We need this so we capture before the SearchEntry.
|
||||
propagation-phase: capture;
|
||||
key-pressed => $search_entry_key_pressed();
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
styles [
|
||||
"dim-label",
|
||||
]
|
||||
|
||||
label: bind $match_label_closure(template.has-search-selected, template.search-selected, template.has-search-total, template.search-total) as <string>;
|
||||
width-chars: 6;
|
||||
xalign: 1.0;
|
||||
}
|
||||
|
||||
Box button_box {
|
||||
orientation: horizontal;
|
||||
spacing: 1;
|
||||
|
||||
styles [
|
||||
"linked",
|
||||
]
|
||||
|
||||
Button prev_button {
|
||||
icon-name: "go-up-symbolic";
|
||||
tooltip-text: _("Previous Match");
|
||||
clicked => $next_match();
|
||||
|
||||
cursor: Gdk.Cursor {
|
||||
name: "pointer";
|
||||
};
|
||||
}
|
||||
|
||||
Button next_button {
|
||||
icon-name: "go-down-symbolic";
|
||||
tooltip-text: _("Next Match");
|
||||
clicked => $previous_match();
|
||||
|
||||
cursor: Gdk.Cursor {
|
||||
name: "pointer";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Button close_button {
|
||||
icon-name: "window-close-symbolic";
|
||||
tooltip-text: _("Close");
|
||||
clicked => $stop_search();
|
||||
|
||||
cursor: Gdk.Cursor {
|
||||
name: "pointer";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,34 @@ Overlay terminal_page {
|
||||
halign: start;
|
||||
has-arrow: false;
|
||||
}
|
||||
|
||||
EventControllerFocus {
|
||||
enter => $focus_enter();
|
||||
leave => $focus_leave();
|
||||
}
|
||||
|
||||
EventControllerKey {
|
||||
key-pressed => $key_pressed();
|
||||
key-released => $key_released();
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
motion => $mouse_motion();
|
||||
leave => $mouse_leave();
|
||||
}
|
||||
|
||||
GestureClick {
|
||||
pressed => $mouse_down();
|
||||
released => $mouse_up();
|
||||
button: 0;
|
||||
}
|
||||
};
|
||||
|
||||
[overlay]
|
||||
@@ -64,6 +92,10 @@ Overlay terminal_page {
|
||||
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
|
||||
transition-type: crossfade;
|
||||
transition-duration: 500;
|
||||
// Revealers take up the full size, we need this to not capture events.
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
Box bell_overlay {
|
||||
styles [
|
||||
@@ -115,12 +147,26 @@ Overlay terminal_page {
|
||||
label: bind template.mouse-hover-url;
|
||||
}
|
||||
|
||||
[overlay]
|
||||
$GhosttySearchOverlay search_overlay {
|
||||
stop-search => $search_stop();
|
||||
search-changed => $search_changed();
|
||||
next-match => $search_next_match();
|
||||
previous-match => $search_previous_match();
|
||||
}
|
||||
|
||||
[overlay]
|
||||
// Apply unfocused-split-fill and unfocused-split-opacity to current surface
|
||||
// this is only applied when a tab has more than one surface
|
||||
Revealer {
|
||||
reveal-child: bind $should_unfocused_split_be_shown(template.focused, template.is-split) as <bool>;
|
||||
transition-duration: 0;
|
||||
// This is all necessary so that the Revealer itself doesn't override
|
||||
// any input events from the other overlays. Namely, if you don't have
|
||||
// these then the search overlay won't get mouse events.
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
DrawingArea {
|
||||
styles [
|
||||
@@ -129,35 +175,6 @@ Overlay terminal_page {
|
||||
}
|
||||
}
|
||||
|
||||
// Event controllers for interactivity
|
||||
EventControllerFocus {
|
||||
enter => $focus_enter();
|
||||
leave => $focus_leave();
|
||||
}
|
||||
|
||||
EventControllerKey {
|
||||
key-pressed => $key_pressed();
|
||||
key-released => $key_released();
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
motion => $mouse_motion();
|
||||
leave => $mouse_leave();
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
}
|
||||
|
||||
GestureClick {
|
||||
pressed => $mouse_down();
|
||||
released => $mouse_up();
|
||||
button: 0;
|
||||
}
|
||||
|
||||
DropTarget drop_target {
|
||||
drop => $drop();
|
||||
actions: copy;
|
||||
@@ -174,6 +191,7 @@ template $GhosttySurface: Adw.Bin {
|
||||
notify::mouse-hover-url => $notify_mouse_hover_url();
|
||||
notify::mouse-hidden => $notify_mouse_hidden();
|
||||
notify::mouse-shape => $notify_mouse_shape();
|
||||
notify::vadjustment => $notify_vadjustment();
|
||||
// Some history: we used to use a Stack here and swap between the
|
||||
// terminal and error pages as needed. But a Stack doesn't play nice
|
||||
// with our SplitTree and Gtk.Paned usage[^1]. Replacing this with
|
||||
|
||||
11
src/apprt/gtk/ui/1.5/surface-scrolled-window.blp
Normal file
11
src/apprt/gtk/ui/1.5/surface-scrolled-window.blp
Normal file
@@ -0,0 +1,11 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhostttySurfaceScrolledWindow: Adw.Bin {
|
||||
notify::surface => $notify_surface();
|
||||
|
||||
Gtk.ScrolledWindow {
|
||||
hscrollbar-policy: never;
|
||||
vscrollbar-policy: bind $scrollbar_policy(template.config) as <Gtk.PolicyType>;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gdk_wayland = @import("gdk_wayland");
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
//! X11 window protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gdk_x11 = @import("gdk_x11");
|
||||
const glib = @import("glib");
|
||||
@@ -175,6 +173,12 @@ pub const Window = struct {
|
||||
|
||||
blur_region: Region = .{},
|
||||
|
||||
// Cache last applied values to avoid redundant X11 property updates.
|
||||
// Redundant property updates seem to cause some visual glitches
|
||||
// with some window managers: https://github.com/ghostty-org/ghostty/pull/8075
|
||||
last_applied_blur_region: ?Region = null,
|
||||
last_applied_decoration_hints: ?MotifWMHints = null,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
@@ -257,30 +261,42 @@ pub const Window = struct {
|
||||
const gtk_widget = self.apprt_window.as(gtk.Widget);
|
||||
const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
// When blur is disabled, remove the property if it was previously set
|
||||
const blur = config.@"background-blur";
|
||||
if (!blur.enabled()) {
|
||||
if (self.last_applied_blur_region != null) {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
self.last_applied_blur_region = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = gtk_widget.getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
|
||||
const blur = config.@"background-blur";
|
||||
// Only update X11 properties when the blur region actually changes
|
||||
if (self.last_applied_blur_region) |last| {
|
||||
if (std.meta.eql(self.blur_region, last)) return;
|
||||
}
|
||||
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur.enabled()) {
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
} else {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
}
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
self.last_applied_blur_region = self.blur_region;
|
||||
}
|
||||
|
||||
fn syncDecorations(self: *Window) !void {
|
||||
@@ -309,6 +325,11 @@ pub const Window = struct {
|
||||
.auto, .client, .none => false,
|
||||
};
|
||||
|
||||
// Only update decoration hints when they actually change
|
||||
if (self.last_applied_decoration_hints) |last| {
|
||||
if (std.meta.eql(hints, last)) return;
|
||||
}
|
||||
|
||||
try self.changeProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
@@ -317,6 +338,7 @@ pub const Window = struct {
|
||||
.{ .mode = .replace },
|
||||
&hints,
|
||||
);
|
||||
self.last_applied_decoration_hints = hints;
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! process.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
|
||||
pub const Errors = error{
|
||||
/// The IPC failed. If a function returns this error, it's expected that
|
||||
|
||||
@@ -54,6 +54,11 @@ pub const Clipboard = enum(Backing) {
|
||||
};
|
||||
};
|
||||
|
||||
pub const ClipboardContent = struct {
|
||||
mime: [:0]const u8,
|
||||
data: [:0]const u8,
|
||||
};
|
||||
|
||||
pub const ClipboardRequestType = enum(u8) {
|
||||
paste,
|
||||
osc_52_read,
|
||||
|
||||
@@ -6,15 +6,15 @@ const build_config = @import("../build_config.zig");
|
||||
const App = @import("../App.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const termio = @import("../termio.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
const MessageData = @import("../datastruct/main.zig").MessageData;
|
||||
|
||||
/// The message types that can be sent to a single surface.
|
||||
pub const Message = union(enum) {
|
||||
/// Represents a write request. Magic number comes from the max size
|
||||
/// we want this union to be.
|
||||
pub const WriteReq = termio.MessageData(u8, 255);
|
||||
pub const WriteReq = MessageData(u8, 255);
|
||||
|
||||
/// Set the title of the surface.
|
||||
/// TODO: we should change this to a "WriteReq" style structure in
|
||||
@@ -104,6 +104,15 @@ pub const Message = union(enum) {
|
||||
/// of the command.
|
||||
stop_command: ?u8,
|
||||
|
||||
/// The scrollbar state changed for the surface.
|
||||
scrollbar: terminal.Scrollbar,
|
||||
|
||||
/// Search progress update
|
||||
search_total: ?usize,
|
||||
|
||||
/// Selected search index change
|
||||
search_selected: ?usize,
|
||||
|
||||
pub const ReportTitleStyle = enum {
|
||||
csi_21_t,
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
@@ -134,7 +134,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
@@ -166,7 +166,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
@@ -113,7 +113,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
const IsSymbol = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
@@ -90,13 +89,15 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *IsSymbol = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var r = f.reader(&read_buf);
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
var d: UTF8Decoder = .{};
|
||||
var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
const n = r.readSliceShort(&buf) catch {
|
||||
log.warn("error reading data file err={?}", .{f_reader.err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
@@ -115,13 +116,15 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *IsSymbol = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var r = f.reader(&read_buf);
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
var d: UTF8Decoder = .{};
|
||||
var buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
const n = r.readSliceShort(&buf) catch {
|
||||
log.warn("error reading data file err={?}", .{f_reader.err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
|
||||
118
src/benchmark/OscParser.zig
Normal file
118
src/benchmark/OscParser.zig
Normal file
@@ -0,0 +1,118 @@
|
||||
//! This benchmark tests the throughput of the OSC parser.
|
||||
const OscParser = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const Parser = @import("../terminal/osc.zig").Parser;
|
||||
const log = std.log.scoped(.@"osc-parser-bench");
|
||||
|
||||
opts: Options,
|
||||
|
||||
/// The file, opened in the setup function.
|
||||
data_f: ?std.fs.File = null,
|
||||
|
||||
parser: Parser,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*OscParser {
|
||||
const ptr = try alloc.create(OscParser);
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{
|
||||
.opts = opts,
|
||||
.data_f = null,
|
||||
.parser = .init(alloc),
|
||||
};
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *OscParser, alloc: Allocator) void {
|
||||
self.parser.deinit();
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *OscParser) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = step,
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *OscParser = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
self.parser.reset();
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *OscParser = @ptrCast(@alignCast(ptr));
|
||||
if (self.data_f) |f| {
|
||||
f.close();
|
||||
self.data_f = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *OscParser = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var r = f.reader(&read_buf);
|
||||
|
||||
var osc_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
while (true) {
|
||||
r.interface.fill(@bitSizeOf(usize) / 8) catch |err| switch (err) {
|
||||
error.EndOfStream => return,
|
||||
error.ReadFailed => return error.BenchmarkFailed,
|
||||
};
|
||||
const len = r.interface.takeInt(usize, .little) catch |err| switch (err) {
|
||||
error.EndOfStream => return,
|
||||
error.ReadFailed => return error.BenchmarkFailed,
|
||||
};
|
||||
|
||||
if (len > osc_buf.len) return error.BenchmarkFailed;
|
||||
|
||||
r.interface.readSliceAll(osc_buf[0..len]) catch |err| switch (err) {
|
||||
error.EndOfStream => return,
|
||||
error.ReadFailed => return error.BenchmarkFailed,
|
||||
};
|
||||
|
||||
for (osc_buf[0..len]) |c| self.parser.next(c);
|
||||
_ = self.parser.end(std.ascii.control_code.bel);
|
||||
self.parser.reset();
|
||||
}
|
||||
}
|
||||
|
||||
test OscParser {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *OscParser = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
}
|
||||
196
src/benchmark/ScreenClone.zig
Normal file
196
src/benchmark/ScreenClone.zig
Normal file
@@ -0,0 +1,196 @@
|
||||
//! This benchmark tests the performance of the Screen.clone
|
||||
//! function. This is useful because it is one of the primary lock
|
||||
//! holders that impact IO performance when the renderer is active.
|
||||
//! We do this very frequently.
|
||||
const ScreenClone = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const terminalpkg = @import("../terminal/main.zig");
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const Terminal = terminalpkg.Terminal;
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
opts: Options,
|
||||
terminal: Terminal,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The type of codepoint width calculation to use.
|
||||
mode: Mode = .clone,
|
||||
|
||||
/// The size of the terminal. This affects benchmarking when
|
||||
/// dealing with soft line wrapping and the memory impact
|
||||
/// of page sizes.
|
||||
@"terminal-rows": u16 = 80,
|
||||
@"terminal-cols": u16 = 120,
|
||||
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
///
|
||||
/// This will be used to initialize the terminal screen state before
|
||||
/// cloning. This data can switch to alt screen if it wants. The time
|
||||
/// to read this is not part of the benchmark.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Mode = enum {
|
||||
/// The baseline mode copies the screen by value.
|
||||
noop,
|
||||
|
||||
/// Full clone
|
||||
clone,
|
||||
|
||||
/// RenderState rather than a screen clone.
|
||||
render,
|
||||
};
|
||||
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*ScreenClone {
|
||||
const ptr = try alloc.create(ScreenClone);
|
||||
errdefer alloc.destroy(ptr);
|
||||
|
||||
ptr.* = .{
|
||||
.opts = opts,
|
||||
.terminal = try .init(alloc, .{
|
||||
.rows = opts.@"terminal-rows",
|
||||
.cols = opts.@"terminal-cols",
|
||||
}),
|
||||
};
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *ScreenClone, alloc: Allocator) void {
|
||||
self.terminal.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *ScreenClone) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = switch (self.opts.mode) {
|
||||
.noop => stepNoop,
|
||||
.clone => stepClone,
|
||||
.render => stepRender,
|
||||
},
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Always reset our terminal state
|
||||
self.terminal.fullReset();
|
||||
|
||||
// Force a style on every single row, which
|
||||
var s = self.terminal.vtStream();
|
||||
defer s.deinit();
|
||||
s.nextSlice("\x1b[48;2;20;40;60m") catch unreachable;
|
||||
for (0..self.terminal.rows - 1) |_| s.nextSlice("hello\r\n") catch unreachable;
|
||||
s.nextSlice("hello") catch unreachable;
|
||||
|
||||
// Setup our terminal state
|
||||
const data_f: std.fs.File = (options.dataFile(
|
||||
self.opts.data,
|
||||
) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
}) orelse return;
|
||||
|
||||
var stream = self.terminal.vtStream();
|
||||
defer stream.deinit();
|
||||
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = data_f.reader(&read_buf);
|
||||
const r = &f_reader.interface;
|
||||
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.readSliceShort(&buf) catch {
|
||||
log.warn("error reading data file err={?}", .{f_reader.err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
stream.nextSlice(buf[0..n]) catch |err| {
|
||||
log.warn("error processing data file chunk err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// We loop because its so fast that a single benchmark run doesn't
|
||||
// properly capture our speeds.
|
||||
for (0..1000) |_| {
|
||||
const s: terminalpkg.Screen = self.terminal.screens.active.*;
|
||||
std.mem.doNotOptimizeAway(s);
|
||||
}
|
||||
}
|
||||
|
||||
fn stepClone(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// We loop because its so fast that a single benchmark run doesn't
|
||||
// properly capture our speeds.
|
||||
for (0..1000) |_| {
|
||||
const s: *terminalpkg.Screen = self.terminal.screens.active;
|
||||
const copy = s.clone(
|
||||
s.alloc,
|
||||
.{ .viewport = .{} },
|
||||
null,
|
||||
) catch |err| {
|
||||
log.warn("error cloning screen err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
std.mem.doNotOptimizeAway(copy);
|
||||
|
||||
// Note: we purposely do not free memory because we don't want
|
||||
// to benchmark that. We'll free when the benchmark exits.
|
||||
}
|
||||
}
|
||||
|
||||
fn stepRender(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// We do this once out of the loop because a significant slowdown
|
||||
// on the first run is allocation. After that first run, even with
|
||||
// a full rebuild, it is much faster. Let's ignore that first run
|
||||
// slowdown.
|
||||
const alloc = self.terminal.screens.active.alloc;
|
||||
var state: terminalpkg.RenderState = .empty;
|
||||
state.update(alloc, &self.terminal) catch |err| {
|
||||
log.warn("error cloning screen err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
|
||||
// We loop because its so fast that a single benchmark run doesn't
|
||||
// properly capture our speeds.
|
||||
for (0..1000) |_| {
|
||||
// Forces a full rebuild because it thinks our screen changed
|
||||
state.screen = .alternate;
|
||||
state.update(alloc, &self.terminal) catch |err| {
|
||||
log.warn("error cloning screen err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
std.mem.doNotOptimizeAway(state);
|
||||
|
||||
// Note: we purposely do not free memory because we don't want
|
||||
// to benchmark that. We'll free when the benchmark exits.
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
// the benchmark results and... I know writing this that we
|
||||
// aren't currently IO bound.
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
// aren't currently IO bound.
|
||||
const f = self.data_f orelse return;
|
||||
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
const r = &f_reader.interface;
|
||||
|
||||
@@ -138,8 +138,15 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const Handler = struct {
|
||||
t: *Terminal,
|
||||
|
||||
pub fn print(self: *Handler, cp: u21) !void {
|
||||
try self.t.print(cp);
|
||||
pub fn vt(
|
||||
self: *Handler,
|
||||
comptime action: Stream.Action.Tag,
|
||||
value: Stream.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.print => try self.t.print(value.cp),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ const cli = @import("../cli.zig");
|
||||
pub const Action = enum {
|
||||
@"codepoint-width",
|
||||
@"grapheme-break",
|
||||
@"screen-clone",
|
||||
@"terminal-parser",
|
||||
@"terminal-stream",
|
||||
@"is-symbol",
|
||||
@"osc-parser",
|
||||
|
||||
/// Returns the struct associated with the action. The struct
|
||||
/// should have a few decls:
|
||||
@@ -22,11 +24,13 @@ pub const Action = enum {
|
||||
/// See TerminalStream for an example.
|
||||
pub fn Struct(comptime action: Action) type {
|
||||
return switch (action) {
|
||||
.@"screen-clone" => @import("ScreenClone.zig"),
|
||||
.@"terminal-stream" => @import("TerminalStream.zig"),
|
||||
.@"codepoint-width" => @import("CodepointWidth.zig"),
|
||||
.@"grapheme-break" => @import("GraphemeBreak.zig"),
|
||||
.@"terminal-parser" => @import("TerminalParser.zig"),
|
||||
.@"is-symbol" => @import("IsSymbol.zig"),
|
||||
.@"osc-parser" => @import("OscParser.zig"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ pub const CApi = @import("CApi.zig");
|
||||
pub const TerminalStream = @import("TerminalStream.zig");
|
||||
pub const CodepointWidth = @import("CodepointWidth.zig");
|
||||
pub const GraphemeBreak = @import("GraphemeBreak.zig");
|
||||
pub const ScreenClone = @import("ScreenClone.zig");
|
||||
pub const TerminalParser = @import("TerminalParser.zig");
|
||||
pub const IsSymbol = @import("IsSymbol.zig");
|
||||
|
||||
|
||||
@@ -16,13 +16,6 @@ const expandPath = @import("../os/path.zig").expand;
|
||||
const gtk = @import("gtk.zig");
|
||||
const GitVersion = @import("GitVersion.zig");
|
||||
|
||||
/// The version of the next release.
|
||||
///
|
||||
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
|
||||
/// Until then this MUST match build.zig.zon and should always be the
|
||||
/// _next_ version to release.
|
||||
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 1 };
|
||||
|
||||
/// Standard build configuration options.
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
target: std.Build.ResolvedTarget,
|
||||
@@ -62,6 +55,7 @@ emit_macos_app: bool = false,
|
||||
emit_terminfo: bool = false,
|
||||
emit_termcap: bool = false,
|
||||
emit_test_exe: bool = false,
|
||||
emit_themes: bool = false,
|
||||
emit_xcframework: bool = false,
|
||||
emit_webdata: bool = false,
|
||||
emit_unicode_table_gen: bool = false,
|
||||
@@ -69,7 +63,7 @@ emit_unicode_table_gen: bool = false,
|
||||
/// Environmental properties
|
||||
env: std.process.EnvMap,
|
||||
|
||||
pub fn init(b: *std.Build) !Config {
|
||||
pub fn init(b: *std.Build, appVersion: []const u8) !Config {
|
||||
// Setup our standard Zig target and optimize options, i.e.
|
||||
// `-Doptimize` and `-Dtarget`.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
@@ -179,7 +173,13 @@ pub fn init(b: *std.Build) !Config {
|
||||
bool,
|
||||
"simd",
|
||||
"Build with SIMD-accelerated code paths. Results in significant performance improvements.",
|
||||
) orelse true;
|
||||
) orelse simd: {
|
||||
// We can't build our SIMD dependencies for Wasm. Note that we may
|
||||
// still use SIMD features in the Wasm-builds.
|
||||
if (target.result.cpu.arch.isWasm()) break :simd false;
|
||||
|
||||
break :simd true;
|
||||
};
|
||||
|
||||
config.wayland = b.option(
|
||||
bool,
|
||||
@@ -217,6 +217,17 @@ pub fn init(b: *std.Build) !Config {
|
||||
// If an explicit version is given, we always use it.
|
||||
try std.SemanticVersion.parse(v)
|
||||
else version: {
|
||||
const app_version = try std.SemanticVersion.parse(appVersion);
|
||||
|
||||
// Is ghostty a dependency? If so, skip git detection.
|
||||
// @src().file won't resolve from b.build_root unless ghostty
|
||||
// is the project being built.
|
||||
b.build_root.handle.access(@src().file, .{}) catch break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
};
|
||||
|
||||
// If no explicit version is given, we try to detect it from git.
|
||||
const vsn = GitVersion.detect(b) catch |err| switch (err) {
|
||||
// If Git isn't available we just make an unknown dev version.
|
||||
@@ -374,6 +385,12 @@ pub fn init(b: *std.Build) !Config {
|
||||
.ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
|
||||
};
|
||||
|
||||
config.emit_themes = b.option(
|
||||
bool,
|
||||
"emit-themes",
|
||||
"Install bundled iTerm2-Color-Schemes Ghostty themes",
|
||||
) orelse true;
|
||||
|
||||
config.emit_webdata = b.option(
|
||||
bool,
|
||||
"emit-webdata",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
const GhosttyBench = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
|
||||
steps: []*std.Build.Step.Compile,
|
||||
|
||||
@@ -170,11 +170,11 @@ pub const Resource = struct {
|
||||
|
||||
/// Returns true if the dist path exists at build time.
|
||||
pub fn exists(self: *const Resource, b: *std.Build) bool {
|
||||
if (std.fs.accessAbsolute(b.pathFromRoot(self.dist), .{})) {
|
||||
if (b.build_root.handle.access(self.dist, .{})) {
|
||||
// If we have a ".git" directory then we're a git checkout
|
||||
// and we never want to use the dist path. This shouldn't happen
|
||||
// so show a warning to the user.
|
||||
if (std.fs.accessAbsolute(b.pathFromRoot(".git"), .{})) {
|
||||
if (b.build_root.handle.access(".git", .{})) {
|
||||
std.log.warn(
|
||||
"dist resource '{s}' should not be in a git checkout",
|
||||
.{self.dist},
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
const GhosttyFrameData = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const DistResource = @import("GhosttyDist.zig").Resource;
|
||||
|
||||
/// The output path for the compressed framedata zig file
|
||||
@@ -43,7 +41,6 @@ pub fn distResources(b: *std.Build) struct {
|
||||
.root_module = b.createModule(.{
|
||||
.target = b.graph.host,
|
||||
}),
|
||||
.use_llvm = true,
|
||||
});
|
||||
exe.addCSourceFile(.{
|
||||
.file = b.path("src/build/framegen/main.c"),
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
const GhosttyLibVt = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const RunStep = std.Build.Step.Run;
|
||||
const Config = @import("Config.zig");
|
||||
const GhosttyZig = @import("GhosttyZig.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const LibtoolStep = @import("LibtoolStep.zig");
|
||||
const LipoStep = @import("LipoStep.zig");
|
||||
|
||||
/// The step that generates the file.
|
||||
step: *std.Build.Step,
|
||||
@@ -17,7 +14,35 @@ artifact: *std.Build.Step.InstallArtifact,
|
||||
/// The final library file
|
||||
output: std.Build.LazyPath,
|
||||
dsym: ?std.Build.LazyPath,
|
||||
pkg_config: std.Build.LazyPath,
|
||||
pkg_config: ?std.Build.LazyPath,
|
||||
|
||||
pub fn initWasm(
|
||||
b: *std.Build,
|
||||
zig: *const GhosttyZig,
|
||||
) !GhosttyLibVt {
|
||||
const target = zig.vt.resolved_target.?;
|
||||
assert(target.result.cpu.arch.isWasm());
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "ghostty-vt",
|
||||
.root_module = zig.vt_c,
|
||||
.version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 },
|
||||
});
|
||||
|
||||
// Allow exported symbols to actually be exported.
|
||||
exe.rdynamic = true;
|
||||
|
||||
// There is no entrypoint for this wasm module.
|
||||
exe.entry = .disabled;
|
||||
|
||||
return .{
|
||||
.step = &exe.step,
|
||||
.artifact = b.addInstallArtifact(exe, .{}),
|
||||
.output = exe.getEmittedBin(),
|
||||
.dsym = null,
|
||||
.pkg_config = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initShared(
|
||||
b: *std.Build,
|
||||
@@ -30,9 +55,10 @@ pub fn initShared(
|
||||
.root_module = zig.vt_c,
|
||||
.version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 },
|
||||
});
|
||||
lib.installHeader(
|
||||
b.path("include/ghostty/vt.h"),
|
||||
"ghostty/vt.h",
|
||||
lib.installHeadersDirectory(
|
||||
b.path("include/ghostty"),
|
||||
"ghostty",
|
||||
.{ .include_extensions = &.{".h"} },
|
||||
);
|
||||
|
||||
// Get our debug symbols
|
||||
@@ -81,9 +107,11 @@ pub fn install(
|
||||
) void {
|
||||
const b = step.owner;
|
||||
step.dependOn(&self.artifact.step);
|
||||
step.dependOn(&b.addInstallFileWithDir(
|
||||
self.pkg_config,
|
||||
.prefix,
|
||||
"share/pkgconfig/libghostty-vt.pc",
|
||||
).step);
|
||||
if (self.pkg_config) |pkg_config| {
|
||||
step.dependOn(&b.addInstallFileWithDir(
|
||||
pkg_config,
|
||||
.prefix,
|
||||
"share/pkgconfig/libghostty-vt.pc",
|
||||
).step);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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 RunStep = std.Build.Step.Run;
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
|
||||
steps: []*std.Build.Step,
|
||||
|
||||
pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !GhosttyResources {
|
||||
var steps: std.ArrayList(*std.Build.Step) = .empty;
|
||||
errdefer steps.deinit(b.allocator);
|
||||
|
||||
@@ -26,6 +25,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
});
|
||||
build_data_exe.linkLibC();
|
||||
|
||||
deps.help_strings.addImport(build_data_exe);
|
||||
|
||||
// Terminfo
|
||||
terminfo: {
|
||||
const os_tag = cfg.target.result.os.tag;
|
||||
@@ -125,14 +126,16 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
}
|
||||
|
||||
// Themes
|
||||
if (b.lazyDependency("iterm2_themes", .{})) |upstream| {
|
||||
const install_step = b.addInstallDirectory(.{
|
||||
.source_dir = upstream.path(""),
|
||||
.install_dir = .{ .custom = "share" },
|
||||
.install_subdir = b.pathJoin(&.{ "ghostty", "themes" }),
|
||||
.exclude_extensions = &.{".md"},
|
||||
});
|
||||
try steps.append(b.allocator, &install_step.step);
|
||||
if (cfg.emit_themes) {
|
||||
if (b.lazyDependency("iterm2_themes", .{})) |upstream| {
|
||||
const install_step = b.addInstallDirectory(.{
|
||||
.source_dir = upstream.path(""),
|
||||
.install_dir = .{ .custom = "share" },
|
||||
.install_subdir = b.pathJoin(&.{ "ghostty", "themes" }),
|
||||
.exclude_extensions = &.{".md"},
|
||||
});
|
||||
try steps.append(b.allocator, &install_step.step);
|
||||
}
|
||||
}
|
||||
|
||||
// Fish shell completions
|
||||
@@ -225,7 +228,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
||||
// 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes'
|
||||
// directory. The syntax then needs to be mapped to the correct language in
|
||||
// the config file within the '~.config/bat' directory
|
||||
// (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config").
|
||||
// (ex: --map-syntax "/Users/user/.config/ghostty/config.ghostty:Ghostty Config").
|
||||
{
|
||||
const run = b.addRunArtifact(build_data_exe);
|
||||
run.addArg("+sublime");
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
const GhosttyWebdata = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
|
||||
steps: []*std.Build.Step,
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn init(
|
||||
.ReleaseSafe,
|
||||
.ReleaseSmall,
|
||||
.ReleaseFast,
|
||||
=> "Release",
|
||||
=> "ReleaseLocal",
|
||||
};
|
||||
|
||||
const xc_arch: ?[]const u8 = switch (deps.xcframework.target) {
|
||||
@@ -151,7 +151,7 @@ pub fn init(
|
||||
|
||||
// This overrides our default behavior and forces logs to show
|
||||
// up on stderr (in addition to the centralized macOS log).
|
||||
open.setEnvironmentVariable("GHOSTTY_LOG", "1");
|
||||
open.setEnvironmentVariable("GHOSTTY_LOG", "stderr,macos");
|
||||
|
||||
// Configure how we're launching
|
||||
open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run");
|
||||
|
||||
@@ -719,15 +719,19 @@ pub fn addSimd(
|
||||
}
|
||||
|
||||
// Highway
|
||||
if (b.lazyDependency("highway", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |highway_dep| {
|
||||
m.linkLibrary(highway_dep.artifact("highway"));
|
||||
if (static_libs) |v| try v.append(
|
||||
b.allocator,
|
||||
highway_dep.artifact("highway").getEmittedBin(),
|
||||
);
|
||||
if (b.systemIntegrationOption("highway", .{ .default = false })) {
|
||||
m.linkSystemLibrary("libhwy", dynamic_link_opts);
|
||||
} else {
|
||||
if (b.lazyDependency("highway", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |highway_dep| {
|
||||
m.linkLibrary(highway_dep.artifact("highway"));
|
||||
if (static_libs) |v| try v.append(
|
||||
b.allocator,
|
||||
highway_dep.artifact("highway").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// utfcpp - This is used as a dependency on our hand-written C++ code
|
||||
@@ -746,6 +750,7 @@ pub fn addSimd(
|
||||
m.addIncludePath(b.path("src"));
|
||||
{
|
||||
// From hwy/detect_targets.h
|
||||
const HWY_AVX10_2: c_int = 1 << 3;
|
||||
const HWY_AVX3_SPR: c_int = 1 << 4;
|
||||
const HWY_AVX3_ZEN4: c_int = 1 << 6;
|
||||
const HWY_AVX3_DL: c_int = 1 << 7;
|
||||
@@ -756,7 +761,7 @@ pub fn addSimd(
|
||||
// The performance difference between AVX2 and AVX512 is not
|
||||
// significant for our use case and AVX512 is very rare on consumer
|
||||
// hardware anyways.
|
||||
const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
|
||||
const HWY_DISABLED_TARGETS: c_int = HWY_AVX10_2 | HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
|
||||
|
||||
m.addCSourceFiles(.{
|
||||
.files = &.{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const UnicodeTables = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
|
||||
/// The exe.
|
||||
props_exe: *std.Build.Step.Compile,
|
||||
|
||||
@@ -24,12 +24,12 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY ./build.zig /src
|
||||
COPY ./build.zig ./build.zig.zon /src/
|
||||
|
||||
# Install zig
|
||||
# https://ziglang.org/download/
|
||||
|
||||
RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$(uname -m)-linux-$ZIG_VERSION.tar.xz" && \
|
||||
RUN export ZIG_VERSION=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$(uname -m)-linux-$ZIG_VERSION.tar.xz" && \
|
||||
tar -xf /tmp/zig.tar.xz -C /opt && \
|
||||
rm /tmp/zig.tar.xz && \
|
||||
ln -s "/opt/zig-$(uname -m)-linux-$ZIG_VERSION/zig" /usr/local/bin/zig
|
||||
@@ -41,4 +41,3 @@ RUN zig build \
|
||||
-Dcpu=baseline
|
||||
|
||||
RUN ./zig-out/bin/ghostty +version
|
||||
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#--------------------------------------------------------------------
|
||||
# Generate documentation with Doxygen
|
||||
#--------------------------------------------------------------------
|
||||
FROM ubuntu:24.04 AS builder
|
||||
FROM --platform=linux/amd64 archlinux:latest AS builder
|
||||
|
||||
# Build argument for noindex header
|
||||
ARG ADD_NOINDEX_HEADER=false
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN pacman -Syu --noconfirm && \
|
||||
pacman -S --noconfirm \
|
||||
doxygen \
|
||||
graphviz \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
graphviz && \
|
||||
pacman -Scc --noconfirm
|
||||
WORKDIR /ghostty
|
||||
COPY include/ ./include/
|
||||
COPY images/ ./images/
|
||||
COPY dist/doxygen/ ./dist/doxygen/
|
||||
COPY example/ ./example/
|
||||
COPY Doxyfile ./
|
||||
COPY DoxygenLayout.xml ./
|
||||
RUN mkdir -p zig-out/share/ghostty/doc/libghostty
|
||||
RUN doxygen
|
||||
|
||||
|
||||
@@ -6,11 +6,27 @@ server {
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
etag on;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# Remove default server config
|
||||
rm -f /etc/nginx/conf.d/default.conf
|
||||
else
|
||||
cat > /etc/nginx/conf.d/default.conf << 'EOF'
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
etag on;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
exec nginx -g "daemon off;"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# FILES
|
||||
|
||||
_\$XDG_CONFIG_HOME/ghostty/config_
|
||||
_\$XDG_CONFIG_HOME/ghostty/config.ghostty_
|
||||
|
||||
: Location of the default configuration file.
|
||||
|
||||
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_
|
||||
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_
|
||||
|
||||
: **On macOS**, location of the default configuration file. This location takes
|
||||
precedence over the XDG environment locations.
|
||||
|
||||
_\$LOCALAPPDATA/ghostty/config_
|
||||
_\$LOCALAPPDATA/ghostty/config.ghostty_
|
||||
|
||||
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
|
||||
for configuration files.
|
||||
@@ -37,6 +37,19 @@ precedence over the XDG environment locations.
|
||||
|
||||
: **WINDOWS ONLY:** alternate location to search for configuration files.
|
||||
|
||||
**GHOSTTY_LOG**
|
||||
|
||||
: The `GHOSTTY_LOG` environment variable can be used to control which
|
||||
destinations receive logs. Ghostty currently defines two destinations:
|
||||
|
||||
: - `stderr` - logging to `stderr`.
|
||||
: - `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
|
||||
|
||||
: Combine values with a comma to enable multiple destinations. Prefix a
|
||||
destination with `no-` to disable it. Enabling and disabling destinations
|
||||
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
|
||||
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
|
||||
|
||||
# BUGS
|
||||
|
||||
See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# FILES
|
||||
|
||||
_\$XDG_CONFIG_HOME/ghostty/config_
|
||||
_\$XDG_CONFIG_HOME/ghostty/config.ghostty_
|
||||
|
||||
: Location of the default configuration file.
|
||||
|
||||
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_
|
||||
_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_
|
||||
|
||||
: **On macOS**, location of the default configuration file. This location takes
|
||||
precedence over the XDG environment locations.
|
||||
|
||||
_\$LOCALAPPDATA/ghostty/config_
|
||||
_\$LOCALAPPDATA/ghostty/config.ghostty_
|
||||
|
||||
: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched
|
||||
for configuration files.
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
To configure Ghostty, you must use a configuration file. GUI-based configuration
|
||||
is on the roadmap but not yet supported. The configuration file must be placed
|
||||
at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config`
|
||||
at `$XDG_CONFIG_HOME/ghostty/config.ghostty`, which defaults to `~/.config/ghostty/config.ghostty`
|
||||
if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
||||
|
||||
**If you are using macOS, the configuration file can also be placed at
|
||||
`$HOME/Library/Application Support/com.mitchellh.ghostty/config`.** This is the
|
||||
`$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.** This is the
|
||||
default configuration location for macOS. It will be searched before any of the
|
||||
XDG environment locations listed above.
|
||||
|
||||
@@ -73,16 +73,12 @@ the public config files of many Ghostty users for examples and inspiration.
|
||||
## Configuration Errors
|
||||
|
||||
If your configuration file has any errors, Ghostty does its best to ignore
|
||||
them and move on. Configuration errors currently show up in the log. The log
|
||||
is written directly to stderr, so it is up to you to figure out how to access
|
||||
that for your system (for now). On macOS, you can also use the system `log` CLI
|
||||
utility with `log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`.
|
||||
them and move on. Configuration errors will be logged.
|
||||
|
||||
## Debugging Configuration
|
||||
|
||||
You can verify that configuration is being properly loaded by looking at the
|
||||
debug output of Ghostty. Documentation for how to view the debug output is in
|
||||
the "building Ghostty" section at the end of the README.
|
||||
debug output of Ghostty.
|
||||
|
||||
In the debug output, you should see in the first 20 lines or so messages about
|
||||
loading (or not loading) a configuration file, as well as any errors it may have
|
||||
@@ -93,3 +89,34 @@ will fall back to default values for erroneous keys.
|
||||
You can also view the full configuration Ghostty is loading using `ghostty
|
||||
+show-config` from the command-line. Use the `--help` flag to additional options
|
||||
for that command.
|
||||
|
||||
## Logging
|
||||
|
||||
Ghostty can write logs to a number of destinations. On all platforms, logging to
|
||||
`stderr` is available. Depending on the platform and how Ghostty was launched,
|
||||
logs sent to `stderr` may be stored by the system and made available for later
|
||||
retrieval.
|
||||
|
||||
On Linux if Ghostty is launched by the default `systemd` user service, you can use
|
||||
`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`.
|
||||
|
||||
On macOS logging to the macOS unified log is available and enabled by default.
|
||||
--Use the system `log` CLI to view Ghostty's logs: `sudo log stream level debug
|
||||
--predicate 'subsystem=="com.mitchellh.ghostty"'`.
|
||||
|
||||
Ghostty's logging can be configured in two ways. The first is by what
|
||||
optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug`
|
||||
optimizations debug logs will be output to `stderr`. If Ghostty is compiled with
|
||||
any other optimization the debug logs will not be output to `stderr`.
|
||||
|
||||
Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used
|
||||
to control which destinations receive logs. Ghostty currently defines two
|
||||
destinations:
|
||||
|
||||
- `stderr` - logging to `stderr`.
|
||||
- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
|
||||
|
||||
Combine values with a comma to enable multiple destinations. Prefix a
|
||||
destination with `no-` to disable it. Enabling and disabling destinations
|
||||
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
|
||||
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const config = @import("config.zig");
|
||||
const config_x = @import("config.x.zig");
|
||||
const d = config.default;
|
||||
@@ -17,11 +18,25 @@ fn computeWidth(
|
||||
_ = cp;
|
||||
_ = backing;
|
||||
_ = tracking;
|
||||
data.width = @intCast(@min(2, @max(0, data.wcwidth)));
|
||||
|
||||
// This condition is to get the previous behavior of uucode's `wcwidth`,
|
||||
// returning the width of a code point in a grapheme cluster but with the
|
||||
// exception to treat emoji modifiers as width 2 so they can be displayed
|
||||
// in isolation. PRs to follow will take advantage of the new uucode
|
||||
// `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split.
|
||||
if (data.wcwidth_zero_in_grapheme and !data.is_emoji_modifier) {
|
||||
data.width = 0;
|
||||
} else {
|
||||
data.width = @min(2, data.wcwidth_standalone);
|
||||
}
|
||||
}
|
||||
|
||||
const width = config.Extension{
|
||||
.inputs = &.{"wcwidth"},
|
||||
.inputs = &.{
|
||||
"wcwidth_standalone",
|
||||
"wcwidth_zero_in_grapheme",
|
||||
"is_emoji_modifier",
|
||||
},
|
||||
.compute = &computeWidth,
|
||||
.fields = &.{
|
||||
.{ .name = "width", .type = u2 },
|
||||
@@ -41,6 +56,7 @@ fn computeIsSymbol(
|
||||
_ = tracking;
|
||||
const block = data.block;
|
||||
data.is_symbol = data.general_category == .other_private_use or
|
||||
block == .arrows or
|
||||
block == .dingbats or
|
||||
block == .emoticons or
|
||||
block == .miscellaneous_symbols or
|
||||
@@ -74,8 +90,7 @@ pub const tables = [_]config.Table{
|
||||
width.field("width"),
|
||||
d.field("grapheme_break"),
|
||||
is_symbol.field("is_symbol"),
|
||||
d.field("is_emoji_modifier"),
|
||||
d.field("is_emoji_modifier_base"),
|
||||
d.field("is_emoji_vs_base"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const help_strings = @import("help_strings");
|
||||
const helpgen_actions = @import("../../input/helpgen_actions.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
|
||||
@@ -7,10 +7,11 @@ pub fn requireZig(comptime required_zig: []const u8) void {
|
||||
const current_vsn = builtin.zig_version;
|
||||
const required_vsn = std.SemanticVersion.parse(required_zig) catch unreachable;
|
||||
if (current_vsn.major != required_vsn.major or
|
||||
current_vsn.minor != required_vsn.minor)
|
||||
current_vsn.minor != required_vsn.minor or
|
||||
current_vsn.patch < required_vsn.patch)
|
||||
{
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Your Zig version v{} does not meet the required build version of v{}",
|
||||
"Your Zig version v{f} does not meet the required build version of v{f}",
|
||||
.{ current_vsn, required_vsn },
|
||||
));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const assert = std.debug.assert;
|
||||
const apprt = @import("apprt.zig");
|
||||
const font = @import("font/main.zig");
|
||||
const rendererpkg = @import("renderer.zig");
|
||||
const WasmTarget = @import("os/wasm/target.zig").Target;
|
||||
const BuildConfig = @import("build/Config.zig");
|
||||
|
||||
pub const ReleaseChannel = BuildConfig.ReleaseChannel;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const diags = @import("diagnostics.zig");
|
||||
@@ -604,7 +604,7 @@ pub fn parseAutoStruct(
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
pub fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
const info = @typeInfo(T).@"struct";
|
||||
comptime assert(info.layout == .@"packed");
|
||||
|
||||
@@ -1427,7 +1427,12 @@ pub const LineIterator = struct {
|
||||
//
|
||||
// This will also optimize reads down the line as we're
|
||||
// more likely to beworking with buffered data.
|
||||
self.r.fillMore() catch {};
|
||||
//
|
||||
// fillMore asserts that the buffer has available capacity,
|
||||
// so skip this if it's full.
|
||||
if (self.r.bufferedLen() < self.r.buffer.len) {
|
||||
self.r.fillMore() catch {};
|
||||
}
|
||||
|
||||
var writer: std.Io.Writer = .fixed(self.entry[2..]);
|
||||
|
||||
@@ -1590,3 +1595,33 @@ test "LineIterator with CRLF line endings" {
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
test "LineIterator with buffered reader" {
|
||||
const testing = std.testing;
|
||||
var f: std.Io.Reader = .fixed("A\nB = C\n");
|
||||
var buf: [2]u8 = undefined;
|
||||
var r = f.limited(.unlimited, &buf);
|
||||
const reader = &r.interface;
|
||||
|
||||
var iter: LineIterator = .init(reader);
|
||||
try testing.expectEqualStrings("--A", iter.next().?);
|
||||
try testing.expectEqualStrings("--B=C", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
test "LineIterator with buffered and primed reader" {
|
||||
const testing = std.testing;
|
||||
var f: std.Io.Reader = .fixed("A\nB = C\n");
|
||||
var buf: [2]u8 = undefined;
|
||||
var r = f.limited(.unlimited, &buf);
|
||||
const reader = &r.interface;
|
||||
|
||||
try reader.fill(buf.len);
|
||||
|
||||
var iter: LineIterator = .init(reader);
|
||||
try testing.expectEqualStrings("--A", iter.next().?);
|
||||
try testing.expectEqualStrings("--B=C", iter.next().?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ const builtin = @import("builtin");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const help_strings = @import("help_strings");
|
||||
const vaxis = @import("vaxis");
|
||||
|
||||
const framedata = @embedFile("framedata");
|
||||
const framedata = @import("framedata").compressed;
|
||||
|
||||
const vxfw = vaxis.vxfw;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("../build_config.zig");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const args = @import("args.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
@@ -30,9 +30,9 @@ pub const Options = struct {
|
||||
/// this yet.
|
||||
///
|
||||
/// The filepath opened is the default user-specific configuration
|
||||
/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`.
|
||||
/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`.
|
||||
/// On macOS, this may also be located at
|
||||
/// `~/Library/Application Support/com.mitchellh.ghostty/config`.
|
||||
/// `~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.
|
||||
/// On macOS, whichever path exists and is non-empty will be prioritized,
|
||||
/// prioritizing the Application Support directory if neither are
|
||||
/// non-empty.
|
||||
@@ -73,7 +73,7 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
|
||||
defer config.deinit();
|
||||
|
||||
// Find the preferred path.
|
||||
const path = try Config.preferredDefaultFilePath(alloc);
|
||||
const path = try configpkg.preferredDefaultFilePath(alloc);
|
||||
defer alloc.free(path);
|
||||
|
||||
// We don't currently support Windows because we use the exec syscall.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
const std = @import("std");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config/Config.zig");
|
||||
const themepkg = @import("../config/theme.zig");
|
||||
const tui = @import("tui.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const global_state = &@import("../global.zig").state;
|
||||
|
||||
const vaxis = @import("vaxis");
|
||||
@@ -180,7 +178,13 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var theme_config = try Config.default(gpa_alloc);
|
||||
defer theme_config.deinit();
|
||||
for (themes.items) |theme| {
|
||||
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
|
||||
if (!shouldIncludeTheme(opts.color, theme_config)) {
|
||||
continue;
|
||||
}
|
||||
if (opts.path)
|
||||
try stdout.print("{s} ({t}) {s}\n", .{ theme.theme, theme.location, theme.path })
|
||||
else
|
||||
@@ -266,7 +270,7 @@ const Preview = struct {
|
||||
.hex = false,
|
||||
.mode = .normal,
|
||||
.color_scheme = .light,
|
||||
.text_input = .init(allocator, &self.vx.unicode),
|
||||
.text_input = .init(allocator),
|
||||
.theme_filter = theme_filter,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ const DiskCache = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../../os/main.zig").xdg;
|
||||
const TempDir = @import("../../os/main.zig").TempDir;
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const xdg = internal_os.xdg;
|
||||
const TempDir = internal_os.TempDir;
|
||||
const Entry = @import("Entry.zig");
|
||||
|
||||
// 512KB - sufficient for approximately 10k entries
|
||||
@@ -69,7 +70,7 @@ pub fn add(
|
||||
|
||||
// Create cache directory if needed
|
||||
if (std.fs.path.dirname(self.path)) |dir| {
|
||||
std.fs.makeDirAbsolute(dir) catch |err| switch (err) {
|
||||
std.fs.cwd().makePath(dir) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => return err,
|
||||
};
|
||||
@@ -180,13 +181,12 @@ pub fn contains(
|
||||
// Open our file
|
||||
const file = std.fs.openFileAbsolute(
|
||||
self.path,
|
||||
.{ .mode = .read_write },
|
||||
.{},
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return false,
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
try fixupPermissions(file);
|
||||
|
||||
// Read existing entries
|
||||
var entries = try readEntries(alloc, file);
|
||||
@@ -327,55 +327,34 @@ fn readEntries(
|
||||
|
||||
// Supports both standalone hostnames and user@hostname format
|
||||
fn isValidCacheKey(key: []const u8) bool {
|
||||
// 253 + 1 + 64 for user@hostname
|
||||
if (key.len == 0 or key.len > 320) return false;
|
||||
if (key.len == 0) return false;
|
||||
|
||||
// Check for user@hostname format
|
||||
if (std.mem.indexOf(u8, key, "@")) |at_pos| {
|
||||
if (std.mem.indexOfScalar(u8, key, '@')) |at_pos| {
|
||||
const user = key[0..at_pos];
|
||||
const hostname = key[at_pos + 1 ..];
|
||||
return isValidUser(user) and isValidHostname(hostname);
|
||||
return isValidUser(user) and isValidHost(hostname);
|
||||
}
|
||||
|
||||
return isValidHostname(key);
|
||||
return isValidHost(key);
|
||||
}
|
||||
|
||||
// Basic hostname validation - accepts domains and IPs
|
||||
// (including IPv6 in brackets)
|
||||
fn isValidHostname(host: []const u8) bool {
|
||||
if (host.len == 0 or host.len > 253) return false;
|
||||
|
||||
// Handle IPv6 addresses in brackets
|
||||
if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') {
|
||||
const ipv6_part = host[1 .. host.len - 1];
|
||||
if (ipv6_part.len == 0) return false;
|
||||
var has_colon = false;
|
||||
for (ipv6_part) |c| {
|
||||
switch (c) {
|
||||
'a'...'f', 'A'...'F', '0'...'9' => {},
|
||||
':' => has_colon = true,
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
return has_colon;
|
||||
// Checks if a host is a valid hostname or IP address
|
||||
fn isValidHost(host: []const u8) bool {
|
||||
// First check for valid hostnames because this is assumed to be the more
|
||||
// likely ssh host format.
|
||||
if (internal_os.hostname.isValid(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard hostname/domain validation
|
||||
for (host) |c| {
|
||||
switch (c) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// No leading/trailing dots or hyphens, no consecutive dots
|
||||
if (host[0] == '.' or host[0] == '-' or
|
||||
host[host.len - 1] == '.' or host[host.len - 1] == '-')
|
||||
{
|
||||
// We also accept valid IP addresses. In practice, IPv4 addresses are also
|
||||
// considered valid hostnames due to their overlapping syntax, so we can
|
||||
// simplify this check to be IPv6-specific.
|
||||
if (std.net.Address.parseIp6(host, 0)) |_| {
|
||||
return true;
|
||||
} else |_| {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std.mem.indexOf(u8, host, "..") == null;
|
||||
}
|
||||
|
||||
fn isValidUser(user: []const u8) bool {
|
||||
@@ -474,98 +453,73 @@ test "disk cache operations" {
|
||||
);
|
||||
}
|
||||
|
||||
// Tests
|
||||
test "hostname validation - valid cases" {
|
||||
test isValidHost {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidHostname("example.com"));
|
||||
try testing.expect(isValidHostname("sub.example.com"));
|
||||
try testing.expect(isValidHostname("host-name.domain.org"));
|
||||
try testing.expect(isValidHostname("192.168.1.1"));
|
||||
try testing.expect(isValidHostname("a"));
|
||||
try testing.expect(isValidHostname("1"));
|
||||
|
||||
// Valid hostnames
|
||||
try testing.expect(isValidHost("localhost"));
|
||||
try testing.expect(isValidHost("example.com"));
|
||||
try testing.expect(isValidHost("sub.example.com"));
|
||||
|
||||
// IPv4 addresses
|
||||
try testing.expect(isValidHost("127.0.0.1"));
|
||||
try testing.expect(isValidHost("192.168.1.1"));
|
||||
|
||||
// IPv6 addresses
|
||||
try testing.expect(isValidHost("::1"));
|
||||
try testing.expect(isValidHost("2001:db8::1"));
|
||||
try testing.expect(isValidHost("2001:db8:0:1:1:1:1:1"));
|
||||
try testing.expect(!isValidHost("fe80::1%eth0")); // scopes not supported
|
||||
|
||||
// Invalid hosts
|
||||
try testing.expect(!isValidHost(""));
|
||||
try testing.expect(!isValidHost("host\nname"));
|
||||
try testing.expect(!isValidHost(".example.com"));
|
||||
try testing.expect(!isValidHost("host..domain"));
|
||||
try testing.expect(!isValidHost("-hostname"));
|
||||
try testing.expect(!isValidHost("hostname-"));
|
||||
try testing.expect(!isValidHost("host name"));
|
||||
try testing.expect(!isValidHost("host_name"));
|
||||
try testing.expect(!isValidHost("host@domain"));
|
||||
try testing.expect(!isValidHost("host:port"));
|
||||
}
|
||||
|
||||
test "hostname validation - IPv6 addresses" {
|
||||
test isValidUser {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidHostname("[::1]"));
|
||||
try testing.expect(isValidHostname("[2001:db8::1]"));
|
||||
try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported
|
||||
try testing.expect(!isValidHostname("[]")); // Empty IPv6
|
||||
try testing.expect(!isValidHostname("[invalid]")); // No colons
|
||||
}
|
||||
|
||||
test "hostname validation - invalid cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(!isValidHostname(""));
|
||||
try testing.expect(!isValidHostname("host\nname"));
|
||||
try testing.expect(!isValidHostname(".example.com"));
|
||||
try testing.expect(!isValidHostname("example.com."));
|
||||
try testing.expect(!isValidHostname("host..domain"));
|
||||
try testing.expect(!isValidHostname("-hostname"));
|
||||
try testing.expect(!isValidHostname("hostname-"));
|
||||
try testing.expect(!isValidHostname("host name"));
|
||||
try testing.expect(!isValidHostname("host_name"));
|
||||
try testing.expect(!isValidHostname("host@domain"));
|
||||
try testing.expect(!isValidHostname("host:port"));
|
||||
|
||||
// Too long
|
||||
const long_host = "a" ** 254;
|
||||
try testing.expect(!isValidHostname(long_host));
|
||||
}
|
||||
|
||||
test "user validation - valid cases" {
|
||||
const testing = std.testing;
|
||||
// Valid
|
||||
try testing.expect(isValidUser("user"));
|
||||
try testing.expect(isValidUser("deploy"));
|
||||
try testing.expect(isValidUser("test-user"));
|
||||
try testing.expect(isValidUser("user-user"));
|
||||
try testing.expect(isValidUser("user_name"));
|
||||
try testing.expect(isValidUser("user.name"));
|
||||
try testing.expect(isValidUser("user123"));
|
||||
try testing.expect(isValidUser("a"));
|
||||
}
|
||||
|
||||
test "user validation - complex realistic cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidUser("git"));
|
||||
try testing.expect(isValidUser("ubuntu"));
|
||||
try testing.expect(isValidUser("root"));
|
||||
try testing.expect(isValidUser("service.account"));
|
||||
try testing.expect(isValidUser("user-with-dashes"));
|
||||
}
|
||||
|
||||
test "user validation - invalid cases" {
|
||||
const testing = std.testing;
|
||||
// Invalid
|
||||
try testing.expect(!isValidUser(""));
|
||||
try testing.expect(!isValidUser("user name"));
|
||||
try testing.expect(!isValidUser("user@domain"));
|
||||
try testing.expect(!isValidUser("user@example"));
|
||||
try testing.expect(!isValidUser("user:group"));
|
||||
try testing.expect(!isValidUser("user\nname"));
|
||||
|
||||
// Too long
|
||||
const long_user = "a" ** 65;
|
||||
try testing.expect(!isValidUser(long_user));
|
||||
try testing.expect(!isValidUser("a" ** 65)); // too long
|
||||
}
|
||||
|
||||
test "cache key validation - hostname format" {
|
||||
test isValidCacheKey {
|
||||
const testing = std.testing;
|
||||
|
||||
// Valid
|
||||
try testing.expect(isValidCacheKey("example.com"));
|
||||
try testing.expect(isValidCacheKey("sub.example.com"));
|
||||
try testing.expect(isValidCacheKey("192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("[::1]"));
|
||||
try testing.expect(!isValidCacheKey(""));
|
||||
try testing.expect(!isValidCacheKey(".invalid.com"));
|
||||
}
|
||||
|
||||
test "cache key validation - user@hostname format" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidCacheKey("::1"));
|
||||
try testing.expect(isValidCacheKey("user@example.com"));
|
||||
try testing.expect(isValidCacheKey("deploy@prod.server.com"));
|
||||
try testing.expect(isValidCacheKey("test-user@192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("user_name@host.domain.org"));
|
||||
try testing.expect(isValidCacheKey("git@github.com"));
|
||||
try testing.expect(isValidCacheKey("ubuntu@[::1]"));
|
||||
try testing.expect(isValidCacheKey("user@192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("user@::1"));
|
||||
|
||||
// Invalid
|
||||
try testing.expect(!isValidCacheKey(""));
|
||||
try testing.expect(!isValidCacheKey(".example.com"));
|
||||
try testing.expect(!isValidCacheKey("@example.com"));
|
||||
try testing.expect(!isValidCacheKey("user@"));
|
||||
try testing.expect(!isValidCacheKey("user@@host"));
|
||||
try testing.expect(!isValidCacheKey("user@.invalid.com"));
|
||||
try testing.expect(!isValidCacheKey("user@@example"));
|
||||
try testing.expect(!isValidCacheKey("user@.example.com"));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../os/xdg.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
pub const Entry = @import("ssh-cache/Entry.zig");
|
||||
|
||||
@@ -3,7 +3,6 @@ const Allocator = std.mem.Allocator;
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config.zig").Config;
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
pub const Options = struct {
|
||||
/// The path of the config file to validate. If this isn't specified,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const file_load = @import("config/file_load.zig");
|
||||
const formatter = @import("config/formatter.zig");
|
||||
pub const Config = @import("config/Config.zig");
|
||||
pub const conditional = @import("config/conditional.zig");
|
||||
@@ -12,6 +13,7 @@ pub const ConditionalState = conditional.State;
|
||||
pub const FileFormatter = formatter.FileFormatter;
|
||||
pub const entryFormatter = formatter.entryFormatter;
|
||||
pub const formatEntry = formatter.formatEntry;
|
||||
pub const preferredDefaultFilePath = file_load.preferredDefaultFilePath;
|
||||
|
||||
// Field types
|
||||
pub const BoldColor = Config.BoldColor;
|
||||
@@ -29,7 +31,6 @@ pub const Keybinds = Config.Keybinds;
|
||||
pub const MouseShiftCapture = Config.MouseShiftCapture;
|
||||
pub const MouseScrollMultiplier = Config.MouseScrollMultiplier;
|
||||
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
|
||||
pub const OptionAsAlt = Config.OptionAsAlt;
|
||||
pub const RepeatableCodepointMap = Config.RepeatableCodepointMap;
|
||||
pub const RepeatableFontVariation = Config.RepeatableFontVariation;
|
||||
pub const RepeatableString = Config.RepeatableString;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const cli = @import("../cli.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const state = &@import("../global.zig").state;
|
||||
const c = @import("../main_c.zig");
|
||||
|
||||
44
src/config/ClipboardCodepointMap.zig
Normal file
44
src/config/ClipboardCodepointMap.zig
Normal file
@@ -0,0 +1,44 @@
|
||||
/// ClipboardCodepointMap is a map of codepoints to replacement values
|
||||
/// for clipboard operations. When copying text to clipboard, matching
|
||||
/// codepoints will be replaced with their mapped values.
|
||||
const ClipboardCodepointMap = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// To ease our usage later, we map it directly to formatter entries.
|
||||
pub const Entry = @import("../terminal/formatter.zig").CodepointMap;
|
||||
pub const Replacement = Entry.Replacement;
|
||||
|
||||
/// The list of entries. We use a multiarraylist for cache-friendly lookups.
|
||||
///
|
||||
/// Note: we do a linear search because we expect to always have very
|
||||
/// few entries, so the overhead of a binary search is not worth it.
|
||||
list: std.MultiArrayList(Entry) = .{},
|
||||
|
||||
pub fn deinit(self: *ClipboardCodepointMap, alloc: Allocator) void {
|
||||
self.list.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. The given allocator is expected to
|
||||
/// be an arena allocator of some sort since the struct itself
|
||||
/// doesn't support fine-grained deallocation of fields.
|
||||
pub fn clone(self: *const ClipboardCodepointMap, alloc: Allocator) !ClipboardCodepointMap {
|
||||
var list = try self.list.clone(alloc);
|
||||
for (list.items(.replacement)) |*r| switch (r.*) {
|
||||
.string => |s| r.string = try alloc.dupe(u8, s),
|
||||
.codepoint => {}, // no allocation needed
|
||||
};
|
||||
|
||||
return .{ .list = list };
|
||||
}
|
||||
|
||||
/// Add an entry to the map.
|
||||
///
|
||||
/// For conflicting codepoints, entries added later take priority over
|
||||
/// entries added earlier.
|
||||
pub fn add(self: *ClipboardCodepointMap, alloc: Allocator, entry: Entry) !void {
|
||||
assert(entry.range[0] <= entry.range[1]);
|
||||
try self.list.append(alloc, entry);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -193,20 +193,32 @@ test "c_get: background-blur" {
|
||||
|
||||
{
|
||||
c.@"background-blur" = .false;
|
||||
var cval: u8 = undefined;
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(0, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .true;
|
||||
var cval: u8 = undefined;
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(20, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .{ .radius = 42 };
|
||||
var cval: u8 = undefined;
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(42, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .@"macos-glass-regular";
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(-1, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .@"macos-glass-clear";
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(-2, cval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Conditionals in Ghostty configuration are based on a static, typed
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const file_load = @import("file_load.zig");
|
||||
|
||||
/// The path to the configuration that should be opened for editing.
|
||||
///
|
||||
@@ -89,20 +89,16 @@ fn configPath(alloc_arena: Allocator) ![]const u8 {
|
||||
/// Returns a const list of possible paths the main config file could be
|
||||
/// in for the current OS.
|
||||
fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 {
|
||||
var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 2);
|
||||
var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 4);
|
||||
errdefer paths.deinit(alloc_arena);
|
||||
|
||||
if (comptime builtin.os.tag == .macos) {
|
||||
paths.appendAssumeCapacity(try internal_os.macos.appSupportDir(
|
||||
alloc_arena,
|
||||
"config",
|
||||
));
|
||||
paths.appendAssumeCapacity(try file_load.defaultAppSupportPath(alloc_arena));
|
||||
paths.appendAssumeCapacity(try file_load.legacyDefaultAppSupportPath(alloc_arena));
|
||||
}
|
||||
|
||||
paths.appendAssumeCapacity(try internal_os.xdg.config(
|
||||
alloc_arena,
|
||||
.{ .subdir = "ghostty/config" },
|
||||
));
|
||||
paths.appendAssumeCapacity(try file_load.defaultXdgPath(alloc_arena));
|
||||
paths.appendAssumeCapacity(try file_load.legacyDefaultXdgPath(alloc_arena));
|
||||
|
||||
return paths.items;
|
||||
}
|
||||
|
||||
166
src/config/file_load.zig
Normal file
166
src/config/file_load.zig
Normal file
@@ -0,0 +1,166 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.config);
|
||||
|
||||
/// Default path for the XDG home configuration file. Returned value
|
||||
/// must be freed by the caller.
|
||||
pub fn defaultXdgPath(alloc: Allocator) ![]const u8 {
|
||||
return try internal_os.xdg.config(
|
||||
alloc,
|
||||
.{ .subdir = "ghostty/config.ghostty" },
|
||||
);
|
||||
}
|
||||
|
||||
/// Ghostty <1.3.0 default path for the XDG home configuration file.
|
||||
/// Returned value must be freed by the caller.
|
||||
pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 {
|
||||
return try internal_os.xdg.config(
|
||||
alloc,
|
||||
.{ .subdir = "ghostty/config" },
|
||||
);
|
||||
}
|
||||
|
||||
/// Preferred default path for the XDG home configuration file.
|
||||
/// Returned value must be freed by the caller.
|
||||
pub fn preferredXdgPath(alloc: Allocator) ![]const u8 {
|
||||
// If the XDG path exists, use that.
|
||||
const xdg_path = try defaultXdgPath(alloc);
|
||||
if (open(xdg_path)) |f| {
|
||||
f.close();
|
||||
return xdg_path;
|
||||
} else |_| {}
|
||||
|
||||
// Try the legacy path
|
||||
errdefer alloc.free(xdg_path);
|
||||
const legacy_xdg_path = try legacyDefaultXdgPath(alloc);
|
||||
if (open(legacy_xdg_path)) |f| {
|
||||
f.close();
|
||||
alloc.free(xdg_path);
|
||||
return legacy_xdg_path;
|
||||
} else |_| {}
|
||||
|
||||
// Legacy path and XDG path both don't exist. Return the
|
||||
// new one.
|
||||
alloc.free(legacy_xdg_path);
|
||||
return xdg_path;
|
||||
}
|
||||
|
||||
/// Default path for the macOS Application Support configuration file.
|
||||
/// Returned value must be freed by the caller.
|
||||
pub fn defaultAppSupportPath(alloc: Allocator) ![]const u8 {
|
||||
return try internal_os.macos.appSupportDir(alloc, "config.ghostty");
|
||||
}
|
||||
|
||||
/// Ghostty <1.3.0 default path for the macOS Application Support
|
||||
/// configuration file. Returned value must be freed by the caller.
|
||||
pub fn legacyDefaultAppSupportPath(alloc: Allocator) ![]const u8 {
|
||||
return try internal_os.macos.appSupportDir(alloc, "config");
|
||||
}
|
||||
|
||||
/// Preferred default path for the macOS Application Support configuration file.
|
||||
/// Returned value must be freed by the caller.
|
||||
pub fn preferredAppSupportPath(alloc: Allocator) ![]const u8 {
|
||||
// If the app support path exists, use that.
|
||||
const app_support_path = try defaultAppSupportPath(alloc);
|
||||
if (open(app_support_path)) |f| {
|
||||
f.close();
|
||||
return app_support_path;
|
||||
} else |_| {}
|
||||
|
||||
// Try the legacy path
|
||||
errdefer alloc.free(app_support_path);
|
||||
const legacy_app_support_path = try legacyDefaultAppSupportPath(alloc);
|
||||
if (open(legacy_app_support_path)) |f| {
|
||||
f.close();
|
||||
alloc.free(app_support_path);
|
||||
return legacy_app_support_path;
|
||||
} else |_| {}
|
||||
|
||||
// Legacy path and app support path both don't exist. Return the
|
||||
// new one.
|
||||
alloc.free(legacy_app_support_path);
|
||||
return app_support_path;
|
||||
}
|
||||
|
||||
/// Returns the path to the preferred default configuration file.
|
||||
/// This is the file where users should place their configuration.
|
||||
///
|
||||
/// This doesn't create or populate the file with any default
|
||||
/// contents; downstream callers must handle this.
|
||||
///
|
||||
/// The returned value must be freed by the caller.
|
||||
pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 {
|
||||
switch (builtin.os.tag) {
|
||||
.macos => {
|
||||
// macOS prefers the Application Support directory
|
||||
// if it exists.
|
||||
const app_support_path = try preferredAppSupportPath(alloc);
|
||||
const app_support_file = open(app_support_path) catch {
|
||||
// Try the XDG path if it exists
|
||||
const xdg_path = try preferredXdgPath(alloc);
|
||||
const xdg_file = open(xdg_path) catch {
|
||||
// If neither file exists, use app support
|
||||
alloc.free(xdg_path);
|
||||
return app_support_path;
|
||||
};
|
||||
xdg_file.close();
|
||||
alloc.free(app_support_path);
|
||||
return xdg_path;
|
||||
};
|
||||
app_support_file.close();
|
||||
return app_support_path;
|
||||
},
|
||||
|
||||
// All other platforms use XDG only
|
||||
else => return try preferredXdgPath(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
const OpenFileError = error{
|
||||
FileNotFound,
|
||||
FileIsEmpty,
|
||||
FileOpenFailed,
|
||||
NotAFile,
|
||||
};
|
||||
|
||||
/// Opens the file at the given path and returns the file handle
|
||||
/// if it exists and is non-empty. This also constrains the possible
|
||||
/// errors to a smaller set that we can explicitly handle.
|
||||
pub fn open(path: []const u8) OpenFileError!std.fs.File {
|
||||
assert(std.fs.path.isAbsolute(path));
|
||||
|
||||
var file = std.fs.openFileAbsolute(
|
||||
path,
|
||||
.{},
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return OpenFileError.FileNotFound,
|
||||
else => {
|
||||
log.warn("unexpected file open error path={s} err={}", .{
|
||||
path,
|
||||
err,
|
||||
});
|
||||
return OpenFileError.FileOpenFailed;
|
||||
},
|
||||
};
|
||||
errdefer file.close();
|
||||
|
||||
const stat = file.stat() catch |err| {
|
||||
log.warn("error getting file stat path={s} err={}", .{
|
||||
path,
|
||||
err,
|
||||
});
|
||||
return OpenFileError.FileOpenFailed;
|
||||
};
|
||||
switch (stat.kind) {
|
||||
.file => {},
|
||||
else => return OpenFileError.NotAFile,
|
||||
}
|
||||
|
||||
if (stat.size == 0) return OpenFileError.FileIsEmpty;
|
||||
|
||||
return file;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const string = @import("string.zig");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const global_state = &@import("../global.zig").state;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! between threads.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Returns a blocking queue implementation for type T.
|
||||
@@ -73,7 +71,7 @@ pub fn BlockingQueue(
|
||||
not_full_waiters: usize = 0,
|
||||
|
||||
/// Allocate the blocking queue on the heap.
|
||||
pub fn create(alloc: Allocator) !*Self {
|
||||
pub fn create(alloc: Allocator) Allocator.Error!*Self {
|
||||
const ptr = try alloc.create(Self);
|
||||
errdefer alloc.destroy(ptr);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
|
||||
/// An associative data structure used for efficiently storing and
|
||||
/// retrieving values which are able to be recomputed if necessary.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
@@ -91,15 +91,24 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// Append a slice to the buffer. If the buffer cannot fit the
|
||||
/// entire slice then an error will be returned. It is up to the
|
||||
/// caller to rotate the circular buffer if they want to overwrite
|
||||
/// the oldest data.
|
||||
pub fn appendSlice(
|
||||
/// Append a single value to the buffer, assuming there is capacity.
|
||||
pub fn appendAssumeCapacity(self: *Self, v: T) void {
|
||||
assert(!self.full);
|
||||
self.storage[self.head] = v;
|
||||
self.head += 1;
|
||||
if (self.head >= self.storage.len) self.head = 0;
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// Append a slice to the buffer.
|
||||
pub fn appendSliceAssumeCapacity(
|
||||
self: *Self,
|
||||
slice: []const T,
|
||||
) Allocator.Error!void {
|
||||
const storage = self.getPtrSlice(self.len(), slice.len);
|
||||
) void {
|
||||
const storage = self.getPtrSlice(
|
||||
self.len(),
|
||||
slice.len,
|
||||
);
|
||||
fastmem.copy(T, storage[0], slice[0..storage[0].len]);
|
||||
fastmem.copy(T, storage[1], slice[storage[0].len..]);
|
||||
}
|
||||
@@ -456,7 +465,7 @@ test "CircBuf append slice" {
|
||||
var buf = try Buf.init(alloc, 5);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
try buf.appendSlice("hello");
|
||||
buf.appendSliceAssumeCapacity("hello");
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 'h');
|
||||
@@ -486,7 +495,7 @@ test "CircBuf append slice with wrap" {
|
||||
try testing.expect(!buf.full);
|
||||
try testing.expectEqual(@as(usize, 2), buf.len());
|
||||
|
||||
try buf.appendSlice("AB");
|
||||
buf.appendSliceAssumeCapacity("AB");
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Create a HashMap for a key type that can be automatically hashed.
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const BlockingQueue = blocking_queue.BlockingQueue;
|
||||
pub const CacheTable = cache_table.CacheTable;
|
||||
pub const CircBuf = circ_buf.CircBuf;
|
||||
pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
|
||||
pub const MessageData = @import("message_data.zig").MessageData;
|
||||
pub const SegmentedPool = segmented_pool.SegmentedPool;
|
||||
pub const SplitTree = split_tree.SplitTree;
|
||||
|
||||
|
||||
124
src/datastruct/message_data.zig
Normal file
124
src/datastruct/message_data.zig
Normal file
@@ -0,0 +1,124 @@
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Creates a union that can be used to accommodate data that fit within an array,
|
||||
/// are a stable pointer, or require deallocation. This is helpful for thread
|
||||
/// messaging utilities.
|
||||
pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type {
|
||||
return union(enum) {
|
||||
pub const Self = @This();
|
||||
|
||||
pub const Small = struct {
|
||||
pub const Max = small_size;
|
||||
pub const Array = [Max]Elem;
|
||||
pub const Len = std.math.IntFittingRange(0, small_size);
|
||||
data: Array = undefined,
|
||||
len: Len = 0,
|
||||
};
|
||||
|
||||
pub const Alloc = struct {
|
||||
alloc: Allocator,
|
||||
data: []Elem,
|
||||
};
|
||||
|
||||
pub const Stable = []const Elem;
|
||||
|
||||
/// A small write where the data fits into this union size.
|
||||
small: Small,
|
||||
|
||||
/// A stable pointer so we can just pass the slice directly through.
|
||||
/// This is useful i.e. for const data.
|
||||
stable: Stable,
|
||||
|
||||
/// Allocated and must be freed with the provided allocator. This
|
||||
/// should be rarely used.
|
||||
alloc: Alloc,
|
||||
|
||||
/// Initializes the union for a given data type. This will
|
||||
/// attempt to fit into a small value if possible, otherwise
|
||||
/// will allocate and put into alloc.
|
||||
///
|
||||
/// This can't and will never detect stable pointers.
|
||||
pub fn init(alloc: Allocator, data: anytype) !Self {
|
||||
switch (@typeInfo(@TypeOf(data))) {
|
||||
.pointer => |info| {
|
||||
assert(info.size == .slice);
|
||||
assert(info.child == Elem);
|
||||
|
||||
// If it fits in our small request, do that.
|
||||
if (data.len <= Small.Max) {
|
||||
var buf: Small.Array = undefined;
|
||||
@memcpy(buf[0..data.len], data);
|
||||
return Self{
|
||||
.small = .{
|
||||
.data = buf,
|
||||
.len = @intCast(data.len),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, allocate
|
||||
const buf = try alloc.dupe(Elem, data);
|
||||
errdefer alloc.free(buf);
|
||||
return Self{
|
||||
.alloc = .{
|
||||
.alloc = alloc,
|
||||
.data = buf,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
switch (self) {
|
||||
.small, .stable => {},
|
||||
.alloc => |v| v.alloc.free(v.data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a const slice of the data pointed to by this request.
|
||||
pub fn slice(self: *const Self) []const Elem {
|
||||
return switch (self.*) {
|
||||
.small => |*v| v.data[0..v.len],
|
||||
.stable => |v| v,
|
||||
.alloc => |v| v.data,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "MessageData init small" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Data = MessageData(u8, 10);
|
||||
const input = "hello!";
|
||||
const io = try Data.init(alloc, @as([]const u8, input));
|
||||
try testing.expect(io == .small);
|
||||
}
|
||||
|
||||
test "MessageData init alloc" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Data = MessageData(u8, 10);
|
||||
const input = "hello! " ** 100;
|
||||
const io = try Data.init(alloc, @as([]const u8, input));
|
||||
try testing.expect(io == .alloc);
|
||||
io.alloc.alloc.free(io.alloc.data);
|
||||
}
|
||||
|
||||
test "MessageData small fits non-u8 sized data" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const len = 500;
|
||||
const Data = MessageData(u8, len);
|
||||
const input: []const u8 = "X" ** len;
|
||||
const io = try Data.init(alloc, input);
|
||||
try testing.expect(io == .small);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const build_config = @import("../build_config.zig");
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user