Merge and fix conflict on README

This commit is contained in:
David
2025-12-03 22:26:07 +01:00
285 changed files with 9550 additions and 3433 deletions

View File

@@ -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);

View File

@@ -17,7 +17,7 @@ pub const Message = apprt.surface.Message;
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 global_state = &@import("global.zig").state;
@@ -26,9 +26,6 @@ const crash = @import("crash/main.zig");
const unicode = @import("unicode/main.zig");
const rendererpkg = @import("renderer.zig");
const termio = @import("termio.zig");
const objc = @import("objc");
const imgui = @import("imgui");
const Pty = @import("pty.zig").Pty;
const font = @import("font/main.zig");
const Command = @import("Command.zig");
const terminal = @import("terminal/main.zig");
@@ -155,6 +152,9 @@ selection_scroll_active: bool = false,
/// the wall clock time that has elapsed between timestamps.
command_timer: ?std.time.Instant = null,
/// Search state
search: ?Search = null,
/// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it
@@ -174,6 +174,26 @@ pub const InputEffect = enum {
closed,
};
/// The search state for the surface.
const Search = struct {
state: terminal.search.Thread,
thread: std.Thread,
pub fn deinit(self: *Search) void {
// Notify the thread to stop
self.state.stop.notify() catch |err| log.err(
"error notifying search thread to stop, may stall err={}",
.{err},
);
// Wait for the OS thread to quit
self.thread.join();
// Now it is safe to deinit the state
self.state.deinit();
}
};
/// Mouse state for the surface.
const Mouse = struct {
/// The last tracked mouse button state by button.
@@ -728,6 +748,9 @@ pub fn init(
}
pub fn deinit(self: *Surface) void {
// Stop search thread
if (self.search) |*s| s.deinit();
// Stop rendering thread
{
self.renderer_thread.stop.notify() catch |err|
@@ -778,6 +801,14 @@ pub fn close(self: *Surface) void {
self.rt_surface.close(self.needsConfirmQuit());
}
/// Returns a mailbox that can be used to send messages to this surface.
inline fn surfaceMailbox(self: *Surface) Mailbox {
return .{
.surface = self,
.app = .{ .rt_app = self.rt_app, .mailbox = &self.app.mailbox },
};
}
/// Forces the surface to render. This is useful for when the surface
/// is in the middle of animation (such as a resize, etc.) or when
/// the render timer is managed manually by the apprt.
@@ -1043,6 +1074,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
log.warn("apprt failed to notify command finish={}", .{err});
};
},
.search_total => |v| {
_ = try self.rt_app.performAction(
.{ .surface = self },
.search_total,
.{ .total = v },
);
},
.search_selected => |v| {
_ = try self.rt_app.performAction(
.{ .surface = self },
.search_selected,
.{ .selected = v },
);
},
}
}
@@ -1301,6 +1348,118 @@ fn reportColorScheme(self: *Surface, force: bool) void {
self.io.queueMessage(.{ .write_stable = output }, .unlocked);
}
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
// IMPORTANT: This function is run on the SEARCH THREAD! It is NOT SAFE
// to access anything other than values that never change on the surface.
// The surface is guaranteed to be valid for the lifetime of the search
// thread.
const self: *Surface = @ptrCast(@alignCast(ud.?));
self.searchCallback_(event) catch |err| {
log.warn("error in search callback err={}", .{err});
};
}
fn searchCallback_(
self: *Surface,
event: terminal.search.Thread.Event,
) !void {
// NOTE: This runs on the search thread.
switch (event) {
.viewport_matches => |matches_unowned| {
var arena: ArenaAllocator = .init(self.alloc);
errdefer arena.deinit();
const alloc = arena.allocator();
const matches = try alloc.dupe(terminal.highlight.Flattened, matches_unowned);
for (matches) |*m| m.* = try m.clone(alloc);
_ = self.renderer_thread.mailbox.push(
.{ .search_viewport_matches = .{
.arena = arena,
.matches = matches,
} },
.forever,
);
try self.renderer_thread.wakeup.notify();
},
.selected_match => |selected_| {
if (selected_) |sel| {
// Copy the flattened match.
var arena: ArenaAllocator = .init(self.alloc);
errdefer arena.deinit();
const alloc = arena.allocator();
const match = try sel.highlight.clone(alloc);
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = .{
.arena = arena,
.match = match,
} },
.forever,
);
// Send the selected index to the surface mailbox
_ = self.surfaceMailbox().push(
.{ .search_selected = sel.idx },
.forever,
);
} else {
// Reset our selected match
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = null },
.forever,
);
// Reset the selected index
_ = self.surfaceMailbox().push(
.{ .search_selected = null },
.forever,
);
}
try self.renderer_thread.wakeup.notify();
},
.total_matches => |total| {
_ = self.surfaceMailbox().push(
.{ .search_total = total },
.forever,
);
},
// When we quit, tell our renderer to reset any search state.
.quit => {
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = null },
.forever,
);
_ = self.renderer_thread.mailbox.push(
.{ .search_viewport_matches = .{
.arena = .init(self.alloc),
.matches = &.{},
} },
.forever,
);
try self.renderer_thread.wakeup.notify();
// Reset search totals in the surface
_ = self.surfaceMailbox().push(
.{ .search_total = null },
.forever,
);
_ = self.surfaceMailbox().push(
.{ .search_selected = null },
.forever,
);
},
// Unhandled, so far.
.complete => {},
}
}
/// Call this when modifiers change. This is safe to call even if modifiers
/// match the previous state.
///
@@ -3305,6 +3464,8 @@ fn mouseReport(
.five => 65,
.six => 66,
.seven => 67,
.eight => 128,
.nine => 129,
else => return, // unsupported
};
}
@@ -4103,7 +4264,7 @@ fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
const cell = pin.rowAndCell().cell;
const link_id = page.lookupHyperlink(cell) orelse return null;
const entry = page.hyperlink_set.get(page.memory, link_id);
return entry.uri.offset.ptr(page.memory)[0..entry.uri.len];
return entry.uri.slice(page.memory);
}
pub fn mousePressureCallback(
@@ -4770,6 +4931,96 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
self.renderer_state.terminal.fullReset();
},
.start_search => {
// To save resources, we don't actually start a search here,
// we just notify the apprt. The real thread will start when
// the first needles are set.
return try self.rt_app.performAction(
.{ .surface = self },
.start_search,
.{ .needle = "" },
);
},
.end_search => {
// We only return that this was performed if we actually
// stopped a search, but we also send the apprt end_search so
// that GUIs can clean up stale stuff.
const performed = self.search != null;
if (self.search) |*s| {
s.deinit();
self.search = null;
}
_ = try self.rt_app.performAction(
.{ .surface = self },
.end_search,
{},
);
return performed;
},
.search => |text| search: {
const s: *Search = if (self.search) |*s| s else init: {
// If we're stopping the search and we had no prior search,
// then there is nothing to do.
if (text.len == 0) return false;
// We need to assign directly to self.search because we need
// a stable pointer back to the thread state.
self.search = .{
.state = try .init(self.alloc, .{
.mutex = self.renderer_state.mutex,
.terminal = self.renderer_state.terminal,
.event_cb = &searchCallback,
.event_userdata = self,
}),
.thread = undefined,
};
const s: *Search = &self.search.?;
errdefer s.state.deinit();
s.thread = try .spawn(
.{},
terminal.search.Thread.threadMain,
.{&s.state},
);
s.thread.setName("search") catch {};
break :init s;
};
// Zero-length text means stop searching.
if (text.len == 0) {
s.deinit();
self.search = null;
break :search;
}
_ = s.state.mailbox.push(
.{ .change_needle = try .init(
self.alloc,
text,
) },
.forever,
);
s.state.wakeup.notify() catch {};
},
.navigate_search => |nav| {
const s: *Search = if (self.search) |*s| s else return false;
_ = s.state.mailbox.push(
.{ .select = switch (nav) {
.next => .next,
.previous => .prev,
} },
.forever,
);
s.state.wakeup.notify() catch {};
},
.copy_to_clipboard => |format| {
// We can read from the renderer state without holding
// the lock because only we will write to this field.

View File

@@ -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");

View File

@@ -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");
@@ -301,6 +301,18 @@ 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,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@@ -358,6 +370,10 @@ pub const Action = union(Key) {
progress_report,
show_on_screen_keyboard,
command_finished,
start_search,
end_search,
search_total,
search_selected,
};
/// Sync with: ghostty_action_u
@@ -770,3 +786,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,
};
}
};

View File

@@ -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");

View File

@@ -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");

View File

@@ -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);

View File

@@ -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,

View File

@@ -43,6 +43,7 @@ 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" },

View File

@@ -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);

View File

@@ -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");
@@ -9,7 +8,6 @@ const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const cgroup = @import("../cgroup.zig");
@@ -729,6 +727,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,
@@ -2339,6 +2342,34 @@ const Action = struct {
}
}
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,

View File

@@ -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");

View File

@@ -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);

View File

@@ -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");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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");

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");

View File

@@ -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;

View File

@@ -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");

View 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;
};
};

View File

@@ -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,17 +7,11 @@ 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;

View 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 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");
@@ -551,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,
@@ -1465,6 +1467,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;
@@ -1949,6 +1955,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,
@@ -3168,6 +3197,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;
@@ -3182,6 +3240,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),
@@ -3201,6 +3260,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", .{});
@@ -3238,6 +3298,10 @@ pub const Surface = extern struct {
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, &.{
@@ -3369,12 +3433,16 @@ const Clipboard = struct {
// 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",
"text/plain;charset=utf-8",
"text/plain",
};
var text_providers: [text_provider_atoms.len]*gdk.ContentProvider = undefined;
for (text_provider_atoms, 0..) |atom, j| {

View File

@@ -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");

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
*/

View File

@@ -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");

View File

@@ -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");

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const build_options = @import("build_options");
const gdk = @import("gdk");
const glib = @import("glib");

View 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";
};
}
}
}
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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");

View File

@@ -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

View File

@@ -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
@@ -107,6 +107,12 @@ pub const Message = union(enum) {
/// 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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,7 +89,7 @@ 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 read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;
@@ -117,7 +116,7 @@ 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 read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;

View 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.
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -8,6 +8,7 @@ const cli = @import("../cli.zig");
pub const Action = enum {
@"codepoint-width",
@"grapheme-break",
@"screen-clone",
@"terminal-parser",
@"terminal-stream",
@"is-symbol",
@@ -22,6 +23,7 @@ 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"),

View File

@@ -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");

View File

@@ -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,

View File

@@ -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},

View File

@@ -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

View File

@@ -3,11 +3,7 @@ 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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -1,7 +1,6 @@
const UnicodeTables = @This();
const std = @import("std");
const Config = @import("Config.zig");
/// The exe.
props_exe: *std.Build.Step.Compile,

View File

@@ -19,23 +19,23 @@ fn computeWidth(
_ = backing;
_ = tracking;
// Emoji modifiers are technically width 0 because they're joining
// points. But we handle joining via grapheme break and don't use width
// there. If a emoji modifier is standalone, we want it to take up
// two columns.
if (data.is_emoji_modifier) {
assert(data.wcwidth == 0);
data.wcwidth = 2;
return;
// 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);
}
data.width = @intCast(@min(2, @max(0, data.wcwidth)));
}
const width = config.Extension{
.inputs = &.{
"wcwidth_standalone",
"wcwidth_zero_in_grapheme",
"is_emoji_modifier",
"wcwidth",
},
.compute = &computeWidth,
.fields = &.{
@@ -90,10 +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_text"),
d.field("is_emoji_vs_emoji"),
d.field("is_emoji_vs_base"),
},
},
};

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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");

View File

@@ -3,7 +3,6 @@ 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 = @import("framedata").compressed;

View File

@@ -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");

View File

@@ -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;

View File

@@ -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

View File

@@ -5,7 +5,7 @@ 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 internal_os = @import("../../os/main.zig");
const xdg = internal_os.xdg;

View File

@@ -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");

View File

@@ -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,

View File

@@ -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");

View File

@@ -4,7 +4,7 @@
const ClipboardCodepointMap = @This();
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
// To ease our usage later, we map it directly to formatter entries.

View File

@@ -13,7 +13,7 @@ const Config = @This();
const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("../global.zig").state;
@@ -29,8 +29,6 @@ const formatterpkg = @import("formatter.zig");
const themepkg = @import("theme.zig");
const url = @import("url.zig");
const Key = @import("key.zig").Key;
const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig");
const MetricModifier = fontpkg.Metrics.Modifier;
const help_strings = @import("help_strings");
pub const Command = @import("command.zig").Command;
@@ -978,6 +976,35 @@ palette: Palette = .{},
/// Available since: 1.1.0
@"split-divider-color": ?Color = null,
/// The foreground and background color for search matches. This only applies
/// to non-focused search matches, also known as candidate matches.
///
/// Valid values:
///
/// - Hex (`#RRGGBB` or `RRGGBB`)
/// - Named X11 color
/// - "cell-foreground" to match the cell foreground color
/// - "cell-background" to match the cell background color
///
/// The default value is black text on a golden yellow background.
@"search-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } },
@"search-background": TerminalColor = .{ .color = .{ .r = 0xFF, .g = 0xE0, .b = 0x82 } },
/// The foreground and background color for the currently selected search match.
/// This is the focused match that will be jumped to when using next/previous
/// search navigation.
///
/// Valid values:
///
/// - Hex (`#RRGGBB` or `RRGGBB`)
/// - Named X11 color
/// - "cell-foreground" to match the cell foreground color
/// - "cell-background" to match the cell background color
///
/// The default value is black text on a soft peach background.
@"search-selected-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } },
@"search-selected-background": TerminalColor = .{ .color = .{ .r = 0xF2, .g = 0xA5, .b = 0x7E } },
/// The command to run, usually a shell. If this is not an absolute path, it'll
/// be looked up in the `PATH`. If this is not set, a default will be looked up
/// from your system. The rules for the default lookup are:
@@ -1765,7 +1792,7 @@ keybind: Keybinds = .{},
/// Note: any font available on the system may be used, this font is not
/// required to be a fixed-width font.
///
/// Available since: 1.1.0 (on GTK)
/// Available since: 1.0.0 on macOS, 1.1.0 on GTK
@"window-title-font-family": ?[:0]const u8 = null,
/// The text that will be displayed in the subtitle of the window. Valid values:
@@ -5203,6 +5230,7 @@ pub const ColorList = struct {
) Allocator.Error!Self {
return .{
.colors = try self.colors.clone(alloc),
.colors_c = try self.colors_c.clone(alloc),
};
}
@@ -5281,6 +5309,26 @@ pub const ColorList = struct {
try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer));
try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.written());
}
test "clone" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var source: Self = .{};
try source.parseCLI(alloc, "#ff0000,#00ff00,#0000ff");
const cloned = try source.clone(alloc);
try testing.expect(source.equal(cloned));
try testing.expectEqual(source.colors_c.items.len, cloned.colors_c.items.len);
for (source.colors_c.items, cloned.colors_c.items) |src_c, clone_c| {
try testing.expectEqual(src_c.r, clone_c.r);
try testing.expectEqual(src_c.g, clone_c.g);
try testing.expectEqual(src_c.b, clone_c.b);
}
}
};
/// Palette is the 256 color palette for 256-color mode. This is still
@@ -6050,6 +6098,20 @@ pub const Keybinds = struct {
.{ .jump_to_prompt = 1 },
);
// Search
try self.set.putFlags(
alloc,
.{ .key = .{ .unicode = 'f' }, .mods = .{ .ctrl = true, .shift = true } },
.start_search,
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .escape } },
.end_search,
.{ .performable = true },
);
// Inspector, matching Chromium
try self.set.put(
alloc,
@@ -6353,6 +6415,38 @@ pub const Keybinds = struct {
.{ .jump_to_prompt = 1 },
);
// Search
try self.set.putFlags(
alloc,
.{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true } },
.start_search,
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .shift = true } },
.end_search,
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .escape } },
.end_search,
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .unicode = 'g' }, .mods = .{ .super = true } },
.{ .navigate_search = .next },
.{ .performable = true },
);
try self.set.putFlags(
alloc,
.{ .key = .{ .unicode = 'g' }, .mods = .{ .super = true, .shift = true } },
.{ .navigate_search = .previous },
.{ .performable = true },
);
// Inspector, matching Chromium
try self.set.put(
alloc,

View File

@@ -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");

View File

@@ -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

View File

@@ -1,9 +1,8 @@
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.

View File

@@ -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 internal_os = @import("../os/main.zig");

View 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");

View File

@@ -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;

View File

@@ -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");

View File

@@ -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.

View File

@@ -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.

View 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 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);

View 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;
/// Create a HashMap for a key type that can be automatically hashed.

View File

@@ -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;

View 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);
}

View 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 testing = std.testing;

View File

@@ -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;

View File

@@ -3,6 +3,7 @@ const std = @import("std");
const Config = @import("../config/Config.zig");
const Action = @import("../cli.zig").ghostty.Action;
const help_strings = @import("help_strings");
/// A fish completions configuration that contains all the available commands
/// and options.
@@ -81,6 +82,15 @@ fn writeCompletions(writer: *std.Io.Writer) !void {
else => {},
}
}
if (@hasDecl(help_strings.Config, field.name)) {
const help = @field(help_strings.Config, field.name);
const desc = getDescription(help);
try writer.writeAll(" -d \"");
try writer.writeAll(desc);
try writer.writeAll("\"");
}
try writer.writeAll("\n");
}
@@ -143,3 +153,54 @@ fn writeCompletions(writer: *std.Io.Writer) !void {
}
}
}
fn getDescription(comptime help: []const u8) []const u8 {
var out: [help.len * 2]u8 = undefined;
var len: usize = 0;
var prev_was_space = false;
for (help, 0..) |c, i| {
switch (c) {
'.' => {
out[len] = '.';
len += 1;
if (i + 1 >= help.len) break;
const next = help[i + 1];
if (next == ' ' or next == '\n') break;
},
'\n' => {
if (!prev_was_space and len > 0) {
out[len] = ' ';
len += 1;
prev_was_space = true;
}
},
'"' => {
out[len] = '\\';
out[len + 1] = '"';
len += 2;
prev_was_space = false;
},
else => {
out[len] = c;
len += 1;
prev_was_space = (c == ' ');
},
}
}
return out[0..len];
}
test "getDescription" {
const testing = std.testing;
const input = "First sentence with \"quotes\"\nand newlines. Second sentence.";
const expected = "First sentence with \\\"quotes\\\" and newlines.";
comptime {
const result = getDescription(input);
try testing.expectEqualStrings(expected, result);
}
}

View File

@@ -1,4 +1,3 @@
const std = @import("std");
const Config = @import("../config/Config.zig");
const Template = struct {

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
/// Same as @memmove but prefers libc memmove if it is
/// available because it is generally much faster?.

View File

@@ -16,7 +16,7 @@
const Atlas = @This();
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const testing = std.testing;
const fastmem = @import("../fastmem.zig");

View File

@@ -4,7 +4,7 @@
const CodepointMap = @This();
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const discovery = @import("discovery.zig");

View File

@@ -16,7 +16,6 @@
const Collection = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const config = @import("../config.zig");
const comparison = @import("../datastruct/comparison.zig");

View File

@@ -7,7 +7,6 @@
const DeferredFace = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const fontconfig = @import("fontconfig");
const macos = @import("macos");

View File

@@ -1,6 +1,7 @@
const Metrics = @This();
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
/// Recommended cell width and height for a monospace grid using this font.
cell_width: u32,
@@ -47,8 +48,9 @@ face_width: f64,
/// The unrounded face height, used in scaling calculations.
face_height: f64,
/// The vertical bearing of face within the pixel-rounded
/// and possibly height-adjusted cell
/// The offset from the bottom of the cell to the bottom
/// of the face's bounding box, based on the rounded and
/// potentially adjusted cell height.
face_y: f64,
/// Minimum acceptable values for some fields to prevent modifiers
@@ -223,25 +225,68 @@ pub const FaceMetrics = struct {
///
/// For any nullable options that are not provided, estimates will be used.
pub fn calc(face: FaceMetrics) Metrics {
// We use the ceiling of the provided cell width and height to ensure
// that the cell is large enough for the provided size, since we cast
// it to an integer later.
// These are the unrounded advance width and line height values,
// which are retained separately from the rounded cell width and
// height values (below), for calculations that need to know how
// much error there is between the design dimensions of the font
// and the pixel dimensions of our cells.
const face_width = face.cell_width;
const face_height = face.lineHeight();
const cell_width = @ceil(face_width);
const cell_height = @ceil(face_height);
// The cell width and height values need to be integers since they
// represent pixel dimensions of the grid cells in the terminal.
//
// We use @round for the cell width to limit the difference from
// the "true" width value to no more than 0.5px. This is a better
// approximation of the authorial intent of the font than ceiling
// would be, and makes the apparent spacing match better between
// low and high DPI displays.
//
// This does mean that it's possible for a glyph to overflow the
// edge of the cell by a pixel if it has no side bearings, but in
// reality such glyphs are generally meant to connect to adjacent
// glyphs in some way so it's not really an issue.
//
// The same is true for the height. Some fonts are poorly authored
// and have a descender on a normal glyph that extends right up to
// the descent value of the face, and this can result in the glyph
// overflowing the bottom of the cell by a pixel, which isn't good
// but if we try to prevent it by increasing the cell height then
// we get line heights that are too large for most users and even
// more inconsistent across DPIs.
//
// Users who experience such cell-height overflows should:
//
// 1. Nag the font author to either redesign the glyph to not go
// so low, or else adjust the descent value in the metadata.
//
// 2. Add an `adjust-cell-height` entry to their config to give
// the cell enough room for the glyph.
const cell_width = @round(face_width);
const cell_height = @round(face_height);
// We split our line gap in two parts, and put half of it on the top
// of the cell and the other half on the bottom, so that our text never
// bumps up against either edge of the cell vertically.
const half_line_gap = face.line_gap / 2;
// Unlike all our other metrics, `cell_baseline` is relative to the
// BOTTOM of the cell.
// NOTE: Unlike all our other metrics, `cell_baseline` is
// relative to the BOTTOM of the cell rather than the top.
const face_baseline = half_line_gap - face.descent;
const cell_baseline = @round(face_baseline);
// We calculate the baseline by trying to center the face vertically
// in the pixel-rounded cell height, so that before rounding it will
// be an even distance from the top and bottom of the cell, meaning
// it either sticks out the same amount or is inset the same amount,
// depending on whether the cell height was rounded up or down from
// the line height. We do this by adding half the difference between
// the cell height and the face height.
const cell_baseline = @round(face_baseline - (cell_height - face_height) / 2);
// We keep track of the vertical bearing of the face in the cell
// We keep track of the offset from the bottom of the cell
// to the bottom of the face's "true" bounding box, which at
// this point, since nothing has been scaled yet, is equivalent
// to the offset between the baseline we draw at (cell_baseline)
// and the one the font wants (face_baseline).
const face_y = cell_baseline - face_baseline;
// We calculate a top_to_baseline to make following calculations simpler.
@@ -311,29 +356,48 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
// here is to center the baseline so that text is vertically
// centered in the cell.
if (comptime tag == .cell_height) {
// We split the difference in half because we want to
// center the baseline in the cell. If the difference
// is odd, one more pixel is added/removed on top than
// on the bottom.
if (new > original) {
const diff = new - original;
const diff_bottom = diff / 2;
const diff_top = diff - diff_bottom;
self.face_y += @floatFromInt(diff_bottom);
self.cell_baseline +|= diff_bottom;
self.underline_position +|= diff_top;
self.strikethrough_position +|= diff_top;
self.overline_position +|= @as(i32, @intCast(diff_top));
} else {
const diff = original - new;
const diff_bottom = diff / 2;
const diff_top = diff - diff_bottom;
self.face_y -= @floatFromInt(diff_bottom);
self.cell_baseline -|= diff_bottom;
self.underline_position -|= diff_top;
self.strikethrough_position -|= diff_top;
self.overline_position -|= @as(i32, @intCast(diff_top));
}
const original_f64: f64 = @floatFromInt(original);
const new_f64: f64 = @floatFromInt(new);
const diff = new_f64 - original_f64;
const half_diff = diff / 2.0;
// If the diff is even, the number of pixels we add
// will be the same for the top and the bottom, but
// if the diff is odd then we want to add the extra
// pixel to the edge of the cell that needs it most.
//
// How much the edge "needs it" depends on whether
// the face is higher or lower than it should be to
// be perfectly centered in the cell.
//
// If the face were perfectly centered then face_y
// would be equal to half of the difference between
// the cell height and the face height.
const position_with_respect_to_center =
self.face_y - (original_f64 - self.face_height) / 2;
const diff_top, const diff_bottom =
if (position_with_respect_to_center > 0)
// The baseline is higher than it should be, so we
// add the extra to the top, or if it's a negative
// diff it gets added to the bottom because of how
// floor and ceil work.
.{ @ceil(half_diff), @floor(half_diff) }
else
// The baseline is lower than it should be, so we
// add the extra to the bottom, or vice versa for
// negative diffs.
.{ @floor(half_diff), @ceil(half_diff) };
// The cell baseline and face_y values are relative to the
// bottom of the cell so we add the bottom diff to them.
addFloatToInt(&self.cell_baseline, diff_bottom);
self.face_y += diff_bottom;
// These are all relative to the top of the cell.
addFloatToInt(&self.underline_position, diff_top);
addFloatToInt(&self.strikethrough_position, diff_top);
self.overline_position +|= @as(i32, @intFromFloat(diff_top));
}
},
inline .icon_height => {
@@ -351,6 +415,21 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
self.clamp();
}
/// Helper function for adding an f64 to a u32.
///
/// Performs saturating addition or subtraction
/// depending on the sign of the provided float.
///
/// The f64 is asserted to have an integer value.
inline fn addFloatToInt(int: *u32, float: f64) void {
assert(@floor(float) == float);
int.* =
if (float >= 0.0)
int.* +| @as(u32, @intFromFloat(float))
else
int.* -| @as(u32, @intFromFloat(-float));
}
/// Clamp all metrics to their allowable range.
fn clamp(self: *Metrics) void {
inline for (std.meta.fields(Metrics)) |field| {
@@ -570,7 +649,9 @@ test "Metrics: adjust cell height smaller" {
defer set.deinit(alloc);
// We choose numbers such that the subtracted number of pixels is odd,
// as that's the case that could most easily have off-by-one errors.
// Here we're removing 25 pixels: 12 on the bottom, 13 on top.
// Here we're removing 25 pixels: 13 on the bottom, 12 on top, split
// that way because we're simulating a face that's 0.33px higher than
// it "should" be (due to rounding).
try set.put(alloc, .cell_height, .{ .percent = 0.75 });
var m: Metrics = init();
@@ -580,14 +661,15 @@ test "Metrics: adjust cell height smaller" {
m.strikethrough_position = 30;
m.overline_position = 0;
m.cell_height = 100;
m.face_height = 99.67;
m.cursor_height = 100;
m.apply(set);
try testing.expectEqual(-11.67, m.face_y);
try testing.expectEqual(-12.67, m.face_y);
try testing.expectEqual(@as(u32, 75), m.cell_height);
try testing.expectEqual(@as(u32, 38), m.cell_baseline);
try testing.expectEqual(@as(u32, 42), m.underline_position);
try testing.expectEqual(@as(u32, 17), m.strikethrough_position);
try testing.expectEqual(@as(i32, -13), m.overline_position);
try testing.expectEqual(@as(u32, 37), m.cell_baseline);
try testing.expectEqual(@as(u32, 43), m.underline_position);
try testing.expectEqual(@as(u32, 18), m.strikethrough_position);
try testing.expectEqual(@as(i32, -12), m.overline_position);
// Cursor height is separate from cell height and does not follow it.
try testing.expectEqual(@as(u32, 100), m.cursor_height);
}
@@ -600,7 +682,9 @@ test "Metrics: adjust cell height larger" {
defer set.deinit(alloc);
// We choose numbers such that the added number of pixels is odd,
// as that's the case that could most easily have off-by-one errors.
// Here we're adding 75 pixels: 37 on the bottom, 38 on top.
// Here we're adding 75 pixels: 37 on the bottom, 38 on top, split
// that way because we're simulating a face that's 0.33px higher
// than it "should" be (due to rounding).
try set.put(alloc, .cell_height, .{ .percent = 1.75 });
var m: Metrics = init();
@@ -610,6 +694,7 @@ test "Metrics: adjust cell height larger" {
m.strikethrough_position = 30;
m.overline_position = 0;
m.cell_height = 100;
m.face_height = 99.67;
m.cursor_height = 100;
m.apply(set);
try testing.expectEqual(37.33, m.face_y);

View File

@@ -19,7 +19,7 @@
const SharedGrid = @This();
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const renderer = @import("../renderer.zig");
const font = @import("main.zig");

View File

@@ -11,7 +11,7 @@ const SharedGridSet = @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 ArenaAllocator = std.heap.ArenaAllocator;
const font = @import("main.zig");

View File

@@ -1,7 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const fontconfig = @import("fontconfig");
const macos = @import("macos");
const opentype = @import("opentype.zig");
@@ -845,15 +844,20 @@ pub const CoreText = struct {
// limitation because we may have used that to filter but we
// don't want it anymore because it'll restrict the characters
// available.
//const desc = self.list.getValueAtIndex(macos.text.FontDescriptor, self.i);
const desc = desc: {
const original = self.list[self.i];
// For some reason simply copying the attributes and recreating
// the descriptor removes the charset restriction. This is tested.
const attrs = original.copyAttributes();
// We create a copy, overwriting the character set attribute.
const attrs = try macos.foundation.MutableDictionary.create(0);
defer attrs.release();
break :desc try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
attrs.setValue(
macos.text.FontAttribute.character_set.key(),
macos.c.kCFNull,
);
break :desc try macos.text.FontDescriptor.createCopyWithAttributes(
self.list[self.i],
@ptrCast(attrs),
);
};
defer desc.release();

View File

@@ -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 macos = @import("macos");
const harfbuzz = @import("harfbuzz");
@@ -367,9 +367,16 @@ pub const Face = struct {
// We don't do this if the glyph has a stretch constraint,
// since in that case the position was already calculated with the
// new cell width in mind.
if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) {
if (constraint.size != .stretch) {
// We add half the difference to re-center.
x += (cell_width - metrics.face_width) / 2;
const dx = (cell_width - metrics.face_width) / 2;
x += dx;
if (dx < 0) {
// For negative diff (cell narrower than advance), we remove the
// integer part and only keep the fractional adjustment needed
// for consistent subpixel positioning.
x -= @trunc(dx);
}
}
// If this is a bitmap glyph, it will always render as full pixels,

View File

@@ -9,14 +9,13 @@ const builtin = @import("builtin");
const freetype = @import("freetype");
const harfbuzz = @import("harfbuzz");
const stb = @import("../../stb/main.zig");
const assert = std.debug.assert;
const assert = @import("../../quirks.zig").inlineAssert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Glyph = font.Glyph;
const Library = font.Library;
const opentype = @import("../opentype.zig");
const fastmem = @import("../../fastmem.zig");
const quirks = @import("../../quirks.zig");
const config = @import("../../config.zig");
@@ -376,11 +375,15 @@ pub const Face = struct {
// If we're gonna be rendering this glyph in monochrome,
// then we should use the monochrome hinter as well, or
// else it won't look very good at all.
.target_mono = self.load_flags.monochrome,
// Otherwise we select hinter based on the `light` flag.
.target_normal = !self.load_flags.light and !self.load_flags.monochrome,
.target_light = self.load_flags.light and !self.load_flags.monochrome,
//
// Otherwise if the user asked for light hinting we
// use that, otherwise we just use the normal target.
.target = if (self.load_flags.monochrome)
.mono
else if (self.load_flags.light)
.light
else
.normal,
// NO_SVG set to true because we don't currently support rendering
// SVG glyphs under FreeType, since that requires bundling another

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("../../quirks.zig").inlineAssert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const js = @import("zig-js");

View File

@@ -2,7 +2,6 @@
//! library implementation(s) require per-process.
const std = @import("std");
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const options = @import("main.zig").options;
const freetype = @import("freetype");
const font = @import("main.zig");

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const assert = std.debug.assert;
const sfnt = @import("sfnt.zig");
/// Font Header Table

Some files were not shown because too many files have changed in this diff Show More