mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-27 19:16:27 +00:00
Merge and fix conflict on README
This commit is contained in:
@@ -5,21 +5,16 @@ const App = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("build_config.zig");
|
||||
const apprt = @import("apprt.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const tracy = @import("tracy");
|
||||
const input = @import("input.zig");
|
||||
const configpkg = @import("config.zig");
|
||||
const Config = configpkg.Config;
|
||||
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
|
||||
const renderer = @import("renderer.zig");
|
||||
const font = @import("font/main.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const macos = @import("macos");
|
||||
const objc = @import("objc");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
|
||||
261
src/Surface.zig
261
src/Surface.zig
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
// The required comptime API for any apprt.
|
||||
pub const App = @import("gtk/App.zig");
|
||||
pub const Surface = @import("gtk/Surface.zig");
|
||||
|
||||
@@ -5,18 +5,13 @@ const App = @This();
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
const CoreApp = @import("../../App.zig");
|
||||
|
||||
const Application = @import("class/application.zig").Application;
|
||||
const Surface = @import("Surface.zig");
|
||||
const gtk_version = @import("gtk_version.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@@ -11,6 +11,20 @@ pub const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
pub const blueprint_compiler_help =
|
||||
\\
|
||||
\\When building from a Git checkout, Ghostty requires
|
||||
\\version {f} or newer of `blueprint-compiler` as a
|
||||
\\build-time dependency. Please install it, ensure that it
|
||||
\\is available on your PATH, and then retry building Ghostty.
|
||||
\\See `HACKING.md` for more details.
|
||||
\\
|
||||
\\This message should *not* appear for normal users, who
|
||||
\\should build Ghostty from official release tarballs instead.
|
||||
\\Please consult https://ghostty.org/docs/install/build for
|
||||
\\more information on the recommended build instructions.
|
||||
;
|
||||
|
||||
const adwaita_version = std.SemanticVersion{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
@@ -79,13 +93,9 @@ pub fn main() !void {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {f} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
++ blueprint_compiler_help,
|
||||
.{required_blueprint_version},
|
||||
);
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
@@ -103,13 +113,9 @@ pub fn main() !void {
|
||||
if (version.order(required_blueprint_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` is the wrong version.
|
||||
\\
|
||||
\\Ghostty requires version {f} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
++ blueprint_compiler_help,
|
||||
.{required_blueprint_version},
|
||||
);
|
||||
std.posix.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -144,13 +150,9 @@ pub fn main() !void {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {f} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
++ blueprint_compiler_help,
|
||||
.{required_blueprint_version},
|
||||
);
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
|
||||
@@ -43,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" },
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/// Contains all the logic for putting the Ghostty process and
|
||||
/// each individual surface into its own cgroup.
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_systemd_cgroup);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gio = @import("gio");
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
@@ -3,10 +3,8 @@ const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_dialog);
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Binding = @import("../../../input.zig").Binding;
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const key = @import("../key.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
|
||||
const cimgui = @import("cimgui");
|
||||
const gl = @import("opengl");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
|
||||
@@ -2,15 +2,12 @@ const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
|
||||
const key = @import("../key.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
const DebugWarning = @import("debug_warning.zig").DebugWarning;
|
||||
const InspectorWidget = @import("inspector_widget.zig").InspectorWidget;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
486
src/apprt/gtk/class/search_overlay.zig
Normal file
486
src/apprt/gtk/class/search_overlay.zig
Normal file
@@ -0,0 +1,486 @@
|
||||
const std = @import("std");
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_search_overlay);
|
||||
|
||||
/// The overlay that shows the current size while a surface is resizing.
|
||||
/// This can be used generically to show pretty much anything with a
|
||||
/// disappearing overlay, but we have no other use at this point so it
|
||||
/// is named specifically for what it does.
|
||||
///
|
||||
/// General usage:
|
||||
///
|
||||
/// 1. Add it to an overlay
|
||||
/// 2. Set the label with `setLabel`
|
||||
/// 3. Schedule to show it with `schedule`
|
||||
///
|
||||
/// Set any properties to change the behavior.
|
||||
pub const SearchOverlay = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = adw.Bin;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttySearchOverlay",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const active = struct {
|
||||
pub const name = "active";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.getter = getSearchActive,
|
||||
.setter = setSearchActive,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"search-total" = struct {
|
||||
pub const name = "search-total";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
u64,
|
||||
.{
|
||||
.default = 0,
|
||||
.minimum = 0,
|
||||
.maximum = std.math.maxInt(u64),
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
u64,
|
||||
.{ .getter = getSearchTotal },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"has-search-total" = struct {
|
||||
pub const name = "has-search-total";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{ .getter = getHasSearchTotal },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"search-selected" = struct {
|
||||
pub const name = "search-selected";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
u64,
|
||||
.{
|
||||
.default = 0,
|
||||
.minimum = 0,
|
||||
.maximum = std.math.maxInt(u64),
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
u64,
|
||||
.{ .getter = getSearchSelected },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"has-search-selected" = struct {
|
||||
pub const name = "has-search-selected";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
bool,
|
||||
.{ .getter = getHasSearchSelected },
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"halign-target" = struct {
|
||||
pub const name = "halign-target";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
gtk.Align,
|
||||
.{
|
||||
.default = .end,
|
||||
.accessor = C.privateShallowFieldAccessor("halign_target"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"valign-target" = struct {
|
||||
pub const name = "valign-target";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
gtk.Align,
|
||||
.{
|
||||
.default = .start,
|
||||
.accessor = C.privateShallowFieldAccessor("valign_target"),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
pub const signals = struct {
|
||||
/// Emitted when the search is stopped (e.g., Escape pressed).
|
||||
pub const @"stop-search" = struct {
|
||||
pub const name = "stop-search";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when the search text changes (debounced).
|
||||
pub const @"search-changed" = struct {
|
||||
pub const name = "search-changed";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{?[*:0]const u8},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when navigating to the next match.
|
||||
pub const @"next-match" = struct {
|
||||
pub const name = "next-match";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
|
||||
/// Emitted when navigating to the previous match.
|
||||
pub const @"previous-match" = struct {
|
||||
pub const name = "previous-match";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The search entry widget.
|
||||
search_entry: *gtk.SearchEntry,
|
||||
|
||||
/// True when a search is active, meaning we should show the overlay.
|
||||
active: bool = false,
|
||||
|
||||
/// Total number of search matches (null means unknown/none).
|
||||
search_total: ?usize = null,
|
||||
|
||||
/// Currently selected match index (null means none selected).
|
||||
search_selected: ?usize = null,
|
||||
|
||||
/// Target horizontal alignment for the overlay.
|
||||
halign_target: gtk.Align = .end,
|
||||
|
||||
/// Target vertical alignment for the overlay.
|
||||
valign_target: gtk.Align = .start,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.c) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
}
|
||||
|
||||
/// Grab focus on the search entry and select all text.
|
||||
pub fn grabFocus(self: *Self) void {
|
||||
const priv = self.private();
|
||||
_ = priv.search_entry.as(gtk.Widget).grabFocus();
|
||||
|
||||
// Select all text in the search entry field. -1 is distance from
|
||||
// the end, causing the entire text to be selected.
|
||||
priv.search_entry.as(gtk.Editable).selectRegion(0, -1);
|
||||
}
|
||||
|
||||
// Set active status, and update search on activation
|
||||
fn setSearchActive(self: *Self, active: bool) void {
|
||||
const priv = self.private();
|
||||
if (!priv.active and active) {
|
||||
const text = priv.search_entry.as(gtk.Editable).getText();
|
||||
signals.@"search-changed".impl.emit(self, null, .{text}, null);
|
||||
}
|
||||
priv.active = active;
|
||||
}
|
||||
|
||||
/// Set the total number of search matches.
|
||||
pub fn setSearchTotal(self: *Self, total: ?usize) void {
|
||||
const priv = self.private();
|
||||
const had_total = priv.search_total != null;
|
||||
if (priv.search_total == total) return;
|
||||
priv.search_total = total;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"search-total".impl.param_spec);
|
||||
if (had_total != (total != null)) {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"has-search-total".impl.param_spec);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the currently selected match index.
|
||||
pub fn setSearchSelected(self: *Self, selected: ?usize) void {
|
||||
const priv = self.private();
|
||||
const had_selected = priv.search_selected != null;
|
||||
if (priv.search_selected == selected) return;
|
||||
priv.search_selected = selected;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"search-selected".impl.param_spec);
|
||||
if (had_selected != (selected != null)) {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"has-search-selected".impl.param_spec);
|
||||
}
|
||||
}
|
||||
|
||||
fn getSearchActive(self: *Self) bool {
|
||||
return self.private().active;
|
||||
}
|
||||
|
||||
fn getSearchTotal(self: *Self) u64 {
|
||||
return self.private().search_total orelse 0;
|
||||
}
|
||||
|
||||
fn getHasSearchTotal(self: *Self) bool {
|
||||
return self.private().search_total != null;
|
||||
}
|
||||
|
||||
fn getSearchSelected(self: *Self) u64 {
|
||||
return self.private().search_selected orelse 0;
|
||||
}
|
||||
|
||||
fn getHasSearchSelected(self: *Self) bool {
|
||||
return self.private().search_selected != null;
|
||||
}
|
||||
|
||||
fn closureMatchLabel(
|
||||
_: *Self,
|
||||
has_selected: bool,
|
||||
selected: u64,
|
||||
has_total: bool,
|
||||
total: u64,
|
||||
) callconv(.c) ?[*:0]const u8 {
|
||||
if (!has_total or total == 0) return glib.ext.dupeZ(u8, "0/0");
|
||||
var buf: [32]u8 = undefined;
|
||||
const label = std.fmt.bufPrintZ(&buf, "{}/{}", .{
|
||||
if (has_selected) selected + 1 else 0,
|
||||
total,
|
||||
}) catch return null;
|
||||
return glib.ext.dupeZ(u8, label);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Template callbacks
|
||||
|
||||
fn searchChanged(entry: *gtk.SearchEntry, self: *Self) callconv(.c) void {
|
||||
const text = entry.as(gtk.Editable).getText();
|
||||
signals.@"search-changed".impl.emit(self, null, .{text}, null);
|
||||
}
|
||||
|
||||
// NOTE: The callbacks below use anyopaque for the first parameter
|
||||
// because they're shared with multiple widgets in the template.
|
||||
|
||||
fn stopSearch(_: *anyopaque, self: *Self) callconv(.c) void {
|
||||
signals.@"stop-search".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
fn nextMatch(_: *anyopaque, self: *Self) callconv(.c) void {
|
||||
signals.@"next-match".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
fn previousMatch(_: *anyopaque, self: *Self) callconv(.c) void {
|
||||
signals.@"previous-match".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
fn searchEntryKeyPressed(
|
||||
_: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
_: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
if (keyval == gdk.KEY_Return or keyval == gdk.KEY_KP_Enter) {
|
||||
if (gtk_mods.shift_mask) {
|
||||
signals.@"previous-match".impl.emit(self, null, .{}, null);
|
||||
} else {
|
||||
signals.@"next-match".impl.emit(self, null, .{}, null);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onDragEnd(
|
||||
_: *gtk.GestureDrag,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
// On drag end, we want to move our halign/valign if we crossed
|
||||
// the midpoint on either axis. This lets the search overlay be
|
||||
// moved to different corners of the parent container.
|
||||
|
||||
const priv = self.private();
|
||||
const widget = self.as(gtk.Widget);
|
||||
const parent = widget.getParent() orelse return;
|
||||
|
||||
const parent_width: f64 = @floatFromInt(parent.getAllocatedWidth());
|
||||
const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight());
|
||||
const self_width: f64 = @floatFromInt(widget.getAllocatedWidth());
|
||||
const self_height: f64 = @floatFromInt(widget.getAllocatedHeight());
|
||||
|
||||
const self_x: f64 = if (priv.halign_target == .start) 0 else parent_width - self_width;
|
||||
const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height;
|
||||
|
||||
const new_x = self_x + offset_x + (self_width / 2);
|
||||
const new_y = self_y + offset_y + (self_height / 2);
|
||||
|
||||
const new_halign: gtk.Align = if (new_x > parent_width / 2) .end else .start;
|
||||
const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start;
|
||||
|
||||
var changed = false;
|
||||
if (new_halign != priv.halign_target) {
|
||||
priv.halign_target = new_halign;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"halign-target".impl.param_spec);
|
||||
changed = true;
|
||||
}
|
||||
if (new_valign != priv.valign_target) {
|
||||
priv.valign_target = new_valign;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) self.as(gtk.Widget).queueResize();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
fn dispose(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
_ = priv;
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
_ = priv;
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 2,
|
||||
.name = "search-overlay",
|
||||
}),
|
||||
);
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("search_entry", .{});
|
||||
|
||||
// Template Callbacks
|
||||
class.bindTemplateCallback("stop_search", &stopSearch);
|
||||
class.bindTemplateCallback("search_changed", &searchChanged);
|
||||
class.bindTemplateCallback("match_label_closure", &closureMatchLabel);
|
||||
class.bindTemplateCallback("next_match", &nextMatch);
|
||||
class.bindTemplateCallback("previous_match", &previousMatch);
|
||||
class.bindTemplateCallback("search_entry_key_pressed", &searchEntryKeyPressed);
|
||||
class.bindTemplateCallback("on_drag_end", &onDragEnd);
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.active.impl,
|
||||
properties.@"search-total".impl,
|
||||
properties.@"has-search-total".impl,
|
||||
properties.@"search-selected".impl,
|
||||
properties.@"has-search-selected".impl,
|
||||
properties.@"halign-target".impl,
|
||||
properties.@"valign-target".impl,
|
||||
});
|
||||
|
||||
// Signals
|
||||
signals.@"stop-search".impl.register(.{});
|
||||
signals.@"search-changed".impl.register(.{});
|
||||
signals.@"next-match".impl.register(.{});
|
||||
signals.@"previous-match".impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
@@ -8,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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
@@ -6,7 +6,6 @@ const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const ext = @import("../ext.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -34,6 +34,18 @@ label.url-overlay.right {
|
||||
border-radius: 6px 0px 0px 0px;
|
||||
}
|
||||
|
||||
/*
|
||||
* GhosttySurface search overlay
|
||||
*/
|
||||
.search-overlay {
|
||||
padding: 6px 8px;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
outline-style: solid;
|
||||
outline-color: #555555;
|
||||
outline-width: 1px;
|
||||
}
|
||||
|
||||
/*
|
||||
* GhosttySurface resize overlay
|
||||
*/
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
//! helpers.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
const testing = std.testing;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const testing = std.testing;
|
||||
|
||||
const gio = @import("gio");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const glib = @import("glib");
|
||||
|
||||
94
src/apprt/gtk/ui/1.2/search-overlay.blp
Normal file
94
src/apprt/gtk/ui/1.2/search-overlay.blp
Normal file
@@ -0,0 +1,94 @@
|
||||
using Gtk 4.0;
|
||||
using Gdk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttySearchOverlay: Adw.Bin {
|
||||
visible: bind template.active;
|
||||
halign-target: end;
|
||||
valign-target: start;
|
||||
halign: bind template.halign-target;
|
||||
valign: bind template.valign-target;
|
||||
|
||||
GestureDrag {
|
||||
button: 1;
|
||||
propagation-phase: capture;
|
||||
drag-end => $on_drag_end();
|
||||
}
|
||||
|
||||
Adw.Bin {
|
||||
Box container {
|
||||
styles [
|
||||
"background",
|
||||
"search-overlay",
|
||||
]
|
||||
|
||||
orientation: horizontal;
|
||||
spacing: 6;
|
||||
|
||||
SearchEntry search_entry {
|
||||
placeholder-text: _("Find…");
|
||||
width-chars: 20;
|
||||
hexpand: true;
|
||||
stop-search => $stop_search();
|
||||
search-changed => $search_changed();
|
||||
next-match => $next_match();
|
||||
previous-match => $previous_match();
|
||||
|
||||
EventControllerKey {
|
||||
// We need this so we capture before the SearchEntry.
|
||||
propagation-phase: capture;
|
||||
key-pressed => $search_entry_key_pressed();
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
styles [
|
||||
"dim-label",
|
||||
]
|
||||
|
||||
label: bind $match_label_closure(template.has-search-selected, template.search-selected, template.has-search-total, template.search-total) as <string>;
|
||||
width-chars: 6;
|
||||
xalign: 1.0;
|
||||
}
|
||||
|
||||
Box button_box {
|
||||
orientation: horizontal;
|
||||
spacing: 1;
|
||||
|
||||
styles [
|
||||
"linked",
|
||||
]
|
||||
|
||||
Button prev_button {
|
||||
icon-name: "go-up-symbolic";
|
||||
tooltip-text: _("Previous Match");
|
||||
clicked => $next_match();
|
||||
|
||||
cursor: Gdk.Cursor {
|
||||
name: "pointer";
|
||||
};
|
||||
}
|
||||
|
||||
Button next_button {
|
||||
icon-name: "go-down-symbolic";
|
||||
tooltip-text: _("Next Match");
|
||||
clicked => $previous_match();
|
||||
|
||||
cursor: Gdk.Cursor {
|
||||
name: "pointer";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Button close_button {
|
||||
icon-name: "window-close-symbolic";
|
||||
tooltip-text: _("Close");
|
||||
clicked => $stop_search();
|
||||
|
||||
cursor: Gdk.Cursor {
|
||||
name: "pointer";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,34 @@ Overlay terminal_page {
|
||||
halign: start;
|
||||
has-arrow: false;
|
||||
}
|
||||
|
||||
EventControllerFocus {
|
||||
enter => $focus_enter();
|
||||
leave => $focus_leave();
|
||||
}
|
||||
|
||||
EventControllerKey {
|
||||
key-pressed => $key_pressed();
|
||||
key-released => $key_released();
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
motion => $mouse_motion();
|
||||
leave => $mouse_leave();
|
||||
}
|
||||
|
||||
GestureClick {
|
||||
pressed => $mouse_down();
|
||||
released => $mouse_up();
|
||||
button: 0;
|
||||
}
|
||||
};
|
||||
|
||||
[overlay]
|
||||
@@ -64,6 +92,10 @@ Overlay terminal_page {
|
||||
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
|
||||
transition-type: crossfade;
|
||||
transition-duration: 500;
|
||||
// Revealers take up the full size, we need this to not capture events.
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
Box bell_overlay {
|
||||
styles [
|
||||
@@ -115,12 +147,26 @@ Overlay terminal_page {
|
||||
label: bind template.mouse-hover-url;
|
||||
}
|
||||
|
||||
[overlay]
|
||||
$GhosttySearchOverlay search_overlay {
|
||||
stop-search => $search_stop();
|
||||
search-changed => $search_changed();
|
||||
next-match => $search_next_match();
|
||||
previous-match => $search_previous_match();
|
||||
}
|
||||
|
||||
[overlay]
|
||||
// Apply unfocused-split-fill and unfocused-split-opacity to current surface
|
||||
// this is only applied when a tab has more than one surface
|
||||
Revealer {
|
||||
reveal-child: bind $should_unfocused_split_be_shown(template.focused, template.is-split) as <bool>;
|
||||
transition-duration: 0;
|
||||
// This is all necessary so that the Revealer itself doesn't override
|
||||
// any input events from the other overlays. Namely, if you don't have
|
||||
// these then the search overlay won't get mouse events.
|
||||
can-focus: false;
|
||||
can-target: false;
|
||||
focusable: false;
|
||||
|
||||
DrawingArea {
|
||||
styles [
|
||||
@@ -129,35 +175,6 @@ Overlay terminal_page {
|
||||
}
|
||||
}
|
||||
|
||||
// Event controllers for interactivity
|
||||
EventControllerFocus {
|
||||
enter => $focus_enter();
|
||||
leave => $focus_leave();
|
||||
}
|
||||
|
||||
EventControllerKey {
|
||||
key-pressed => $key_pressed();
|
||||
key-released => $key_released();
|
||||
}
|
||||
|
||||
EventControllerMotion {
|
||||
motion => $mouse_motion();
|
||||
leave => $mouse_leave();
|
||||
}
|
||||
|
||||
EventControllerScroll {
|
||||
scroll => $scroll();
|
||||
scroll-begin => $scroll_begin();
|
||||
scroll-end => $scroll_end();
|
||||
flags: both_axes;
|
||||
}
|
||||
|
||||
GestureClick {
|
||||
pressed => $mouse_down();
|
||||
released => $mouse_up();
|
||||
button: 0;
|
||||
}
|
||||
|
||||
DropTarget drop_target {
|
||||
drop => $drop();
|
||||
actions: copy;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gdk_wayland = @import("gdk_wayland");
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
//! X11 window protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gdk_x11 = @import("gdk_x11");
|
||||
const glib = @import("glib");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
@@ -134,7 +134,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
@@ -166,7 +166,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
@@ -113,7 +113,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
const IsSymbol = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
@@ -90,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;
|
||||
|
||||
|
||||
196
src/benchmark/ScreenClone.zig
Normal file
196
src/benchmark/ScreenClone.zig
Normal file
@@ -0,0 +1,196 @@
|
||||
//! This benchmark tests the performance of the Screen.clone
|
||||
//! function. This is useful because it is one of the primary lock
|
||||
//! holders that impact IO performance when the renderer is active.
|
||||
//! We do this very frequently.
|
||||
const ScreenClone = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const terminalpkg = @import("../terminal/main.zig");
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const Terminal = terminalpkg.Terminal;
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
opts: Options,
|
||||
terminal: Terminal,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The type of codepoint width calculation to use.
|
||||
mode: Mode = .clone,
|
||||
|
||||
/// The size of the terminal. This affects benchmarking when
|
||||
/// dealing with soft line wrapping and the memory impact
|
||||
/// of page sizes.
|
||||
@"terminal-rows": u16 = 80,
|
||||
@"terminal-cols": u16 = 120,
|
||||
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
///
|
||||
/// This will be used to initialize the terminal screen state before
|
||||
/// cloning. This data can switch to alt screen if it wants. The time
|
||||
/// to read this is not part of the benchmark.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Mode = enum {
|
||||
/// The baseline mode copies the screen by value.
|
||||
noop,
|
||||
|
||||
/// Full clone
|
||||
clone,
|
||||
|
||||
/// RenderState rather than a screen clone.
|
||||
render,
|
||||
};
|
||||
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*ScreenClone {
|
||||
const ptr = try alloc.create(ScreenClone);
|
||||
errdefer alloc.destroy(ptr);
|
||||
|
||||
ptr.* = .{
|
||||
.opts = opts,
|
||||
.terminal = try .init(alloc, .{
|
||||
.rows = opts.@"terminal-rows",
|
||||
.cols = opts.@"terminal-cols",
|
||||
}),
|
||||
};
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *ScreenClone, alloc: Allocator) void {
|
||||
self.terminal.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *ScreenClone) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = switch (self.opts.mode) {
|
||||
.noop => stepNoop,
|
||||
.clone => stepClone,
|
||||
.render => stepRender,
|
||||
},
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Always reset our terminal state
|
||||
self.terminal.fullReset();
|
||||
|
||||
// Force a style on every single row, which
|
||||
var s = self.terminal.vtStream();
|
||||
defer s.deinit();
|
||||
s.nextSlice("\x1b[48;2;20;40;60m") catch unreachable;
|
||||
for (0..self.terminal.rows - 1) |_| s.nextSlice("hello\r\n") catch unreachable;
|
||||
s.nextSlice("hello") catch unreachable;
|
||||
|
||||
// Setup our terminal state
|
||||
const data_f: std.fs.File = (options.dataFile(
|
||||
self.opts.data,
|
||||
) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
}) orelse return;
|
||||
|
||||
var stream = self.terminal.vtStream();
|
||||
defer stream.deinit();
|
||||
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = data_f.reader(&read_buf);
|
||||
const r = &f_reader.interface;
|
||||
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.readSliceShort(&buf) catch {
|
||||
log.warn("error reading data file err={?}", .{f_reader.err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
stream.nextSlice(buf[0..n]) catch |err| {
|
||||
log.warn("error processing data file chunk err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// We loop because its so fast that a single benchmark run doesn't
|
||||
// properly capture our speeds.
|
||||
for (0..1000) |_| {
|
||||
const s: terminalpkg.Screen = self.terminal.screens.active.*;
|
||||
std.mem.doNotOptimizeAway(s);
|
||||
}
|
||||
}
|
||||
|
||||
fn stepClone(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// We loop because its so fast that a single benchmark run doesn't
|
||||
// properly capture our speeds.
|
||||
for (0..1000) |_| {
|
||||
const s: *terminalpkg.Screen = self.terminal.screens.active;
|
||||
const copy = s.clone(
|
||||
s.alloc,
|
||||
.{ .viewport = .{} },
|
||||
null,
|
||||
) catch |err| {
|
||||
log.warn("error cloning screen err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
std.mem.doNotOptimizeAway(copy);
|
||||
|
||||
// Note: we purposely do not free memory because we don't want
|
||||
// to benchmark that. We'll free when the benchmark exits.
|
||||
}
|
||||
}
|
||||
|
||||
fn stepRender(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// We do this once out of the loop because a significant slowdown
|
||||
// on the first run is allocation. After that first run, even with
|
||||
// a full rebuild, it is much faster. Let's ignore that first run
|
||||
// slowdown.
|
||||
const alloc = self.terminal.screens.active.alloc;
|
||||
var state: terminalpkg.RenderState = .empty;
|
||||
state.update(alloc, &self.terminal) catch |err| {
|
||||
log.warn("error cloning screen err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
|
||||
// We loop because its so fast that a single benchmark run doesn't
|
||||
// properly capture our speeds.
|
||||
for (0..1000) |_| {
|
||||
// Forces a full rebuild because it thinks our screen changed
|
||||
state.screen = .alternate;
|
||||
state.update(alloc, &self.terminal) catch |err| {
|
||||
log.warn("error cloning screen err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
std.mem.doNotOptimizeAway(state);
|
||||
|
||||
// Note: we purposely do not free memory because we don't want
|
||||
// to benchmark that. We'll free when the benchmark exits.
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
// the benchmark results and... I know writing this that we
|
||||
// aren't currently IO bound.
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
var r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
// aren't currently IO bound.
|
||||
const f = self.data_f orelse return;
|
||||
|
||||
var read_buf: [4096]u8 = undefined;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var f_reader = f.reader(&read_buf);
|
||||
const r = &f_reader.interface;
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
const GhosttyBench = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
|
||||
steps: []*std.Build.Step.Compile,
|
||||
|
||||
@@ -170,11 +170,11 @@ pub const Resource = struct {
|
||||
|
||||
/// Returns true if the dist path exists at build time.
|
||||
pub fn exists(self: *const Resource, b: *std.Build) bool {
|
||||
if (std.fs.accessAbsolute(b.pathFromRoot(self.dist), .{})) {
|
||||
if (b.build_root.handle.access(self.dist, .{})) {
|
||||
// If we have a ".git" directory then we're a git checkout
|
||||
// and we never want to use the dist path. This shouldn't happen
|
||||
// so show a warning to the user.
|
||||
if (std.fs.accessAbsolute(b.pathFromRoot(".git"), .{})) {
|
||||
if (b.build_root.handle.access(".git", .{})) {
|
||||
std.log.warn(
|
||||
"dist resource '{s}' should not be in a git checkout",
|
||||
.{self.dist},
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
const GhosttyFrameData = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const DistResource = @import("GhosttyDist.zig").Resource;
|
||||
|
||||
/// The output path for the compressed framedata zig file
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const UnicodeTables = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
|
||||
/// The exe.
|
||||
props_exe: *std.Build.Step.Compile,
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,7 +9,6 @@ const assert = std.debug.assert;
|
||||
const apprt = @import("apprt.zig");
|
||||
const font = @import("font/main.zig");
|
||||
const rendererpkg = @import("renderer.zig");
|
||||
const WasmTarget = @import("os/wasm/target.zig").Target;
|
||||
const BuildConfig = @import("build/Config.zig");
|
||||
|
||||
pub const ReleaseChannel = BuildConfig.ReleaseChannel;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const diags = @import("diagnostics.zig");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("../build_config.zig");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const args = @import("args.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../os/xdg.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
pub const Entry = @import("ssh-cache/Entry.zig");
|
||||
|
||||
@@ -3,7 +3,6 @@ const Allocator = std.mem.Allocator;
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config.zig").Config;
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
pub const Options = struct {
|
||||
/// The path of the config file to validate. If this isn't specified,
|
||||
|
||||
@@ -1,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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Conditionals in Ghostty configuration are based on a static, typed
|
||||
|
||||
@@ -1,9 +1,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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const string = @import("string.zig");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const global_state = &@import("../global.zig").state;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! between threads.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Returns a blocking queue implementation for type T.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
|
||||
/// An associative data structure used for efficiently storing and
|
||||
/// retrieving values which are able to be recomputed if necessary.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
@@ -91,15 +91,24 @@ pub fn CircBuf(comptime T: type, comptime default: T) type {
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// Append a slice to the buffer. If the buffer cannot fit the
|
||||
/// entire slice then an error will be returned. It is up to the
|
||||
/// caller to rotate the circular buffer if they want to overwrite
|
||||
/// the oldest data.
|
||||
pub fn appendSlice(
|
||||
/// Append a single value to the buffer, assuming there is capacity.
|
||||
pub fn appendAssumeCapacity(self: *Self, v: T) void {
|
||||
assert(!self.full);
|
||||
self.storage[self.head] = v;
|
||||
self.head += 1;
|
||||
if (self.head >= self.storage.len) self.head = 0;
|
||||
self.full = self.head == self.tail;
|
||||
}
|
||||
|
||||
/// Append a slice to the buffer.
|
||||
pub fn appendSliceAssumeCapacity(
|
||||
self: *Self,
|
||||
slice: []const T,
|
||||
) Allocator.Error!void {
|
||||
const storage = self.getPtrSlice(self.len(), slice.len);
|
||||
) void {
|
||||
const storage = self.getPtrSlice(
|
||||
self.len(),
|
||||
slice.len,
|
||||
);
|
||||
fastmem.copy(T, storage[0], slice[0..storage[0].len]);
|
||||
fastmem.copy(T, storage[1], slice[storage[0].len..]);
|
||||
}
|
||||
@@ -456,7 +465,7 @@ test "CircBuf append slice" {
|
||||
var buf = try Buf.init(alloc, 5);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
try buf.appendSlice("hello");
|
||||
buf.appendSliceAssumeCapacity("hello");
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 'h');
|
||||
@@ -486,7 +495,7 @@ test "CircBuf append slice with wrap" {
|
||||
try testing.expect(!buf.full);
|
||||
try testing.expectEqual(@as(usize, 2), buf.len());
|
||||
|
||||
try buf.appendSlice("AB");
|
||||
buf.appendSliceAssumeCapacity("AB");
|
||||
{
|
||||
var it = buf.iterator(.forward);
|
||||
try testing.expect(it.next().?.* == 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Create a HashMap for a key type that can be automatically hashed.
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const BlockingQueue = blocking_queue.BlockingQueue;
|
||||
pub const CacheTable = cache_table.CacheTable;
|
||||
pub const CircBuf = circ_buf.CircBuf;
|
||||
pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
|
||||
pub const MessageData = @import("message_data.zig").MessageData;
|
||||
pub const SegmentedPool = segmented_pool.SegmentedPool;
|
||||
pub const SplitTree = split_tree.SplitTree;
|
||||
|
||||
|
||||
124
src/datastruct/message_data.zig
Normal file
124
src/datastruct/message_data.zig
Normal file
@@ -0,0 +1,124 @@
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Creates a union that can be used to accommodate data that fit within an array,
|
||||
/// are a stable pointer, or require deallocation. This is helpful for thread
|
||||
/// messaging utilities.
|
||||
pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type {
|
||||
return union(enum) {
|
||||
pub const Self = @This();
|
||||
|
||||
pub const Small = struct {
|
||||
pub const Max = small_size;
|
||||
pub const Array = [Max]Elem;
|
||||
pub const Len = std.math.IntFittingRange(0, small_size);
|
||||
data: Array = undefined,
|
||||
len: Len = 0,
|
||||
};
|
||||
|
||||
pub const Alloc = struct {
|
||||
alloc: Allocator,
|
||||
data: []Elem,
|
||||
};
|
||||
|
||||
pub const Stable = []const Elem;
|
||||
|
||||
/// A small write where the data fits into this union size.
|
||||
small: Small,
|
||||
|
||||
/// A stable pointer so we can just pass the slice directly through.
|
||||
/// This is useful i.e. for const data.
|
||||
stable: Stable,
|
||||
|
||||
/// Allocated and must be freed with the provided allocator. This
|
||||
/// should be rarely used.
|
||||
alloc: Alloc,
|
||||
|
||||
/// Initializes the union for a given data type. This will
|
||||
/// attempt to fit into a small value if possible, otherwise
|
||||
/// will allocate and put into alloc.
|
||||
///
|
||||
/// This can't and will never detect stable pointers.
|
||||
pub fn init(alloc: Allocator, data: anytype) !Self {
|
||||
switch (@typeInfo(@TypeOf(data))) {
|
||||
.pointer => |info| {
|
||||
assert(info.size == .slice);
|
||||
assert(info.child == Elem);
|
||||
|
||||
// If it fits in our small request, do that.
|
||||
if (data.len <= Small.Max) {
|
||||
var buf: Small.Array = undefined;
|
||||
@memcpy(buf[0..data.len], data);
|
||||
return Self{
|
||||
.small = .{
|
||||
.data = buf,
|
||||
.len = @intCast(data.len),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, allocate
|
||||
const buf = try alloc.dupe(Elem, data);
|
||||
errdefer alloc.free(buf);
|
||||
return Self{
|
||||
.alloc = .{
|
||||
.alloc = alloc,
|
||||
.data = buf,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
switch (self) {
|
||||
.small, .stable => {},
|
||||
.alloc => |v| v.alloc.free(v.data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a const slice of the data pointed to by this request.
|
||||
pub fn slice(self: *const Self) []const Elem {
|
||||
return switch (self.*) {
|
||||
.small => |*v| v.data[0..v.len],
|
||||
.stable => |v| v,
|
||||
.alloc => |v| v.data,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "MessageData init small" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Data = MessageData(u8, 10);
|
||||
const input = "hello!";
|
||||
const io = try Data.init(alloc, @as([]const u8, input));
|
||||
try testing.expect(io == .small);
|
||||
}
|
||||
|
||||
test "MessageData init alloc" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const Data = MessageData(u8, 10);
|
||||
const input = "hello! " ** 100;
|
||||
const io = try Data.init(alloc, @as([]const u8, input));
|
||||
try testing.expect(io == .alloc);
|
||||
io.alloc.alloc.free(io.alloc.data);
|
||||
}
|
||||
|
||||
test "MessageData small fits non-u8 sized data" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const len = 500;
|
||||
const Data = MessageData(u8, len);
|
||||
const input: []const u8 = "X" ** len;
|
||||
const io = try Data.init(alloc, input);
|
||||
try testing.expect(io == .small);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const build_config = @import("../build_config.zig");
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const std = @import("std");
|
||||
const Config = @import("../config/Config.zig");
|
||||
|
||||
const Template = struct {
|
||||
|
||||
@@ -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?.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user