From 892dc2789657c4161137af191b49bcb7723ea7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 28 Oct 2024 00:50:24 -0400 Subject: [PATCH 01/46] Make sure a potential port component is considered during hostname validation --- src/termio/stream_handler.zig | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 90a33e8b7..00762bc73 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1057,13 +1057,36 @@ pub const StreamHandler = struct { // Get the raw string of the URI. Its unclear to me if the various // tags of this enum guarantee no percent-encoding so we just // check all of it. This isn't a performance critical path. - const host = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, + const host = host: { + const h = switch (host_component) { + .raw => |v| v, + .percent_encoded => |v| v, + }; + if (h.len == 0 or std.mem.eql(u8, "localhost", h)) { + break :host_valid true; + } + + // When the "Private Wi-Fi address" setting is toggled on macOS the hostname + // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. + // The URI will be parsed as if the last set o digit is a port, so we need to + // make sure that part is included when it's set. + if (uri.port) |port| { + // 65_535 is considered the highest port number on Linux. + const PORT_NUMBER_MAX_DIGITS = 5; + + // Make sure there is space for a max length hostname + the max number of digits. + var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; + + const host_and_port = std.fmt.bufPrint(&host_and_port_buf, "{s}:{d}", .{ h, port }) catch |err| { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); + break :host_valid false; + }; + + break :host host_and_port; + } else { + break :host h; + } }; - if (host.len == 0 or std.mem.eql(u8, "localhost", host)) { - break :host_valid true; - } // Otherwise, it must match our hostname. var buf: [posix.HOST_NAME_MAX]u8 = undefined; From 3a3da82aa98513de45fbc1abd081ea93d420c7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 31 Oct 2024 18:09:36 -0400 Subject: [PATCH 02/46] Update explanation for number of digits in port number The explanation now refers to RFC 793 instead of just claiming some arbitrary value as truth. The previous value was correct, but now there is a proper source for the correct value. --- src/termio/stream_handler.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 00762bc73..bd2017f89 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1071,7 +1071,9 @@ pub const StreamHandler = struct { // The URI will be parsed as if the last set o digit is a port, so we need to // make sure that part is included when it's set. if (uri.port) |port| { - // 65_535 is considered the highest port number on Linux. + // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent + // the maximum since 2^16 - 1 = 65_535. + // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. const PORT_NUMBER_MAX_DIGITS = 5; // Make sure there is space for a max length hostname + the max number of digits. From 3b0a34afbca40694b3b170b7f1cb681e9454435a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 31 Oct 2024 21:55:02 -0400 Subject: [PATCH 03/46] Extract OSC 7 hostname parsing into helper functions --- src/termio/stream_handler.zig | 111 ++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index bd2017f89..8db67f66c 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1030,6 +1030,48 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } + pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { + // Get the raw string of the URI. Its unclear to me if the various + // tags of this enum guarantee no percent-encoding so we just + // check all of it. This isn't a performance critical path. + const host_component = uri.host orelse return error.NoHostnameInUri; + const host = switch (host_component) { + .raw => |v| v, + .percent_encoded => |v| v, + }; + + // When the "Private Wi-Fi address" setting is toggled on macOS the hostname + // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. + // The URI will be parsed as if the last set o digit is a port, so we need to + // make sure that part is included when it's set. + if (uri.port) |port| { + var fbs = std.io.fixedBufferStream(buf); + std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { + error.NoSpaceLeft => return error.NoSpaceLeft, + else => unreachable, + }; + + return fbs.getWritten(); + } + + return host; + } + + pub fn isLocalHostname(hostname: []const u8) !bool { + // A 'localhost' hostname is always considered local. + if (std.mem.eql(u8, "localhost", hostname)) { + return true; + } + + // If hostname is not "localhost" it must match our hostname. + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const ourHostname = posix.gethostname(&buf) catch |err| { + return err; + }; + + return std.mem.eql(u8, hostname, ourHostname); + } + pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { if (builtin.os.tag == .windows) { log.warn("reportPwd unimplemented on windows", .{}); @@ -1048,57 +1090,34 @@ pub const StreamHandler = struct { return; } + // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent + // the maximum since 2^16 - 1 = 65_535. + // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. + const PORT_NUMBER_MAX_DIGITS = 5; + // Make sure there is space for a max length hostname + the max number of digits. + var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; + + const hostname = bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { + error.NoHostnameInUri => { + log.warn("OSC 7 uri must contain a hostname: {}", .{err}); + return; + }, + error.NoSpaceLeft => |e| { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{e}); + return; + }, + }; + // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = host_valid: { - const host_component = uri.host orelse break :host_valid false; - - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const host = host: { - const h = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - if (h.len == 0 or std.mem.eql(u8, "localhost", h)) { - break :host_valid true; - } - - // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. - // The URI will be parsed as if the last set o digit is a port, so we need to - // make sure that part is included when it's set. - if (uri.port) |port| { - // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent - // the maximum since 2^16 - 1 = 65_535. - // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. - const PORT_NUMBER_MAX_DIGITS = 5; - - // Make sure there is space for a max length hostname + the max number of digits. - var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - - const host_and_port = std.fmt.bufPrint(&host_and_port_buf, "{s}:{d}", .{ h, port }) catch |err| { - log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); - break :host_valid false; - }; - - break :host host_and_port; - } else { - break :host h; - } - }; - - // Otherwise, it must match our hostname. - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const hostname = posix.gethostname(&buf) catch |err| { + const host_valid = isLocalHostname(hostname) catch |err| switch (err) { + error.PermissionDenied, error.Unexpected => { log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); - break :host_valid false; - }; - - break :host_valid std.mem.eql(u8, host, hostname); + return; + }, }; + if (!host_valid) { log.warn("OSC 7 host must be local", .{}); return; From 03bb16fcec53ba884c04e725a48fe828b907308d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 16:54:38 -0500 Subject: [PATCH 04/46] Move hostname helpers to src/os/hostname.zig --- src/os/hostname.zig | 44 ++++++++++++++++++++++++++++++++ src/termio/stream_handler.zig | 47 +++-------------------------------- 2 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 src/os/hostname.zig diff --git a/src/os/hostname.zig b/src/os/hostname.zig new file mode 100644 index 000000000..0bed2d547 --- /dev/null +++ b/src/os/hostname.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const posix = std.posix; + +pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { + // Get the raw string of the URI. Its unclear to me if the various + // tags of this enum guarantee no percent-encoding so we just + // check all of it. This isn't a performance critical path. + const host_component = uri.host orelse return error.NoHostnameInUri; + const host = switch (host_component) { + .raw => |v| v, + .percent_encoded => |v| v, + }; + + // When the "Private Wi-Fi address" setting is toggled on macOS the hostname + // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. + // The URI will be parsed as if the last set o digit is a port, so we need to + // make sure that part is included when it's set. + if (uri.port) |port| { + var fbs = std.io.fixedBufferStream(buf); + std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { + error.NoSpaceLeft => return error.NoSpaceLeft, + else => unreachable, + }; + + return fbs.getWritten(); + } + + return host; +} + +pub fn isLocalHostname(hostname: []const u8) !bool { + // A 'localhost' hostname is always considered local. + if (std.mem.eql(u8, "localhost", hostname)) { + return true; + } + + // If hostname is not "localhost" it must match our hostname. + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const ourHostname = posix.gethostname(&buf) catch |err| { + return err; + }; + + return std.mem.eql(u8, hostname, ourHostname); +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 8db67f66c..c97c533ea 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -5,6 +5,7 @@ const xev = @import("xev"); const apprt = @import("../apprt.zig"); const build_config = @import("../build_config.zig"); const configpkg = @import("../config.zig"); +const hostname = @import("../os/hostname.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); const terminal = @import("../terminal/main.zig"); @@ -1030,48 +1031,6 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } - pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return error.NoHostnameInUri; - const host = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. - // The URI will be parsed as if the last set o digit is a port, so we need to - // make sure that part is included when it's set. - if (uri.port) |port| { - var fbs = std.io.fixedBufferStream(buf); - std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { - error.NoSpaceLeft => return error.NoSpaceLeft, - else => unreachable, - }; - - return fbs.getWritten(); - } - - return host; - } - - pub fn isLocalHostname(hostname: []const u8) !bool { - // A 'localhost' hostname is always considered local. - if (std.mem.eql(u8, "localhost", hostname)) { - return true; - } - - // If hostname is not "localhost" it must match our hostname. - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = posix.gethostname(&buf) catch |err| { - return err; - }; - - return std.mem.eql(u8, hostname, ourHostname); - } - pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { if (builtin.os.tag == .windows) { log.warn("reportPwd unimplemented on windows", .{}); @@ -1097,7 +1056,7 @@ pub const StreamHandler = struct { // Make sure there is space for a max length hostname + the max number of digits. var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - const hostname = bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { + const hostname_from_uri = hostname.bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { error.NoHostnameInUri => { log.warn("OSC 7 uri must contain a hostname: {}", .{err}); return; @@ -1111,7 +1070,7 @@ pub const StreamHandler = struct { // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = isLocalHostname(hostname) catch |err| switch (err) { + const host_valid = hostname.isLocalHostname(hostname_from_uri) catch |err| switch (err) { error.PermissionDenied, error.Unexpected => { log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); return; From 78abd051a21cf493fb353dd72c651795dbf22401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 17:13:12 -0500 Subject: [PATCH 05/46] os/hostname: test isLocalHostname --- src/os/hostname.zig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 0bed2d547..dcc802b65 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -42,3 +42,18 @@ pub fn isLocalHostname(hostname: []const u8) !bool { return std.mem.eql(u8, hostname, ourHostname); } + +test "isLocalHostname returns true when provided hostname is localhost" { + try std.testing.expect(try isLocalHostname("localhost")); +} + +test "isLocalHostname returns true when hostname is local" { + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const localHostname = try posix.gethostname(&buf); + + try std.testing.expect(try isLocalHostname(localHostname)); +} + +test "isLocalHostname returns false when hostname is not local" { + try std.testing.expectEqual(false, try isLocalHostname("not-the-local-hostname")); +} From 9ae6806e30e1d83dfa697ed26bac39db2dbad912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 17:25:00 -0500 Subject: [PATCH 06/46] os/hostname: test bufPrintHostnameFromFileUri Note that this includes some failing tests because I want to make the uri handling better and more specific. It's a little bit too general right now so I want to lock it down to: 1. macOS only; and 2. valid mac address values because that's how the macOS private Wi-Fi address thing works; randomizes your mac address and sets that as your hostname. --- src/os/hostname.zig | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index dcc802b65..31738a90f 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -43,6 +43,50 @@ pub fn isLocalHostname(hostname: []const u8) !bool { return std.mem.eql(u8, hostname, ourHostname); } +test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { + const uri = try std.Uri.parse("file://localhost/"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectEqualStrings("localhost", actual); +} + +test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { + const uri = try std.Uri.parse("file://12:34:56:78:90:12"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); +} + +test "bufPrintHostnameFromFileUri returns only hostname when there is a port component in the URI" { + // First: try with a non-2-digit port, to test general port handling. + const four_port_uri = try std.Uri.parse("file://has-a-port:1234"); + + var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; + const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri); + + try std.testing.expectEqualStrings("has-a-port", four_port_actual); + + // Second: try with a 2-digit port to test mac-address handling. + const two_port_uri = try std.Uri.parse("file://has-a-port:12"); + + var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; + const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri); + + try std.testing.expectEqualStrings("has-a-port", two_port_actual); + + // Third: try with a mac-address that has a port-component added to it to test mac-address handling. + const mac_with_port_uri = try std.Uri.parse("file://12:34:56:78:90:12:1234"); + + var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; + const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri); + + try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); +} + test "isLocalHostname returns true when provided hostname is localhost" { try std.testing.expect(try isLocalHostname("localhost")); } From e85b11403145ea01aff8efcbbf5e1480334f5ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 18:58:50 -0500 Subject: [PATCH 07/46] os/hostname: add better validation for mac-address hostnames --- src/os/hostname.zig | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 31738a90f..81ef0e6e3 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -12,10 +12,26 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { }; // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a string of digits separated by a colon, e.g. '12:34:56:78:90:12'. - // The URI will be parsed as if the last set o digit is a port, so we need to - // make sure that part is included when it's set. + // is set to a random mac address, e.g. '12:34:56:78:90:ab'. + // The URI will be parsed as if the last set of digits is a port number, so + // we need to make sure that part is included when it's set. + + // We're only interested in special port handling when the current hostname is a + // partial MAC address that's potentially missing the last component. + // If that's not the case we just return the plain URI hostname directly. + // NOTE: This implementation is not sufficient to verify a valid mac address, but + // it's probably sufficient for this specific purpose. + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) { + return host; + } + if (uri.port) |port| { + // If the port is not a 2-digit number we're not looking at a partial MAC-address, + // and instead just a regular port so we return the plain URI hostname. + if (port < 10 or port > 99) { + return host; + } + var fbs = std.io.fixedBufferStream(buf); std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { error.NoSpaceLeft => return error.NoSpaceLeft, From 9c2f260351f9445133e2f7dde5cef90975cb7268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 4 Nov 2024 19:20:09 -0500 Subject: [PATCH 08/46] os/hostname: add and use explicit error structs --- src/os/hostname.zig | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 81ef0e6e3..e05586167 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,11 +1,21 @@ const std = @import("std"); const posix = std.posix; -pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { +const HostnameParsingError = error{ + NoHostnameInUri, + NoSpaceLeft, +}; + +const LocalHostnameValidationError = error{ + PermissionDenied, + Unexpected, +}; + +pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) HostnameParsingError![]const u8 { // Get the raw string of the URI. Its unclear to me if the various // tags of this enum guarantee no percent-encoding so we just // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return error.NoHostnameInUri; + const host_component = uri.host orelse return HostnameParsingError.NoHostnameInUri; const host = switch (host_component) { .raw => |v| v, .percent_encoded => |v| v, @@ -34,7 +44,7 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { var fbs = std.io.fixedBufferStream(buf); std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { - error.NoSpaceLeft => return error.NoSpaceLeft, + error.NoSpaceLeft => return HostnameParsingError.NoSpaceLeft, else => unreachable, }; @@ -44,7 +54,7 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) ![]const u8 { return host; } -pub fn isLocalHostname(hostname: []const u8) !bool { +pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. if (std.mem.eql(u8, "localhost", hostname)) { return true; @@ -52,8 +62,9 @@ pub fn isLocalHostname(hostname: []const u8) !bool { // If hostname is not "localhost" it must match our hostname. var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = posix.gethostname(&buf) catch |err| { - return err; + const ourHostname = posix.gethostname(&buf) catch |err| switch (err) { + error.PermissionDenied => return LocalHostnameValidationError.PermissionDenied, + error.Unexpected => return LocalHostnameValidationError.Unexpected, }; return std.mem.eql(u8, hostname, ourHostname); @@ -103,6 +114,24 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); } +test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is missing from uri" { + const uri = try std.Uri.parse("file:///"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual); +} + +test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer has insufficient size" { + const uri = try std.Uri.parse("file://12:34:56:78:90:12/"); + + var buf: [5]u8 = undefined; + const actual = bufPrintHostnameFromFileUri(&buf, uri); + + try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual); +} + test "isLocalHostname returns true when provided hostname is localhost" { try std.testing.expect(try isLocalHostname("localhost")); } From 4a263f43afa946047a4f967799af5c12cc0a6501 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 10:30:48 -0800 Subject: [PATCH 09/46] stylistic changes --- src/os/hostname.zig | 80 ++++++++++++++++------------------- src/os/main.zig | 5 +++ src/termio/stream_handler.zig | 17 +++++--- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index e05586167..6956ed71f 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,22 +1,21 @@ const std = @import("std"); const posix = std.posix; -const HostnameParsingError = error{ +pub const HostnameParsingError = error{ NoHostnameInUri, NoSpaceLeft, }; -const LocalHostnameValidationError = error{ - PermissionDenied, - Unexpected, -}; - -pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) HostnameParsingError![]const u8 { +/// Print the hostname from a file URI into a buffer. +pub fn bufPrintHostnameFromFileUri( + buf: []u8, + uri: std.Uri, +) HostnameParsingError![]const u8 { // Get the raw string of the URI. Its unclear to me if the various // tags of this enum guarantee no percent-encoding so we just // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return HostnameParsingError.NoHostnameInUri; - const host = switch (host_component) { + const host_component = uri.host orelse return error.NoHostnameInUri; + const host: []const u8 = switch (host_component) { .raw => |v| v, .percent_encoded => |v| v, }; @@ -31,42 +30,42 @@ pub fn bufPrintHostnameFromFileUri(buf: []u8, uri: std.Uri) HostnameParsingError // If that's not the case we just return the plain URI hostname directly. // NOTE: This implementation is not sufficient to verify a valid mac address, but // it's probably sufficient for this specific purpose. - if (host.len != 14 or std.mem.count(u8, host, ":") != 4) { - return host; - } + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) return host; - if (uri.port) |port| { - // If the port is not a 2-digit number we're not looking at a partial MAC-address, - // and instead just a regular port so we return the plain URI hostname. - if (port < 10 or port > 99) { - return host; - } + // If we don't have a port then we can return the hostname as-is because + // it's not a partial MAC-address. + const port = uri.port orelse return host; - var fbs = std.io.fixedBufferStream(buf); - std.fmt.format(fbs.writer().any(), "{s}:{d}", .{ host, port }) catch |err| switch (err) { - error.NoSpaceLeft => return HostnameParsingError.NoSpaceLeft, - else => unreachable, - }; + // If the port is not a 2-digit number we're not looking at a partial + // MAC-address, and instead just a regular port so we return the plain + // URI hostname. + if (port < 10 or port > 99) return host; - return fbs.getWritten(); - } + var fbs = std.io.fixedBufferStream(buf); + try std.fmt.format( + fbs.writer(), + "{s}:{d}", + .{ host, port }, + ); - return host; + return fbs.getWritten(); } +pub const LocalHostnameValidationError = error{ + PermissionDenied, + Unexpected, +}; + +/// Checks if a hostname is local to the current machine. This matches +/// both "localhost" and the current hostname of the machine (as returned +/// by `gethostname`). pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. - if (std.mem.eql(u8, "localhost", hostname)) { - return true; - } + if (std.mem.eql(u8, "localhost", hostname)) return true; // If hostname is not "localhost" it must match our hostname. var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = posix.gethostname(&buf) catch |err| switch (err) { - error.PermissionDenied => return LocalHostnameValidationError.PermissionDenied, - error.Unexpected => return LocalHostnameValidationError.Unexpected, - }; - + const ourHostname = try posix.gethostname(&buf); return std.mem.eql(u8, hostname, ourHostname); } @@ -75,7 +74,6 @@ test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("localhost", actual); } @@ -84,7 +82,6 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); } @@ -94,7 +91,6 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri); - try std.testing.expectEqualStrings("has-a-port", four_port_actual); // Second: try with a 2-digit port to test mac-address handling. @@ -102,7 +98,6 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri); - try std.testing.expectEqualStrings("has-a-port", two_port_actual); // Third: try with a mac-address that has a port-component added to it to test mac-address handling. @@ -110,7 +105,6 @@ test "bufPrintHostnameFromFileUri returns only hostname when there is a port com var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); } @@ -119,7 +113,6 @@ test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is var buf: [posix.HOST_NAME_MAX]u8 = undefined; const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual); } @@ -128,7 +121,6 @@ test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer var buf: [5]u8 = undefined; const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual); } @@ -139,10 +131,12 @@ test "isLocalHostname returns true when provided hostname is localhost" { test "isLocalHostname returns true when hostname is local" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const localHostname = try posix.gethostname(&buf); - try std.testing.expect(try isLocalHostname(localHostname)); } test "isLocalHostname returns false when hostname is not local" { - try std.testing.expectEqual(false, try isLocalHostname("not-the-local-hostname")); + try std.testing.expectEqual( + false, + try isLocalHostname("not-the-local-hostname"), + ); } diff --git a/src/os/main.zig b/src/os/main.zig index 7eed97445..fbe0ac411 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -17,6 +17,7 @@ const resourcesdir = @import("resourcesdir.zig"); // Namespaces pub const args = @import("args.zig"); pub const cgroup = @import("cgroup.zig"); +pub const hostname = @import("hostname.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); @@ -42,3 +43,7 @@ pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index c97c533ea..2d399e8c1 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -5,7 +5,7 @@ const xev = @import("xev"); const apprt = @import("../apprt.zig"); const build_config = @import("../build_config.zig"); const configpkg = @import("../config.zig"); -const hostname = @import("../os/hostname.zig"); +const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); const terminal = @import("../terminal/main.zig"); @@ -1055,8 +1055,10 @@ pub const StreamHandler = struct { const PORT_NUMBER_MAX_DIGITS = 5; // Make sure there is space for a max length hostname + the max number of digits. var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - - const hostname_from_uri = hostname.bufPrintHostnameFromFileUri(&host_and_port_buf, uri) catch |err| switch (err) { + const hostname_from_uri = internal_os.hostname.bufPrintHostnameFromFileUri( + &host_and_port_buf, + uri, + ) catch |err| switch (err) { error.NoHostnameInUri => { log.warn("OSC 7 uri must contain a hostname: {}", .{err}); return; @@ -1070,13 +1072,16 @@ pub const StreamHandler = struct { // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = hostname.isLocalHostname(hostname_from_uri) catch |err| switch (err) { - error.PermissionDenied, error.Unexpected => { + const host_valid = internal_os.hostname.isLocalHostname( + hostname_from_uri, + ) catch |err| switch (err) { + error.PermissionDenied, + error.Unexpected, + => { log.warn("failed to get hostname for OSC 7 validation: {}", .{err}); return; }, }; - if (!host_valid) { log.warn("OSC 7 host must be local", .{}); return; From c8b99f78914ec017be3fb425711032c78e8e02a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 10:36:11 -0800 Subject: [PATCH 10/46] remove refalldecls test --- src/os/main.zig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/os/main.zig b/src/os/main.zig index fbe0ac411..48a712d40 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -43,7 +43,3 @@ pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; - -test { - @import("std").testing.refAllDecls(@This()); -} From 34c3da43023b0c058c0647b604b8bc4b2f87ff7b Mon Sep 17 00:00:00 2001 From: Nico Elbers Date: Tue, 5 Nov 2024 18:29:40 +0100 Subject: [PATCH 11/46] docs: fix the nixos install instructions Not updating inputs resulted in a crash for me, this fixed it. Relevant link from discord: https://discord.com/channels/1005603569187160125/1301217629268213770 --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 796590401..3c3a2460d 100644 --- a/README.md +++ b/README.md @@ -789,7 +789,14 @@ Below is an example: # # Instead, either run `nix flake update` or `nixos-rebuild build` # as the current user, and then run `sudo nixos-rebuild switch`. - ghostty.url = "git+ssh://git@github.com/ghostty-org/ghostty"; + ghostty = { + url = "git+ssh://git@github.com/ghostty-org/ghostty"; + + # NOTE: The below 2 lines are only required on nixos-unstable, + # if you're on stable, they may break your build + inputs.nixpkgs-stable.follows = "nixpkgs"; + inputs.nixpkgs-unstable.follows = "nixpkgs"; + }; }; outputs = { nixpkgs, ghostty, ... }: { From e08eeb2b2ad810c4db22530a181858caee834b22 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 16:13:53 -0800 Subject: [PATCH 12/46] coretext: set variations on deferred face load This commit makes CoreText behave a lot like FreeType where we set the variation axes on the deferred face load. This fixes a bug where the `slnt` variation axis could not be set with CoreText with the Monaspace Argon Variable font. This was a bug found in Discord. Specifically, with the Monaspace Argon Variable font, the `slnt` variation axis could not be set with CoreText. I'm not sure _exactly_ what causes this but I suspect it has to do with the `slnt` axis being a negative value. I'm not sure if this is a bug with CoreText or not. What was happening was that with CoreText, we set the variation axes during discovery and expect them to be preserved in the resulting discovered faces. That seems to be true with the `wght` axis but not the `slnt` axis for whatever reason. --- src/font/DeferredFace.zig | 48 ++++++++------------------------------ src/font/discovery.zig | 11 +++++++-- src/font/face/coretext.zig | 3 +++ 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index db9e23623..3ee104386 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -57,6 +57,11 @@ pub const CoreText = struct { /// The initialized font font: *macos.text.Font, + /// Variations to apply to this font. We apply the variations to the + /// search descriptor but sometimes when the font collection is + /// made the variation axes are reset so we have to reapply them. + variations: []const font.face.Variation, + pub fn deinit(self: *CoreText) void { self.font.release(); self.* = undefined; @@ -194,7 +199,10 @@ fn loadCoreText( ) !Face { _ = lib; const ct = self.ct.?; - return try Face.initFontCopy(ct.font, opts); + var face = try Face.initFontCopy(ct.font, opts); + errdefer face.deinit(); + try face.setVariations(ct.variations, opts); + return face; } fn loadCoreTextFreetype( @@ -236,43 +244,7 @@ fn loadCoreTextFreetype( //std.log.warn("path={s}", .{path_slice}); var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts); errdefer face.deinit(); - - // If our ct font has variations, apply them to the face. - if (ct.font.copyAttribute(.variation)) |variations| vars: { - defer variations.release(); - if (variations.getCount() == 0) break :vars; - - // This configuration is just used for testing so we don't want to - // have to pass a full allocator through so use the stack. We - // shouldn't have a lot of variations and if we do we should use - // another mechanism. - // - // On macOS the default stack size for a thread is 512KB and the main - // thread gets megabytes so 16KB is a safe stack allocation. - var data: [1024 * 16]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&data); - const alloc = fba.allocator(); - - var face_vars = std.ArrayList(font.face.Variation).init(alloc); - const kav = try variations.getKeysAndValues(alloc); - for (kav.keys, kav.values) |key, value| { - const num: *const macos.foundation.Number = @ptrCast(key.?); - const val: *const macos.foundation.Number = @ptrCast(value.?); - - var num_i32: i32 = undefined; - if (!num.getValue(.sint32, &num_i32)) continue; - - var val_f64: f64 = undefined; - if (!val.getValue(.float64, &val_f64)) continue; - - try face_vars.append(.{ - .id = @bitCast(num_i32), - .value = val_f64, - }); - } - - try face.setVariations(face_vars.items, opts); - } + try face.setVariations(ct.variations, opts); return face; } diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 3aa16eebf..aeaeb955e 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -79,7 +79,7 @@ pub const Descriptor = struct { // This is not correct, but we don't currently depend on the // hash value being different based on decimal values of variations. - autoHash(hasher, @as(u64, @intFromFloat(variation.value))); + autoHash(hasher, @as(i64, @intFromFloat(variation.value))); } } @@ -384,6 +384,7 @@ pub const CoreText = struct { return DiscoverIterator{ .alloc = alloc, .list = zig_list, + .variations = desc.variations, .i = 0, }; } @@ -420,6 +421,7 @@ pub const CoreText = struct { return DiscoverIterator{ .alloc = alloc, .list = list, + .variations = desc.variations, .i = 0, }; } @@ -443,6 +445,7 @@ pub const CoreText = struct { return DiscoverIterator{ .alloc = alloc, .list = list, + .variations = desc.variations, .i = 0, }; } @@ -721,6 +724,7 @@ pub const CoreText = struct { pub const DiscoverIterator = struct { alloc: Allocator, list: []const *macos.text.FontDescriptor, + variations: []const Variation, i: usize, pub fn deinit(self: *DiscoverIterator) void { @@ -756,7 +760,10 @@ pub const CoreText = struct { defer self.i += 1; return DeferredFace{ - .ct = .{ .font = font }, + .ct = .{ + .font = font, + .variations = self.variations, + }, }; } }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 2403b3902..363dbacd8 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -229,6 +229,9 @@ pub const Face = struct { vs: []const font.face.Variation, opts: font.face.Options, ) !void { + // If we have no variations, we don't need to do anything. + if (vs.len == 0) return; + // Create a new font descriptor with all the variations set. var desc = self.font.copyDescriptor(); defer desc.release(); From 98c4c453ee70256f205d2615e1595c51236084c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 16:32:23 -0800 Subject: [PATCH 13/46] update iterm2 themes --- build.zig.zon | 4 ++-- nix/zigCacheHash.nix | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b475b25ca..ad9a92d11 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -49,8 +49,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b4a9c4d.tar.gz", - .hash = "122056fbb29863ec1678b7954fb76b1533ad8c581a34577c1b2efe419e29e05596df", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/da56d590c4237c96d81cc5ed987ea098eebefdf6.tar.gz", + .hash = "1220fac17a112b0dd11ec85e5b31a30f05bfaed897c03f31285276544db30d010c41", }, .vaxis = .{ .url = "git+https://github.com/rockorager/libvaxis?ref=main#a1b43d24653670d612b91f0855b165e6c987b809", diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index 65d26e146..ebc46799f 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-dNGDbVaPxDbIrDUkDwjzeRHHVcX4KnWKciXiTp1c7lE=" +"sha256-fTNqNTfElvZPxJiNQJ/RxrSMCiKZPU3705CY7fznKhY=" From 65f1cefb4e8fb2da369d8afdcf2ea6e15ffde164 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Nov 2024 16:51:13 -0800 Subject: [PATCH 14/46] config: add "initial-command" config, "-e" sets that Fixes #2601 It is more expected behavior that `-e` affects only the first window. By introducing a dedicated configuration we avoid making `-e` too magical: its simply syntax sugar for setting the "initial-command" configuration. --- src/App.zig | 5 +++++ src/Surface.zig | 17 +++++++++++++---- src/config/Config.zig | 31 +++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/App.zig b/src/App.zig index c54c67167..cc8277c52 100644 --- a/src/App.zig +++ b/src/App.zig @@ -66,6 +66,11 @@ font_grid_set: font.SharedGridSet, last_notification_time: ?std.time.Instant = null, last_notification_digest: u64 = 0, +/// Set to false once we've created at least one surface. This +/// never goes true again. This can be used by surfaces to determine +/// if they are the first surface. +first: bool = true, + pub const CreateError = Allocator.Error || font.SharedGridSet.InitError; /// Initialize the main app instance. This creates the main window, sets diff --git a/src/Surface.zig b/src/Surface.zig index d19f9e812..d0c199010 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -474,13 +474,19 @@ pub fn init( .config = derived_config, }; + // The command we're going to execute + const command: ?[]const u8 = if (app.first) + config.@"initial-command" orelse config.command + else + config.command; + // Start our IO implementation // This separate block ({}) is important because our errdefers must // be scoped here to be valid. { // Initialize our IO backend var io_exec = try termio.Exec.init(alloc, .{ - .command = config.command, + .command = command, .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", .working_directory = config.@"working-directory", @@ -618,9 +624,9 @@ pub fn init( // For xdg-terminal-exec execution we special-case and set the window // title to the command being executed. This allows window managers // to set custom styling based on the command being executed. - const command = config.command orelse break :xdg; - if (command.len > 0) { - const title = alloc.dupeZ(u8, command) catch |err| { + const v = command orelse break :xdg; + if (v.len > 0) { + const title = alloc.dupeZ(u8, v) catch |err| { log.warn( "error copying command for title, title will not be set err={}", .{err}, @@ -635,6 +641,9 @@ pub fn init( ); } } + + // We are no longer the first surface + app.first = false; } pub fn deinit(self: *Surface) void { diff --git a/src/config/Config.zig b/src/config/Config.zig index a674046e1..e6b9d35ab 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -513,7 +513,26 @@ palette: Palette = .{}, /// arguments are provided, the command will be executed using `/bin/sh -c`. /// Ghostty does not do any shell command parsing. /// -/// If you're using the `ghostty` CLI there is also a shortcut to run a command +/// This command will be used for all new terminal surfaces, i.e. new windows, +/// tabs, etc. If you want to run a command only for the first terminal surface +/// created when Ghostty starts, use the `initial-command` configuration. +/// +/// Ghostty supports the common `-e` flag for executing a command with +/// arguments. For example, `ghostty -e fish --with --custom --args`. +/// This flag sets the `initial-command` configuration, see that for more +/// information. +command: ?[]const u8 = null, + +/// This is the same as "command", but only applies to the first terminal +/// surface created when Ghostty starts. Subsequent terminal surfaces will use +/// the `command` configuration. +/// +/// After the first terminal surface is created (or closed), there is no +/// way to run this initial command again automatically. As such, setting +/// this at runtime works but will only affect the next terminal surface +/// if it is the first one ever created. +/// +/// If you're using the `ghostty` CLI there is also a shortcut to set this /// with arguments directly: you can use the `-e` flag. For example: `ghostty -e /// fish --with --custom --args`. The `-e` flag automatically forces some /// other behaviors as well: @@ -525,7 +544,7 @@ palette: Palette = .{}, /// process will exit when the command exits. Additionally, the /// `quit-after-last-window-closed-delay` is unset. /// -command: ?[]const u8 = null, +@"initial-command": ?[]const u8 = null, /// If true, keep the terminal open after the command exits. Normally, the /// terminal window closes when the running command (such as a shell) exits. @@ -2356,7 +2375,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { } self.@"_xdg-terminal-exec" = true; - self.command = command.items[0 .. command.items.len - 1]; + self.@"initial-command" = command.items[0 .. command.items.len - 1]; return; } } @@ -2755,7 +2774,7 @@ pub fn parseManuallyHook( return false; } - self.command = command.items[0 .. command.items.len - 1]; + self.@"initial-command" = command.items[0 .. command.items.len - 1]; // See "command" docs for the implied configurations and why. self.@"gtk-single-instance" = .false; @@ -2945,7 +2964,7 @@ test "parse e: command only" { var it: TestIterator = .{ .data = &.{"foo"} }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("foo", cfg.command.?); + try testing.expectEqualStrings("foo", cfg.@"initial-command".?); } test "parse e: command and args" { @@ -2956,7 +2975,7 @@ test "parse e: command and args" { var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("echo foo bar baz", cfg.command.?); + try testing.expectEqualStrings("echo foo bar baz", cfg.@"initial-command".?); } test "clone default" { From 1e003b2e0fa794a1430be958a324afa227896c6a Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Tue, 5 Nov 2024 23:16:01 +0100 Subject: [PATCH 15/46] gtk: implement toggle_split_zoom --- src/apprt/gtk/App.zig | 9 ++++- src/apprt/gtk/Split.zig | 17 +++++++-- src/apprt/gtk/Surface.zig | 77 +++++++++++++++++++++++++++++++++++++-- src/input/Binding.zig | 3 +- 4 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index e6bf24bd1..fa73c2436 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -471,12 +471,12 @@ pub fn performAction( .mouse_shape => try self.setMouseShape(target, value), .mouse_over_link => self.setMouseOverLink(target, value), .toggle_tab_overview => self.toggleTabOverview(target), + .toggle_split_zoom => self.toggleSplitZoom(target), .toggle_window_decorations => self.toggleWindowDecorations(target), .quit_timer => self.quitTimer(value), // Unimplemented .close_all_windows, - .toggle_split_zoom, .toggle_quick_terminal, .toggle_visibility, .size_limit, @@ -671,6 +671,13 @@ fn toggleTabOverview(_: *App, target: apprt.Target) void { } } +fn toggleSplitZoom(_: *App, target: apprt.Target) void { + switch (target) { + .app => {}, + .surface => |surface| surface.rt_surface.toggleSplitZoom(), + } +} + fn toggleWindowDecorations( _: *App, target: apprt.Target, diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 5afac6f5b..54fa30e1c 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -77,6 +77,7 @@ pub fn init( }); errdefer surface.destroy(alloc); sibling.dimSurface(); + sibling.setSplitZoom(false); // Create the actual GTKPaned, attach the proper children. const orientation: c_uint = switch (direction) { @@ -258,7 +259,7 @@ pub fn grabFocus(self: *Split) void { /// Update the paned children to represent the current state. /// This should be called anytime the top/left or bottom/right /// element is changed. -fn updateChildren(self: *const Split) void { +pub fn updateChildren(self: *const Split) void { // We have to set both to null. If we overwrite the pane with // the same value, then GTK bugs out (the GL area unrealizes // and never rerealizes). @@ -372,7 +373,15 @@ fn directionNext(self: *const Split, from: Side) ?struct { } } -fn removeChildren(self: *const Split) void { - c.gtk_paned_set_start_child(@ptrCast(self.paned), null); - c.gtk_paned_set_end_child(@ptrCast(self.paned), null); +pub fn detachTopLeft(self: *const Split) void { + c.gtk_paned_set_start_child(self.paned, null); +} + +pub fn detachBottomRight(self: *const Split) void { + c.gtk_paned_set_end_child(self.paned, null); +} + +fn removeChildren(self: *const Split) void { + self.detachTopLeft(); + self.detachBottomRight(); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9c7a394f4..8172b7490 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -330,6 +330,9 @@ url_widget: ?URLWidget = null, /// The overlay that shows resizing information. resize_overlay: ResizeOverlay = .{}, +/// Whether or not the current surface is zoomed in (see `toggle_split_zoom`). +zoomed_in: bool = false, + /// If non-null this is the widget on the overlay which dims the surface when it is unfocused unfocused_widget: ?*c.GtkWidget = null, @@ -643,6 +646,8 @@ pub fn redraw(self: *Surface) void { /// Close this surface. pub fn close(self: *Surface, processActive: bool) void { + self.setSplitZoom(false); + // If we're not part of a window hierarchy, we never confirm // so we can just directly remove ourselves and exit. const window = self.container.window() orelse { @@ -791,7 +796,16 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void } pub fn grabFocus(self: *Surface) void { - if (self.container.tab()) |tab| tab.focus_child = self; + if (self.container.tab()) |tab| { + // If any other surface was focused and zoomed in, set it to non zoomed in + // so that self can grab focus. + if (tab.focus_child) |focus_child| { + if (focus_child.zoomed_in and focus_child != self) { + focus_child.setSplitZoom(false); + } + } + tab.focus_child = self; + } const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); _ = c.gtk_widget_grab_focus(widget); @@ -801,7 +815,7 @@ pub fn grabFocus(self: *Surface) void { fn updateTitleLabels(self: *Surface) void { // If we have no title, then we have nothing to update. - const title = self.title_text orelse return; + const title = self.getTitle() orelse return; // If we have a tab and are the focused child, then we have to update the tab if (self.container.tab()) |tab| { @@ -822,9 +836,19 @@ fn updateTitleLabels(self: *Surface) void { } } +const zoom_title_prefix = "🔍 "; + pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { const alloc = self.app.core_app.alloc; - const copy = try alloc.dupeZ(u8, slice); + + // Always allocate with the "🔍 " at the beginning and slice accordingly + // is the surface is zoomed in or not. + const copy: [:0]const u8 = copy: { + const new_title = try alloc.allocSentinel(u8, zoom_title_prefix.len + slice.len, 0); + @memcpy(new_title[0..zoom_title_prefix.len], zoom_title_prefix); + @memcpy(new_title[zoom_title_prefix.len..], slice); + break :copy new_title; + }; errdefer alloc.free(copy); if (self.title_text) |old| alloc.free(old); @@ -834,7 +858,14 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { } pub fn getTitle(self: *Surface) ?[:0]const u8 { - return self.title_text; + if (self.title_text) |title_text| { + return if (self.zoomed_in) + title_text + else + title_text[zoom_title_prefix.len..]; + } + + return null; } pub fn setMouseShape( @@ -1875,3 +1906,41 @@ pub fn present(self: *Surface) void { self.grabFocus(); } + +fn detachFromSplit(self: *Surface) void { + const split = self.container.split() orelse return; + switch (self.container.splitSide() orelse unreachable) { + .top_left => split.detachTopLeft(), + .bottom_right => split.detachBottomRight(), + } +} + +fn attachToSplit(self: *Surface) void { + const split = self.container.split() orelse return; + split.updateChildren(); +} + +pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void { + if (new_split_zoom == self.zoomed_in) return; + const tab = self.container.tab() orelse return; + + const tab_widget = tab.elem.widget(); + const surface_widget = self.primaryWidget(); + + if (new_split_zoom) { + self.detachFromSplit(); + c.gtk_box_remove(tab.box, tab_widget); + c.gtk_box_append(tab.box, surface_widget); + } else { + c.gtk_box_remove(tab.box, surface_widget); + self.attachToSplit(); + c.gtk_box_append(tab.box, tab_widget); + } + + self.zoomed_in = new_split_zoom; + self.grabFocus(); +} + +pub fn toggleSplitZoom(self: *Surface) void { + self.setSplitZoom(!self.zoomed_in); +} diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 5a4cd3f3e..347bc56d2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -317,8 +317,7 @@ pub const Action = union(enum) { /// Focus on a split in a given direction. goto_split: SplitFocusDirection, - /// zoom/unzoom the current split. This is currently only supported - /// on macOS. Contributions welcome for other platforms. + /// zoom/unzoom the current split. toggle_split_zoom: void, /// Resize the current split by moving the split divider in the given From 964f2ce96a01b5d05719c08fb48101d7c61ccdf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Nov 2024 12:53:34 -0800 Subject: [PATCH 16/46] font/coretext: always score based on style string length This fixes an issue where for the regular style we were picking a suboptimal style because for some font faces we were choosing more bold faces (just as chance). This modifies our scoring to take the style length into account even for regular style. We already had this logic for explicit styles. --- src/font/discovery.zig | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index aeaeb955e..67236d5c9 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -685,30 +685,29 @@ pub const CoreText = struct { break :style .unmatched; defer style.release(); + // Get our style string + var buf: [128]u8 = undefined; + const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; + // If we have a specific desired style, attempt to search for that. if (desc.style) |desired_style| { - var buf: [128]u8 = undefined; - const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; - // Matching style string gets highest score if (std.mem.eql(u8, desired_style, style_str)) break :style .match; - - // Otherwise the score is based on the length of the style string. - // Shorter styles are scored higher. - break :style @enumFromInt(100 -| style_str.len); + } else if (!desc.bold and !desc.italic) { + // If we do not, and we have no symbolic traits, then we try + // to find "regular" (or no style). If we have symbolic traits + // we do nothing but we can improve scoring by taking that into + // account, too. + if (std.mem.eql(u8, "Regular", style_str)) { + break :style .match; + } } - // If we do not, and we have no symbolic traits, then we try - // to find "regular" (or no style). If we have symbolic traits - // we do nothing but we can improve scoring by taking that into - // account, too. - if (!desc.bold and !desc.italic) { - var buf: [128]u8 = undefined; - const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; - if (std.mem.eql(u8, "Regular", style_str)) break :style .match; - } - - break :style .unmatched; + // Otherwise the score is based on the length of the style string. + // Shorter styles are scored higher. This is a heuristic that + // if we don't have a desired style then shorter tends to be + // more often the "regular" style. + break :style @enumFromInt(100 -| style_str.len); }; score_acc.traits = traits: { From 94542b04f2f9e958f922be7e4400e1742e617a2d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Nov 2024 12:56:12 -0800 Subject: [PATCH 17/46] font/coretext: do not set variation axes in discovery This was causing discovery to find some odd fonts under certain scenarios (namely: Recursive Mono). Due to our prior fix in e08eeb2b2ad810c4db22530a181858caee834b22 we no longer need to set variations here for them to stick. --- src/font/discovery.zig | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 67236d5c9..e73ea626f 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -235,21 +235,7 @@ pub const Descriptor = struct { ); } - // Build our descriptor from attrs - var desc = try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs)); - errdefer desc.release(); - - // Variations are built by copying the descriptor. I don't know a way - // to set it on attrs directly. - for (self.variations) |v| { - const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id)); - defer id.release(); - const next = try desc.createCopyWithVariation(id, v.value); - desc.release(); - desc = next; - } - - return desc; + return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs)); } }; From b5ed4cb6802e9c947729940825bdc95188668f12 Mon Sep 17 00:00:00 2001 From: phillip-hirsch Date: Wed, 6 Nov 2024 17:06:24 -0500 Subject: [PATCH 18/46] feat: Add syntax highlighting for bat --- build.zig | 17 ++++++++++++ src/config/sublime_syntax.zig | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/config/sublime_syntax.zig diff --git a/build.zig b/build.zig index fb74de340..2cce41a31 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,7 @@ const font = @import("src/font/main.zig"); const renderer = @import("src/renderer.zig"); const terminfo = @import("src/terminfo/main.zig"); const config_vim = @import("src/config/vim.zig"); +const config_sublime_syntax = @import("src/config/sublime_syntax.zig"); const fish_completions = @import("src/build/fish_completions.zig"); const build_config = @import("src/build_config.zig"); const BuildConfig = build_config.BuildConfig; @@ -515,6 +516,22 @@ pub fn build(b: *std.Build) !void { }); } + // Sublime syntax highlighting for bat cli tool + // NOTE: The current implementation requires symlinking the generated + // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' + // directory. The syntax then needs to be mapped to the correct language in + // the config file within the '~.config/bat' directory + // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). + { + const wf = b.addWriteFiles(); + _ = wf.add("ghostty.sublime-syntax", config_sublime_syntax.syntax); + b.installDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/bat/syntaxes", + }); + } + // Documentation if (emit_docs) { try buildDocumentation(b, config); diff --git a/src/config/sublime_syntax.zig b/src/config/sublime_syntax.zig new file mode 100644 index 000000000..1b7c4900a --- /dev/null +++ b/src/config/sublime_syntax.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const Config = @import("Config.zig"); + +const Template = struct { + const header = + \\%YAML 1.2 + \\--- + \\# See http://www.sublimetext.com/docs/syntax.html + \\name: Ghostty Config + \\file_extensions: + \\ - ghostty + \\scope: source.ghostty + \\ + \\contexts: + \\ main: + \\ # Comments + \\ - match: '#.*$' + \\ scope: comment.line.number-sign.ghostty + \\ + \\ # Keywords + \\ - match: '\b( + ; + const footer = + \\)\b' + \\ scope: keyword.other.ghostty + \\ + ; +}; + +/// Check if a field is internal (starts with underscore) +fn isInternal(name: []const u8) bool { + return name.len > 0 and name[0] == '_'; +} + +/// Generate keywords from Config fields +fn generateKeywords() []const u8 { + @setEvalBranchQuota(5000); + var keywords: []const u8 = ""; + const config_fields = @typeInfo(Config).Struct.fields; + + for (config_fields) |field| { + if (isInternal(field.name)) continue; + if (keywords.len > 0) keywords = keywords ++ "|"; + keywords = keywords ++ field.name; + } + + return keywords; +} + +/// Complete Sublime syntax file content +pub const syntax = Template.header ++ generateKeywords() ++ Template.footer; From 04419873462c9e78dc6da5e8fba9646b9fad53a3 Mon Sep 17 00:00:00 2001 From: Meili C Date: Fri, 1 Nov 2024 14:54:24 -0800 Subject: [PATCH 19/46] font feature: add git branch characters addresses #2561 - adds support for most Git branch drawing characters as specified in ![kitty/7681](https://github.com/kovidgoyal/kitty/pull/7681) except for fading vertical and horizontal lines. Adds git_draw_node function and a new Git node type. Add this range (0xf5d0...0xf60d) for Git branch characters, to tests. adds vline_middle_xy and hline_middle_xy for node connections. add git characters to Face.zig. --- src/font/sprite/Box.zig | 398 +++++++++++++++++++++++++++++++++++++++ src/font/sprite/Face.zig | 7 + 2 files changed, 405 insertions(+) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index 477708c99..80369d8fd 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -79,6 +79,15 @@ const Quads = packed struct(u4) { br: bool = false, }; +/// Specification of a git branch node, which can have any of +/// 4 lines connecting to it in vertical or horizontal alignment +const NodeAlign = packed struct(u4) { + up: bool = false, + down: bool = false, + left: bool = false, + right: bool = false, +}; + /// Alignment of a figure within a cell const Alignment = struct { horizontal: enum { @@ -1302,6 +1311,320 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void // '🯯' 0x1fbef => self.draw_circle(canvas, Alignment.top_left, true), + // '' + 0x0f5d0 => self.hline_middle(canvas, Thickness.light), + + // '' + 0x0f5d1 => self.vline_middle(canvas, Thickness.light), + + // '' + // 0x0f5d2 => self.draw_dash_fading(canvas, 4, Direction.LEFT, Thickness.light.height(self.thickness)), + + // '' + // 0x0f5d3 => self.draw_dash_fading(canvas, 4, Direction.RIGHT, Thickness.light.height(self.thickness)), + + // '' + // 0x0f5d4 => self.draw_dash_fading(canvas, 5, Direction.UP, Thickness.light.height(self.thickness)), + + // '' + // 0x0f5d5 => self.draw_dash_fading(canvas, 5, Direction.DOWN, Thickness.light.height(self.thickness)), + + // '' + 0x0f5d6 => try self.draw_light_arc(canvas, .br), + + // '' + 0x0f5d7 => try self.draw_light_arc(canvas, .bl), + + // '' + 0x0f5d8 => try self.draw_light_arc(canvas, .tr), + + // '' + 0x0f5d9 => try self.draw_light_arc(canvas, .tl), + + // '' + 0x0f5da => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tr); + }, + + // '' + 0x0f5db => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .br); + }, + + // '' + 0x0f5dc => { + try self.draw_light_arc(canvas, .tr); + try self.draw_light_arc(canvas, .br); + }, + + // '' + 0x0f5dd => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tl); + }, + + // '' + 0x0f5de => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .bl); + }, + + // '' + 0x0f5df => { + try self.draw_light_arc(canvas, .tl); + try self.draw_light_arc(canvas, .bl); + }, + + // '' + 0x0f5e0 => { + try self.draw_light_arc(canvas, .bl); + self.hline_middle(canvas, Thickness.light); + }, + + // '' + 0x0f5e1 => { + try self.draw_light_arc(canvas, .br); + self.hline_middle(canvas, Thickness.light); + }, + + // '' + 0x0f5e2 => { + try self.draw_light_arc(canvas, .br); + try self.draw_light_arc(canvas, .bl); + }, + + // '' + 0x0f5e3 => { + try self.draw_light_arc(canvas, .tl); + self.hline_middle(canvas, Thickness.light); + }, + + // '' + 0x0f5e4 => { + try self.draw_light_arc(canvas, .tr); + self.hline_middle(canvas, Thickness.light); + }, + + // '' + 0x0f5e5 => { + try self.draw_light_arc(canvas, .tr); + try self.draw_light_arc(canvas, .tl); + }, + + // '' + 0x0f5e6 => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tl); + try self.draw_light_arc(canvas, .tr); + }, + + // '' + 0x0f5e7 => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .bl); + try self.draw_light_arc(canvas, .br); + }, + + // '' + 0x0f5e8 => { + self.hline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .bl); + try self.draw_light_arc(canvas, .tl); + }, + + // '' + 0x0f5e9 => { + self.hline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tr); + try self.draw_light_arc(canvas, .br); + }, + + // '' + 0x0f5ea => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tl); + try self.draw_light_arc(canvas, .br); + }, + + // '' + 0x0f5eb => { + self.vline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tr); + try self.draw_light_arc(canvas, .bl); + }, + + // '' + 0x0f5ec => { + self.hline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tl); + try self.draw_light_arc(canvas, .br); + }, + + // '' + 0x0f5ed => { + self.hline_middle(canvas, Thickness.light); + try self.draw_light_arc(canvas, .tr); + try self.draw_light_arc(canvas, .bl); + }, + + // '' + 0x0f5ee => self.draw_git_node(canvas, .{}, Thickness.light, true), + + // '' + 0x0f5ef => self.draw_git_node(canvas, .{}, Thickness.light, false), + + // '' + 0x0f5f0 => { + self.draw_git_node(canvas, .{ .right = true }, Thickness.light, true); + }, + + // '' + 0x0f5f1 => { + self.draw_git_node(canvas, .{ .right = true }, Thickness.light, false); + }, + + // '' + 0x0f5f2 => { + self.draw_git_node(canvas, .{ .left = true }, Thickness.light, true); + }, + + // '' + 0x0f5f3 => { + self.draw_git_node(canvas, .{ .left = true }, Thickness.light, false); + }, + + // '' + 0x0f5f4 => { + self.draw_git_node(canvas, .{ .left = true, .right = true }, Thickness.light, true); + }, + + // '' + 0x0f5f5 => { + self.draw_git_node(canvas, .{ .left = true, .right = true }, Thickness.light, false); + }, + + // '' + 0x0f5f6 => { + self.draw_git_node(canvas, .{ .down = true }, Thickness.light, true); + }, + + // '' + 0x0f5f7 => { + self.draw_git_node(canvas, .{ .down = true }, Thickness.light, false); + }, + + // '' + 0x0f5f8 => { + self.draw_git_node(canvas, .{ .up = true }, Thickness.light, true); + }, + + // '' + 0x0f5f9 => { + self.draw_git_node(canvas, .{ .up = true }, Thickness.light, false); + }, + + // '' + 0x0f5fa => { + self.draw_git_node(canvas, .{ .up = true, .down = true }, Thickness.light, true); + }, + + // '' + 0x0f5fb => { + self.draw_git_node(canvas, .{ .up = true, .down = true }, Thickness.light, false); + }, + + // '' + 0x0f5fc => { + self.draw_git_node(canvas, .{ .right = true, .down = true }, Thickness.light, true); + }, + + // '' + 0x0f5fd => { + self.draw_git_node(canvas, .{ .right = true, .down = true }, Thickness.light, false); + }, + + // '' + 0x0f5fe => { + self.draw_git_node(canvas, .{ .left = true, .down = true }, Thickness.light, true); + }, + + // '' + 0x0f5ff => { + self.draw_git_node(canvas, .{ .left = true, .down = true }, Thickness.light, false); + }, + + // '' + 0x0f600 => { + self.draw_git_node(canvas, .{ .up = true, .right = true }, Thickness.light, true); + }, + + // '' + 0x0f601 => { + self.draw_git_node(canvas, .{ .up = true, .right = true }, Thickness.light, false); + }, + + // '' + 0x0f602 => { + self.draw_git_node(canvas, .{ .up = true, .left = true }, Thickness.light, true); + }, + + // '' + 0x0f603 => { + self.draw_git_node(canvas, .{ .up = true, .left = true }, Thickness.light, false); + }, + + // '' + 0x0f604 => { + self.draw_git_node(canvas, .{ .up = true, .down = true, .right = true }, Thickness.light, true); + }, + + // '' + 0x0f605 => { + self.draw_git_node(canvas, .{ .up = true, .down = true, .right = true }, Thickness.light, false); + }, + + // '' + 0x0f606 => { + self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true }, Thickness.light, true); + }, + + // '' + 0x0f607 => { + self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true }, Thickness.light, false); + }, + + // '' + 0x0f608 => { + self.draw_git_node(canvas, .{ .down = true, .left = true, .right = true }, Thickness.light, true); + }, + + // '' + 0x0f609 => { + self.draw_git_node(canvas, .{ .down = true, .left = true, .right = true }, Thickness.light, false); + }, + + // '' + 0x0f60a => { + self.draw_git_node(canvas, .{ .up = true, .left = true, .right = true }, Thickness.light, true); + }, + + // '' + 0x0f60b => { + self.draw_git_node(canvas, .{ .up = true, .left = true, .right = true }, Thickness.light, false); + }, + + // '' + 0x0f60c => { + self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true, .right = true }, Thickness.light, true); + }, + + // '' + 0x0f60d => { + self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true, .right = true }, Thickness.light, false); + }, + // Not official box characters but special characters we hide // in the high bits of a unicode codepoint. @intFromEnum(Sprite.cursor_rect) => self.draw_cursor_rect(canvas), @@ -1793,6 +2116,57 @@ fn draw_cell_diagonal( ) catch {}; } +fn draw_git_node( + self: Box, + canvas: *font.sprite.Canvas, + comptime nodes: NodeAlign, + comptime thickness: Thickness, + comptime filled: bool, +) void { + const float_width: f64 = @floatFromInt(self.width); + const float_height: f64 = @floatFromInt(self.height); + + const x: f64 = float_width / 2; + const y: f64 = float_height / 2; + + // we need at least 1px leeway to see node + right/left line + const r: f64 = 0.47 * @min(float_width, float_height); + + var ctx: z2d.Context = .{ + .surface = canvas.sfc, + .pattern = .{ + .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, + }, + }, + .line_width = @floatFromInt(thickness.height(self.thickness)), + }; + + var path = z2d.Path.init(canvas.alloc); + defer path.deinit(); + + const int_radius: u32 = @intFromFloat(r); + + if (nodes.up) + self.vline_middle_xy(canvas, 0, (self.height / 2) - int_radius, self.width, thickness); + if (nodes.down) + self.vline_middle_xy(canvas, (self.height / 2) + int_radius, self.height, self.width, thickness); + if (nodes.left) + self.hline_middle_xy(canvas, 0, (self.width / 2) - int_radius, self.height, thickness); + if (nodes.right) + self.hline_middle_xy(canvas, (self.width / 2) + int_radius, self.width, self.height, thickness); + + if (filled) { + path.arc(x, y, r, 0, std.math.pi * 2, false, null) catch return; + path.close() catch return; + ctx.fill(canvas.alloc, path) catch return; + } else { + path.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2, false, null) catch return; + path.close() catch return; + ctx.stroke(canvas.alloc, path) catch return; + } +} + fn draw_circle( self: Box, canvas: *font.sprite.Canvas, @@ -2377,6 +2751,16 @@ fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void { self.vline(canvas, 0, self.height, 0, thick_px); } +fn vline_middle_xy(self: Box, canvas: *font.sprite.Canvas, y: u32, y2: u32, x: u32, thickness: Thickness) void { + const thick_px = thickness.height(self.thickness); + self.vline(canvas, y, y2, (x -| thick_px) / 2, thick_px); +} + +fn hline_middle_xy(self: Box, canvas: *font.sprite.Canvas, x: u32, x2: u32, y: u32, thickness: Thickness) void { + const thick_px = thickness.height(self.thickness); + self.hline(canvas, x, x2, (y -| thick_px) / 2, thick_px); +} + fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { const thick_px = thickness.height(self.thickness); self.vline(canvas, 0, self.height, (self.width -| thick_px) / 2, thick_px); @@ -2480,6 +2864,20 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { ); } + // Git Branch characters + cp = 0xf5d0; + while (cp <= 0xf60d) : (cp += 1) { + switch (cp) { + 0xf5d0...0xf60d, + => _ = try self.renderGlyph( + alloc, + atlas, + cp, + ), + else => {}, + } + } + // Symbols for Legacy Computing. cp = 0x1fb00; while (cp <= 0x1fbef) : (cp += 1) { diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index af82bb731..650bd2a5f 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -276,6 +276,13 @@ const Kind = enum { 0xE0D4, => .powerline, + // (Git Branch) + //           + //                     + //                     + //             + 0xF5D0...0xF60D => .box, + else => null, }; } From 4dbf404dc330d6af610519abeecfc7eb2d8b4617 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 6 Nov 2024 18:27:22 -0500 Subject: [PATCH 20/46] font/sprite: cleanup branch drawing character impl, implement fade-out lines --- src/font/sprite/Box.zig | 619 ++++++++++++++++++------------- src/font/sprite/Face.zig | 22 +- src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 1048593 bytes 3 files changed, 371 insertions(+), 270 deletions(-) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index 80369d8fd..382aa4206 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -79,13 +79,15 @@ const Quads = packed struct(u4) { br: bool = false, }; -/// Specification of a git branch node, which can have any of -/// 4 lines connecting to it in vertical or horizontal alignment -const NodeAlign = packed struct(u4) { +/// Specification of a branch drawing node, which consists of a +/// circle which is either empty or filled, and lines connecting +/// optionally between the circle and each of the 4 edges. +const BranchNode = packed struct(u5) { up: bool = false, + right: bool = false, down: bool = false, left: bool = false, - right: bool = false, + filled: bool = false, }; /// Alignment of a figure within a cell @@ -483,14 +485,14 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void // '╬' 0x256c => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), // '╭' - 0x256d => try self.draw_light_arc(canvas, .br), + 0x256d => try self.draw_arc(canvas, .br, .light), // '╮' - 0x256e => try self.draw_light_arc(canvas, .bl), + 0x256e => try self.draw_arc(canvas, .bl, .light), // '╯' - 0x256f => try self.draw_light_arc(canvas, .tl), + 0x256f => try self.draw_arc(canvas, .tl, .light), // '╰' - 0x2570 => try self.draw_light_arc(canvas, .tr), + 0x2570 => try self.draw_arc(canvas, .tr, .light), // '╱' 0x2571 => self.draw_light_diagonal_upper_right_to_lower_left(canvas), // '╲' @@ -1311,319 +1313,329 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void // '🯯' 0x1fbef => self.draw_circle(canvas, Alignment.top_left, true), + // (Below:) + // Branch drawing character set, used for drawing git-like + // graphs in the terminal. Originally implemented in Kitty. + // Ref: + // - https://github.com/kovidgoyal/kitty/pull/7681 + // - https://github.com/kovidgoyal/kitty/pull/7805 + // NOTE: Kitty is GPL licensed, and its code was not referenced + // for these characters, only the loose specification of + // the character set in the pull request descriptions. + // + // TODO(qwerasd): This should be in another file, but really the + // general organization of the sprite font code + // needs to be reworked eventually. + // + //           + //                     + //                     + //             + // '' - 0x0f5d0 => self.hline_middle(canvas, Thickness.light), - + 0x0f5d0 => self.hline_middle(canvas, .light), // '' - 0x0f5d1 => self.vline_middle(canvas, Thickness.light), - + 0x0f5d1 => self.vline_middle(canvas, .light), // '' - // 0x0f5d2 => self.draw_dash_fading(canvas, 4, Direction.LEFT, Thickness.light.height(self.thickness)), - + 0x0f5d2 => self.draw_fading_line(canvas, .right, .light), // '' - // 0x0f5d3 => self.draw_dash_fading(canvas, 4, Direction.RIGHT, Thickness.light.height(self.thickness)), - + 0x0f5d3 => self.draw_fading_line(canvas, .left, .light), // '' - // 0x0f5d4 => self.draw_dash_fading(canvas, 5, Direction.UP, Thickness.light.height(self.thickness)), - + 0x0f5d4 => self.draw_fading_line(canvas, .bottom, .light), // '' - // 0x0f5d5 => self.draw_dash_fading(canvas, 5, Direction.DOWN, Thickness.light.height(self.thickness)), - + 0x0f5d5 => self.draw_fading_line(canvas, .top, .light), // '' - 0x0f5d6 => try self.draw_light_arc(canvas, .br), - + 0x0f5d6 => try self.draw_arc(canvas, .br, .light), // '' - 0x0f5d7 => try self.draw_light_arc(canvas, .bl), - + 0x0f5d7 => try self.draw_arc(canvas, .bl, .light), // '' - 0x0f5d8 => try self.draw_light_arc(canvas, .tr), - + 0x0f5d8 => try self.draw_arc(canvas, .tr, .light), // '' - 0x0f5d9 => try self.draw_light_arc(canvas, .tl), - + 0x0f5d9 => try self.draw_arc(canvas, .tl, .light), // '' 0x0f5da => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tr); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .tr, .light); }, - // '' 0x0f5db => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .br); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .br, .light); }, - // '' 0x0f5dc => { - try self.draw_light_arc(canvas, .tr); - try self.draw_light_arc(canvas, .br); + try self.draw_arc(canvas, .tr, .light); + try self.draw_arc(canvas, .br, .light); }, - // '' 0x0f5dd => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tl); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .tl, .light); }, - // '' 0x0f5de => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .bl); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .bl, .light); }, - // '' 0x0f5df => { - try self.draw_light_arc(canvas, .tl); - try self.draw_light_arc(canvas, .bl); + try self.draw_arc(canvas, .tl, .light); + try self.draw_arc(canvas, .bl, .light); }, // '' 0x0f5e0 => { - try self.draw_light_arc(canvas, .bl); - self.hline_middle(canvas, Thickness.light); + try self.draw_arc(canvas, .bl, .light); + self.hline_middle(canvas, .light); }, - // '' 0x0f5e1 => { - try self.draw_light_arc(canvas, .br); - self.hline_middle(canvas, Thickness.light); + try self.draw_arc(canvas, .br, .light); + self.hline_middle(canvas, .light); }, - // '' 0x0f5e2 => { - try self.draw_light_arc(canvas, .br); - try self.draw_light_arc(canvas, .bl); + try self.draw_arc(canvas, .br, .light); + try self.draw_arc(canvas, .bl, .light); }, - // '' 0x0f5e3 => { - try self.draw_light_arc(canvas, .tl); - self.hline_middle(canvas, Thickness.light); + try self.draw_arc(canvas, .tl, .light); + self.hline_middle(canvas, .light); }, - // '' 0x0f5e4 => { - try self.draw_light_arc(canvas, .tr); - self.hline_middle(canvas, Thickness.light); + try self.draw_arc(canvas, .tr, .light); + self.hline_middle(canvas, .light); }, - // '' 0x0f5e5 => { - try self.draw_light_arc(canvas, .tr); - try self.draw_light_arc(canvas, .tl); + try self.draw_arc(canvas, .tr, .light); + try self.draw_arc(canvas, .tl, .light); }, - // '' 0x0f5e6 => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tl); - try self.draw_light_arc(canvas, .tr); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .tl, .light); + try self.draw_arc(canvas, .tr, .light); }, - // '' 0x0f5e7 => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .bl); - try self.draw_light_arc(canvas, .br); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .bl, .light); + try self.draw_arc(canvas, .br, .light); }, - // '' 0x0f5e8 => { - self.hline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .bl); - try self.draw_light_arc(canvas, .tl); + self.hline_middle(canvas, .light); + try self.draw_arc(canvas, .bl, .light); + try self.draw_arc(canvas, .tl, .light); }, - // '' 0x0f5e9 => { - self.hline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tr); - try self.draw_light_arc(canvas, .br); + self.hline_middle(canvas, .light); + try self.draw_arc(canvas, .tr, .light); + try self.draw_arc(canvas, .br, .light); }, - // '' 0x0f5ea => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tl); - try self.draw_light_arc(canvas, .br); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .tl, .light); + try self.draw_arc(canvas, .br, .light); }, - // '' 0x0f5eb => { - self.vline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tr); - try self.draw_light_arc(canvas, .bl); + self.vline_middle(canvas, .light); + try self.draw_arc(canvas, .tr, .light); + try self.draw_arc(canvas, .bl, .light); }, - // '' 0x0f5ec => { - self.hline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tl); - try self.draw_light_arc(canvas, .br); + self.hline_middle(canvas, .light); + try self.draw_arc(canvas, .tl, .light); + try self.draw_arc(canvas, .br, .light); }, - // '' 0x0f5ed => { - self.hline_middle(canvas, Thickness.light); - try self.draw_light_arc(canvas, .tr); - try self.draw_light_arc(canvas, .bl); + self.hline_middle(canvas, .light); + try self.draw_arc(canvas, .tr, .light); + try self.draw_arc(canvas, .bl, .light); }, - // '' - 0x0f5ee => self.draw_git_node(canvas, .{}, Thickness.light, true), - + 0x0f5ee => self.draw_branch_node(canvas, .{ .filled = true }, .light), // '' - 0x0f5ef => self.draw_git_node(canvas, .{}, Thickness.light, false), + 0x0f5ef => self.draw_branch_node(canvas, .{}, .light), // '' - 0x0f5f0 => { - self.draw_git_node(canvas, .{ .right = true }, Thickness.light, true); - }, - + 0x0f5f0 => self.draw_branch_node(canvas, .{ + .right = true, + .filled = true, + }, .light), // '' - 0x0f5f1 => { - self.draw_git_node(canvas, .{ .right = true }, Thickness.light, false); - }, - + 0x0f5f1 => self.draw_branch_node(canvas, .{ + .right = true, + }, .light), // '' - 0x0f5f2 => { - self.draw_git_node(canvas, .{ .left = true }, Thickness.light, true); - }, - + 0x0f5f2 => self.draw_branch_node(canvas, .{ + .left = true, + .filled = true, + }, .light), // '' - 0x0f5f3 => { - self.draw_git_node(canvas, .{ .left = true }, Thickness.light, false); - }, - + 0x0f5f3 => self.draw_branch_node(canvas, .{ + .left = true, + }, .light), // '' - 0x0f5f4 => { - self.draw_git_node(canvas, .{ .left = true, .right = true }, Thickness.light, true); - }, - + 0x0f5f4 => self.draw_branch_node(canvas, .{ + .left = true, + .right = true, + .filled = true, + }, .light), // '' - 0x0f5f5 => { - self.draw_git_node(canvas, .{ .left = true, .right = true }, Thickness.light, false); - }, - + 0x0f5f5 => self.draw_branch_node(canvas, .{ + .left = true, + .right = true, + }, .light), // '' - 0x0f5f6 => { - self.draw_git_node(canvas, .{ .down = true }, Thickness.light, true); - }, - + 0x0f5f6 => self.draw_branch_node(canvas, .{ + .down = true, + .filled = true, + }, .light), // '' - 0x0f5f7 => { - self.draw_git_node(canvas, .{ .down = true }, Thickness.light, false); - }, - + 0x0f5f7 => self.draw_branch_node(canvas, .{ + .down = true, + }, .light), // '' - 0x0f5f8 => { - self.draw_git_node(canvas, .{ .up = true }, Thickness.light, true); - }, - + 0x0f5f8 => self.draw_branch_node(canvas, .{ + .up = true, + .filled = true, + }, .light), // '' - 0x0f5f9 => { - self.draw_git_node(canvas, .{ .up = true }, Thickness.light, false); - }, - + 0x0f5f9 => self.draw_branch_node(canvas, .{ + .up = true, + }, .light), // '' - 0x0f5fa => { - self.draw_git_node(canvas, .{ .up = true, .down = true }, Thickness.light, true); - }, - + 0x0f5fa => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .filled = true, + }, .light), // '' - 0x0f5fb => { - self.draw_git_node(canvas, .{ .up = true, .down = true }, Thickness.light, false); - }, - + 0x0f5fb => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + }, .light), // '' - 0x0f5fc => { - self.draw_git_node(canvas, .{ .right = true, .down = true }, Thickness.light, true); - }, - + 0x0f5fc => self.draw_branch_node(canvas, .{ + .right = true, + .down = true, + .filled = true, + }, .light), // '' - 0x0f5fd => { - self.draw_git_node(canvas, .{ .right = true, .down = true }, Thickness.light, false); - }, - + 0x0f5fd => self.draw_branch_node(canvas, .{ + .right = true, + .down = true, + }, .light), // '' - 0x0f5fe => { - self.draw_git_node(canvas, .{ .left = true, .down = true }, Thickness.light, true); - }, - + 0x0f5fe => self.draw_branch_node(canvas, .{ + .left = true, + .down = true, + .filled = true, + }, .light), // '' - 0x0f5ff => { - self.draw_git_node(canvas, .{ .left = true, .down = true }, Thickness.light, false); - }, + 0x0f5ff => self.draw_branch_node(canvas, .{ + .left = true, + .down = true, + }, .light), // '' - 0x0f600 => { - self.draw_git_node(canvas, .{ .up = true, .right = true }, Thickness.light, true); - }, - + 0x0f600 => self.draw_branch_node(canvas, .{ + .up = true, + .right = true, + .filled = true, + }, .light), // '' - 0x0f601 => { - self.draw_git_node(canvas, .{ .up = true, .right = true }, Thickness.light, false); - }, - + 0x0f601 => self.draw_branch_node(canvas, .{ + .up = true, + .right = true, + }, .light), // '' - 0x0f602 => { - self.draw_git_node(canvas, .{ .up = true, .left = true }, Thickness.light, true); - }, - + 0x0f602 => self.draw_branch_node(canvas, .{ + .up = true, + .left = true, + .filled = true, + }, .light), // '' - 0x0f603 => { - self.draw_git_node(canvas, .{ .up = true, .left = true }, Thickness.light, false); - }, - + 0x0f603 => self.draw_branch_node(canvas, .{ + .up = true, + .left = true, + }, .light), // '' - 0x0f604 => { - self.draw_git_node(canvas, .{ .up = true, .down = true, .right = true }, Thickness.light, true); - }, - + 0x0f604 => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .right = true, + .filled = true, + }, .light), // '' - 0x0f605 => { - self.draw_git_node(canvas, .{ .up = true, .down = true, .right = true }, Thickness.light, false); - }, - + 0x0f605 => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .right = true, + }, .light), // '' - 0x0f606 => { - self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true }, Thickness.light, true); - }, - + 0x0f606 => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .left = true, + .filled = true, + }, .light), // '' - 0x0f607 => { - self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true }, Thickness.light, false); - }, - + 0x0f607 => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .left = true, + }, .light), // '' - 0x0f608 => { - self.draw_git_node(canvas, .{ .down = true, .left = true, .right = true }, Thickness.light, true); - }, - + 0x0f608 => self.draw_branch_node(canvas, .{ + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), // '' - 0x0f609 => { - self.draw_git_node(canvas, .{ .down = true, .left = true, .right = true }, Thickness.light, false); - }, - + 0x0f609 => self.draw_branch_node(canvas, .{ + .down = true, + .left = true, + .right = true, + }, .light), // '' - 0x0f60a => { - self.draw_git_node(canvas, .{ .up = true, .left = true, .right = true }, Thickness.light, true); - }, - + 0x0f60a => self.draw_branch_node(canvas, .{ + .up = true, + .left = true, + .right = true, + .filled = true, + }, .light), // '' - 0x0f60b => { - self.draw_git_node(canvas, .{ .up = true, .left = true, .right = true }, Thickness.light, false); - }, - + 0x0f60b => self.draw_branch_node(canvas, .{ + .up = true, + .left = true, + .right = true, + }, .light), // '' - 0x0f60c => { - self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true, .right = true }, Thickness.light, true); - }, - + 0x0f60c => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), // '' - 0x0f60d => { - self.draw_git_node(canvas, .{ .up = true, .down = true, .left = true, .right = true }, Thickness.light, false); - }, + 0x0f60d => self.draw_branch_node(canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + }, .light), // Not official box characters but special characters we hide // in the high bits of a unicode codepoint. @@ -2116,21 +2128,98 @@ fn draw_cell_diagonal( ) catch {}; } -fn draw_git_node( +fn draw_fading_line( self: Box, canvas: *font.sprite.Canvas, - comptime nodes: NodeAlign, + comptime to: Edge, comptime thickness: Thickness, - comptime filled: bool, ) void { + const thick_px = thickness.height(self.thickness); const float_width: f64 = @floatFromInt(self.width); const float_height: f64 = @floatFromInt(self.height); - const x: f64 = float_width / 2; - const y: f64 = float_height / 2; + // Top of horizontal strokes + const h_top = (self.height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (self.width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; - // we need at least 1px leeway to see node + right/left line - const r: f64 = 0.47 * @min(float_width, float_height); + // If we're fading to the top or left, we start with 0.0 + // and increment up as we progress, otherwise we start + // at 255.0 and increment down (negative). + var color: f64 = switch (to) { + .top, .left => 0.0, + .bottom, .right => 255.0, + }; + const inc: f64 = 255.0 / switch (to) { + .top => float_height, + .bottom => -float_height, + .left => float_width, + .right => -float_width, + }; + + switch (to) { + .top, .bottom => { + for (0..self.height) |y| { + for (v_left..v_right) |x| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + .left, .right => { + for (0..self.width) |x| { + for (h_top..h_bottom) |y| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + } +} + +fn draw_branch_node( + self: Box, + canvas: *font.sprite.Canvas, + node: BranchNode, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(self.thickness); + const float_width: f64 = @floatFromInt(self.width); + const float_height: f64 = @floatFromInt(self.height); + const float_thick: f64 = @floatFromInt(thick_px); + + // Top of horizontal strokes + const h_top = (self.height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (self.width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // We calculate the center of the circle this way + // to ensure it aligns with box drawing characters + // since the lines are sometimes off center to + // make sure they aren't split between pixels. + const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2; + const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2; + // The radius needs to be the smallest distance from the center to an edge. + const r: f64 = @min( + @min(cx, cy), + @min(float_width - cx, float_height - cy), + ); var ctx: z2d.Context = .{ .surface = canvas.sfc, @@ -2139,29 +2228,30 @@ fn draw_git_node( .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, }, }, - .line_width = @floatFromInt(thickness.height(self.thickness)), + .line_width = float_thick, }; var path = z2d.Path.init(canvas.alloc); defer path.deinit(); - const int_radius: u32 = @intFromFloat(r); + // These @intFromFloat casts shouldn't ever fail since r can never + // be greater than cx or cy, so when subtracting it from them the + // result can never be negative. + if (node.up) + self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r))); + if (node.right) + self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.width, h_bottom); + if (node.down) + self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.height); + if (node.left) + self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom); - if (nodes.up) - self.vline_middle_xy(canvas, 0, (self.height / 2) - int_radius, self.width, thickness); - if (nodes.down) - self.vline_middle_xy(canvas, (self.height / 2) + int_radius, self.height, self.width, thickness); - if (nodes.left) - self.hline_middle_xy(canvas, 0, (self.width / 2) - int_radius, self.height, thickness); - if (nodes.right) - self.hline_middle_xy(canvas, (self.width / 2) + int_radius, self.width, self.height, thickness); - - if (filled) { - path.arc(x, y, r, 0, std.math.pi * 2, false, null) catch return; + if (node.filled) { + path.arc(cx, cy, r, 0, std.math.pi * 2, false, null) catch return; path.close() catch return; ctx.fill(canvas.alloc, path) catch return; } else { - path.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2, false, null) catch return; + path.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2, false, null) catch return; path.close() catch return; ctx.stroke(canvas.alloc, path) catch return; } @@ -2492,12 +2582,13 @@ fn draw_edge_triangle( try ctx.fill(canvas.alloc, path); } -fn draw_light_arc( +fn draw_arc( self: Box, canvas: *font.sprite.Canvas, comptime corner: Corner, + comptime thickness: Thickness, ) !void { - const thick_px = Thickness.light.height(self.thickness); + const thick_px = thickness.height(self.thickness); const float_width: f64 = @floatFromInt(self.width); const float_height: f64 = @floatFromInt(self.height); const float_thick: f64 = @floatFromInt(thick_px); @@ -2751,16 +2842,6 @@ fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void { self.vline(canvas, 0, self.height, 0, thick_px); } -fn vline_middle_xy(self: Box, canvas: *font.sprite.Canvas, y: u32, y2: u32, x: u32, thickness: Thickness) void { - const thick_px = thickness.height(self.thickness); - self.vline(canvas, y, y2, (x -| thick_px) / 2, thick_px); -} - -fn hline_middle_xy(self: Box, canvas: *font.sprite.Canvas, x: u32, x2: u32, y: u32, thickness: Thickness) void { - const thick_px = thickness.height(self.thickness); - self.hline(canvas, x, x2, (y -| thick_px) / 2, thick_px); -} - fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { const thick_px = thickness.height(self.thickness); self.vline(canvas, 0, self.height, (self.width -| thick_px) / 2, thick_px); @@ -2864,20 +2945,6 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { ); } - // Git Branch characters - cp = 0xf5d0; - while (cp <= 0xf60d) : (cp += 1) { - switch (cp) { - 0xf5d0...0xf60d, - => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - // Symbols for Legacy Computing. cp = 0x1fb00; while (cp <= 0x1fbef) : (cp += 1) { @@ -2932,6 +2999,32 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { else => {}, } } + + // Branch drawing character set, used for drawing git-like + // graphs in the terminal. Originally implemented in Kitty. + // Ref: + // - https://github.com/kovidgoyal/kitty/pull/7681 + // - https://github.com/kovidgoyal/kitty/pull/7805 + // NOTE: Kitty is GPL licensed, and its code was not referenced + // for these characters, only the loose specification of + // the character set in the pull request descriptions. + // + // TODO(qwerasd): This should be in another file, but really the + // general organization of the sprite font code + // needs to be reworked eventually. + // + //           + //                     + //                     + //             + cp = 0xf5d0; + while (cp <= 0xf60d) : (cp += 1) { + _ = try self.renderGlyph( + alloc, + atlas, + cp, + ); + } } test "render all sprites" { diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 650bd2a5f..ca0ed96e8 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -263,6 +263,21 @@ const Kind = enum { 0x1FBCE...0x1FBEF, => .box, + // Branch drawing character set, used for drawing git-like + // graphs in the terminal. Originally implemented in Kitty. + // Ref: + // - https://github.com/kovidgoyal/kitty/pull/7681 + // - https://github.com/kovidgoyal/kitty/pull/7805 + // NOTE: Kitty is GPL licensed, and its code was not referenced + // for these characters, only the loose specification of + // the character set in the pull request descriptions. + // + //           + //                     + //                     + //             + 0xF5D0...0xF60D => .box, + // Powerline fonts 0xE0B0, 0xE0B4, @@ -276,13 +291,6 @@ const Kind = enum { 0xE0D4, => .powerline, - // (Git Branch) - //           - //                     - //                     - //             - 0xF5D0...0xF60D => .box, - else => null, }; } diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm index 29ae539e709deba5b723a5e3fba111c1914a0cd7..1301a4299816fec222672eb24f327db221007b21 100644 GIT binary patch delta 21898 zcmeHOdwdktz2B2$_ID=x%44&6kg&4}U|zW4O?XHYN&tmGYb;X4c;PA^t8BQ$mfret zV*x>pguRRTQAHC!QlV6v>=hj-An~gB3T`N1%LPif_-LUva6i=xwcfp-b7p5}HoIW3 zEalV7{4qOo=A7^Edw%Eq9_P%Cud&M4Sk<_atV~*&yfS5_u+q2mxGW?cYd#5!i6Xi!dxg&EjvX zb^4@%*u4lu+*JXAh&ml-&&JR+SglutXB}8^`W6@-%d<)_+Hlrag2N|`#O}IiE&p7n zCx?;fB77l@yAdjiM7 zgRvaT$`a3UJlehmMmc{9j3HPt1@ifz?%Nm3X87>Lvzdrh>tTFfjTP}yX7g3+Aq6Ks zsZ7P*TcCoo-McrIZDnQR+0KjbamY2dz(5k2a=5eGXvPUcjWnsMDv?B($4zH6?3gpH z?`Z@Z+k9rSDLCM#V8=7fWDvV8U~!-<`f0vTs^i++zCBi()zyjagbf{2z#iu`k{J<| z$K47eH2pdPfmo8WW-*eRH;`fy!eNUcQwc|#H;^pY-lGI=fH_Rl%(AZ~nZ5gCD9s|0gWuN!SukcY2SfgQ8h>6I!Ni4)quBu|(N!*m?C zZtW|;V5qI-9M_ZLKCHvm@yU81Q4)>7zMPC1v?5B zu*JEx`=oTaq7EkN2yfgNOL*ZzPPmB_r-VYiPCPq+*s=S6s1bY43#qudNmXq=bo1O0HO}Qe3+>mg3T- zoZ?Dy9Swzoq2AUSH=Z~zxNyr#lIfpwx)9%8P6qg-QS7(4&|TBo8w%QNN=*e$DN|ef zPn^o*b&X0nQz!m>CAkj5jj+8Oaxk-;F=Z$9YE(-b3{l*i=DyA}lVa??4^}X5Ci*L; zIj4rso;O+Oc}IFFlhP)2xxU(C~UNITQ+FdO34xQC28dEZ22u=~AsL>alh$ z@6sAFQ%!?1Qn`cT74VrgWB~3yFW3d{xNvs^DdJuW!p;rJGGU-kx}MuQjd-`t1~Mj7 z!wDaXr*kl;>BNnv1ilh9#dmZMYQ%Hyy;nuh4(6f}o5+F)ZzYak6Dh>d0Ah>`4^3eq z0(aMwK|Cx7JYs~2yBYBeV>pQHsV8IY@j{G$&S^V~NfI8}!rUB>8T#lkIvlARAy=J3 zISUtRbk&1}Ld13C4u4NaF79}gWfAL0X4HHVBQY3QxVEzskJWkDF_X+tqV@;(krE|B zYXN)nI^x2kn^-Dw@d%v}PJLEFHt|V(^^h}vzRFQMSf+uvUSXS5!>@~?x$-?j)(jG4%nOZcL+T1d56@!Rm4JXd(&JA=^%rr*DBF$|52^Ulpt!Cr>7r z!XmKAGhT$j3WcmJO|*KPI+b7wiy4Iu$R;fdNSd-wW@Tx8pX2P=#PaAoFWBT6+oE^? z^dVs|7|xwb91pqf0op`;QcfQaq!?{KTz1$^MY%M-w5srf3A;{3 zWx`ZHlaKg6;y7osNNSVszY+EjT-QX4eNxTkR3pPOW6x?3u;X<|$L7gk!Ugw(N%npV zUP_3_C|gxWZaUma{1u;?@P_r^h_eG6Z&x^RWhD_062p(ZWMD~;HO>xiyj|bKm6b(E z#yY&PI#bzNfp6?;TfBdVtMWKZ$8hH%pP3Gm*uaZy_0?ymH{R}Vg3E@+VXA0XR&^?} zRqX}0dM~~ad^J7Qy4q)sQ=YV?*m^^gFtFa}h*{ZdYy5;?T^|{bo!H|67hCL)#n}hI z$-<6{oyxIo28{7nFy4#6mh26+VxR&Xakfz6?H?zk?ACy^iSYj`B)daF46t1&bsJdQ z5WZqp3Z2gLLfVD)O8ad7UCp=tJtE+3^)MK#mXIno7fRVQaA5F$$Yyy+7S6s|$v(;> zc}O{rEmiNOSTM~4sVqy(!u%!dwfi-##L)^{uv`^WH~TUU^;P2@E^DXi;41Oem#`ks`Zf#Jm4cl`;$0~9sJjI1KG+oAhe_kB*f@JCygc&oVp}s&jcxwVPMIU& z|C-Q<1~q;5x%#>dxBja4xxdGlCPpahazHvqE~D}i133gMo`y0U_cXY|Cl3t8##_NG zr+L80J3yHO7MRofPUL-X)R=zb38C!ZT#&%BSONj*&*UPMe|X6)ejM9^b>G3#CFqE(WVd7h^SW(F|a)1`f2j7C-1hYMmi#9*Fu)93DkS&F7#0n3L;WLNbN;t<4 zdvypsy9684X^DJrcUZm&+m?X&>tIj^GXkY|Q|vAOLCY=)rL^T1aN^N})Pk#@hIJ8E zdbs`lC&l0fqBl=WXD0!hTVYKY^E54vPz(>{p-TmRuo=v_>UsFL%ZOQK6s_oa!)U}O zHp63K;3KpkimXFE_y_n!a%@>!beW6Qi&yrJpdd@LX2?wkT@b10 z@JSgQ1uGo%J(PaJ{$t0oCaDKHv|1`g9q*w-t|+933DzHgG3t|BfT43;JQbljn1uV> zeGpd4jWg-(geiwcx;BP&=p(g5pKuJFa#{+h3nfOU@frFAc8bYXwu|vAmeaYg@dzEj zg)-!{q~|0W?nxo-U}93R<^bDJEu^1f<7ryLpUP4Xj3wCd480?Q_g%FU)mr!bL=n91 z9y&Kx4-2&3^06(~4N=-;`k)Y<`LAT@|IEmWpyeUj6_9F@zOK?9{AbnyuDqUE^hF~7 zaTR@u$kHV7J73$lUlO`}lBlSq!a-jmk&deK$44`gFl`#pfV51Ln1=OJp#r7JkcsVn zIycgfG*)J2oi$;bmx`EH4LNbD6b7V*%Ltc?-)*2@q0~T+MtTsQWAERiOm_WL%A%um zjIXY)6b7VoQJUFIzmI2Lpo4Wvt=mke@fTbHX=m7`zt=!D<9GO^NA(@NIK*!*L0rG< z0@go7O)b?*08eLkb0a4Qc8nIwvDqisu=Q0zz^*3x2wMI^Ykkre{UqKm0Z$56+`Eo8 zq2&u&8<75&XAgWmZeqD!d|G8j(SnctyI{tqvy4$DRz68{(emH@huFS>K7|jRphld# zj}BpbsS=)HmeBhqwaGhPr-ukmYNC#d>u38$hVn5(*&mJ4Nv-nsx9Eq2y@M3+nHOk& zUn7f})h;H@gqA{SjNv1Kxg!=Hd09&}<$P!z_1Ei!8rOe?wSKBeMMq zZrHS6LQc@@@#zE9j)ylzgdAo<{!1q$7td^`Ci&HU^tazg$YE;6u4A+aHy)swcz8=h z(lI9KchQoz(Ny`B{qz{YjW5uGes$t79fr>ypy_z{MFpDTyc0ARum4YKM%+d%a^oT9 z8ZQCRjL&z_cBXZT^FC%+A2O`%)QaP(A(Kfe!qYd1S#sB5x>uX3X;{CRj%2LMBik@8 zY}!WmvfBbI{WUdXcR8dW?xI%w%P~bEgYb_xh<3T_*w449^NhyAL zh@rI6;_%jsQGWm9_&|fOxs$$woz;+wp3ej`c0Vrw)^`(-ANm8e_ioIxcc+#euI1M(kQg=V9}cg5BRa&nSQSISc(A z)i8z`4Pe(BbdsFiO~(`bwjd7j@0e#{j7;c!LGfJkm?I0fJs+>_LEcsr2VuzxV`)J8 z3sLjWBE5*;@@f{b2#Y^9mR+#gQC=-wEW$A*MhlkgrzZJfLHwn_15({zkP@@$~4lxKZ3c~y+?VnERIA>Xsi{)KeEUSWur%>@e;&{2D z3v&-qtNegP{FRQ4`IrV(sDo-e!ZP4@Q#dt?UK79Pcm<*h^G;BU+>kEr)Z=w&cq96S z_eYNB5?y%DSAs=u$co0>so_oWrAs&mL0RH3okbBl!+{2iu2ijwyfjyQO;1nv0az`E z^Ft}ivUpc}R)H8Eg+Ft!648ZepHuKjg1oFi+@(kQsV3!aj?@fgK8eO4o#RMD#X>9- zM3cPCCBDR`yNz!(j|!dKyLIi-+Di6+!u-nGrE9nD4S&A7MicNu?b6_G4TkWp#~~*< z6l_J;6d1{Zaz>cz2X$rdLP zymgf5^5vys@1H5i|5_rxqU*ZlZjH`Q4LHpYBYYCyo=e3K-=L%9>=f|^g0+>R3r~Me zQ}N6>D#$+?D!!@*(f!kVs{yBB3<%{Q6WI`aOaVXpE2TK|D@x^uhecb%6phYKJmCh3 z?->N7Jp`u{(JVhaD!Tp%t$vrjekZ{hY=|DNjILj()jt-lAGTw@eO(?l=J0Oz!tkHO zM)J&^E!>u{rIXjt3SXtnE1Fl=uH8si6n*GIOhP^O~J^w18#h-6ta@sL0@_) z-t7g4)%}I`FyzL&N+C1+K=(CT$c+oUFu-f$-wcL=>{F>=tDBiOzvQ1eCBD>qWySTC zX28_n3$_bu+oNc^)uY;qhijX7ko=f^nq)!mK+&9|dwxW#8uEm7zXBXqk6!m4T(ANL zc*EZkhFT4V=4LK|{amq9@0AtTSDFFmKWMz5?p>kd9(5Y)ROb$tx;@9)NMt6n3)d7V z!9}aVk>~atzfG&5hrl~lLqUeyb6f|;o@3o=$o2LrpY?@$4TkV1z)p5~rAob5R$O0c zhNw^A66#~J;&YLK#Yn3DHc`X2MVlK*h~t%szK?RvQI}fiwj$9f(*x-DBynLdSfgn zzO&FDm(#JQ$HWG7enoQv(pF`>&5_t+XjNj5MHhBFt$4=@a3*<=JuZ=!(viCYj8q4Z;ia~?E0?utL&m$5XEt}GYLW;OU3 z3_&G_v#1V{nOqR>@PaeR{RJ~s{s;EdWR~nL@Unr7wAD2+=u*a@GLQ+%{k(|+?kDcR zm^3dcvu*jkkj382(SE!AoC9plvPN6y%#2=gP5S@7X8FG6@h+n+#COZ8h42NZejua@ zI+eYmyqUkX%Aq&0cYEdR-5!5QX5#M_@6g+HIK8`k>35Vn{$>R4jGC*Le-##$A$%i> s;({xZAnM8n~VQA0ybood5s; delta 7484 zcmeHLeQ;FO75A;Xd(X++B%6e6Q1Wp%u!%sFEq(+ckeK3h2!!m6t)Qqxu#!Rwj4+BV z>UPMCbYM*0h4!IQQ6drn_1oEh_Q>0O=6Y?l{KM3+Sh)fz~gWIX&7g%OU zNj(h$>HM!9PC647*Bi2cDMf}HLT6WKKt&OeOI6#h&NlHV_`0aI{D@(iaOE_nQ`$8o7_JBZY_lt-H`AtHv7a25Vs~F3%#iOrsI5TN_zr$n5 zo2aKgIq=`42L7=qH)Ip%`0~UHRIyQ1CVcTymF667$Xoa?(8Nt*6z_nb!c8KNjH1dZ;sz@;&VH=PTvq4k+)ZYAyQuXuN17q0 zTb-81#a_b$GXC}rJTBaE+#t-drl0Yv(4HF zUKVg0j85g|8}dHOujH*hBUWkL<6aJQKx8Gnv!KTRP~4aZDp~pCizxfJ=5$ptQ+#i5 z<%=QvxQo)&qdk|j4Vht5nc_|T z$)i32-c)WL`KQ7k40)~P*Uel|Zd$ci9Qa>&Q)hak{7XftwKHZ5U-3NL5kp?~ts~Ef zAus>dk!LaTuvqH_#Wmk@%4f&m&9TuW@-(i!4CN0;qo~WYC#XZaBUmy*vkU=*c;dK`nQqxJjeHhhU!e zxzsY=nfvt4_XO2HiNkqL7OEvp@YK~T{S{n8f`xh;Yu#ouAwwrIlQKI+F5eS#IC2lk z$@mZjPJ;-Qg4ZJX$v+2l>e-I@sg|PniGZgioJt|KzyXJn&w@KGBDa}73pTEkJPm$M z@&q&Uy6SremZnAXzn~hkNp1u^BtYkUxNWNC6m)A5`I+DVvQ7V#YzMb3;W#aRAKkGW zp5q)w%Oo4jm{q`q4{0P__z@@DZROtd$210-^1lW9(_*XxP|go>yO}BFKMzhV9?200 z<78STIbH=$!%2+-Wmutlim=@tkq7L9MstpR?l}jKv29?HT(0p%d5`B^I1!QmG(#;j z_#Md~4T7&iI1y~pF115{ETTFPB*&ro0uIN*f7>(@4?mcWVQTq4{*rVb=+I;_*^qZyq9|RcO#O#3(~1>AJ=lXr&o+KSNvHZn+=cSPX`X_t;Ny-c{%?uo}wE@p_BevjUn=Hh7I4;;?FLl zcaL9^E%PHu4uy+h7ReRZWoG;9B`aa#9=dZW&Y_y6=%VH)aYoEoo6Q>D^JvCmyq|`> z??76;3h(~1L<7xi#Cob}#0;8LX3L=U%{Yx-YQ<8^$WT?&fIRMsp_~l}YFvkv^vS;+ ziSylZYYmnpyT6pt;s^2owJa50hqq|7y#>oGnW3s?8J^ajZk-}R3&2Ai zw_<@6b(RXhXlkrRugY1DMYapdZ8qgrvRn|0Y{_?q^i=P&RQR{1a1#qReWmaYQ#i=N zE7^nK)WkvjWz#R2(#u(TwdwDV3d~qTC5Xv!z07jEilIR3R_+!owuRQ&yi|3_?j9)l zN=&kl#`&EtmA?ij+4w1toow#4xR6$-vYy2$UzT`1EC-Lfp)a z!@T@A)&bJL6lT)rin(vZxgYH1ojicswTK*RqYw7+@-Vh*5jOfkCwl4KPSjOl4@R^I zdvNMa^wRM+QCCH8W7wpLF}-81x~jREUO0q9X#F8{((ywq@E23SXLByV+O{;e*o8W?9l)K From a420496e952900c02d0f61f794997ca27a566511 Mon Sep 17 00:00:00 2001 From: phillip-hirsch Date: Wed, 6 Nov 2024 19:12:58 -0500 Subject: [PATCH 21/46] feat: Add bat syntax highlighting to macOS package contents --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 414719f10..bc7f468e0 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; }; 55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; }; 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; @@ -95,6 +96,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = ""; }; 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = ""; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; @@ -366,6 +368,7 @@ A5A1F8862A489D7400D1E8BC /* Resources */ = { isa = PBXGroup; children = ( + 29C15B1C2CDC3B2000520DD4 /* bat */, 55154BDF2B33911F001622DC /* ghostty */, 552964E52B34A9B400030505 /* vim */, A586167B2B7703CC009BDB1D /* fish */, @@ -538,6 +541,7 @@ A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */, A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, + 29C15B1D2CDC3B2900520DD4 /* bat in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */, From aed51fd0b00018eeea71ba1afcb48d18792ae46c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Nov 2024 14:15:31 -0800 Subject: [PATCH 22/46] terminal: PageList rename "page" to "node" everywhere This is more correct: a pagelist is a linked list of nodes, not pages. The nodes themselves contain pages but we were previously calling the nodes "pages" which was confusing, especially as I plan some future changes to the way pages are stored. --- src/Surface.zig | 2 +- src/renderer/Metal.zig | 11 +- src/renderer/OpenGL.zig | 4 +- src/renderer/cell.zig | 2 +- src/renderer/link.zig | 8 +- src/terminal/PageList.zig | 706 +++++++++++++++++++------------------ src/terminal/Screen.zig | 232 ++++++------ src/terminal/Selection.zig | 6 +- src/terminal/Terminal.zig | 208 +++++------ 9 files changed, 591 insertions(+), 588 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index d0c199010..10ecfd8f1 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3115,7 +3115,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { /// if there is no hyperlink. fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 { _ = self; - const page = &pin.page.data; + const page = &pin.node.data; const cell = pin.rowAndCell().cell; const link_id = page.lookupHyperlink(cell) orelse return null; const entry = page.hyperlink_set.get(page.memory, link_id); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index cb0f5a3de..742dfbcd4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1040,7 +1040,7 @@ pub fn updateFrame( null, ); while (it.next()) |chunk| { - var dirty_set = chunk.page.data.dirtyBitSet(); + var dirty_set = chunk.node.data.dirtyBitSet(); dirty_set.unsetAll(); } } @@ -2364,7 +2364,7 @@ fn rebuildCells( // True if this cell is selected const selected: bool = if (screen.selection) |sel| sel.contains(screen, .{ - .page = row.page, + .node = row.node, .y = row.y, .x = @intCast( // Spacer tails should show the selection @@ -2512,12 +2512,7 @@ fn rebuildCells( ); }; - if (style.flags.overline) self.addOverline( - @intCast(x), - @intCast(y), - fg, - alpha - ) catch |err| { + if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { log.warn( "error adding overline to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 324fe14b3..5313315b1 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -844,7 +844,7 @@ pub fn updateFrame( null, ); while (it.next()) |chunk| { - var dirty_set = chunk.page.data.dirtyBitSet(); + var dirty_set = chunk.node.data.dirtyBitSet(); dirty_set.unsetAll(); } } @@ -1411,7 +1411,7 @@ pub fn rebuildCells( // True if this cell is selected const selected: bool = if (screen.selection) |sel| sel.contains(screen, .{ - .page = row.page, + .node = row.node, .y = row.y, .x = @intCast( // Spacer tails should show the selection diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 1a315a0d8..c84fbcc6f 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -70,7 +70,7 @@ pub fn fgMode( } // If we are at the end of the screen its definitely constrained - if (cell_pin.x == cell_pin.page.data.size.cols - 1) break :text .constrained; + if (cell_pin.x == cell_pin.node.data.size.cols - 1) break :text .constrained; // If we have a previous cell and it was PUA then we need to // also constrain. This is so that multiple PUA glyphs align. diff --git a/src/renderer/link.zig b/src/renderer/link.zig index aa2db2b8d..994190ec8 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -122,7 +122,7 @@ pub const Set = struct { if (!mouse_cell.hyperlink) return; // Get our hyperlink entry - const page = &mouse_pin.page.data; + const page: *terminal.Page = &mouse_pin.node.data; const link_id = page.lookupHyperlink(mouse_cell) orelse { log.warn("failed to find hyperlink for cell", .{}); return; @@ -165,7 +165,7 @@ pub const Set = struct { for (row_pin.cells(.right), 0..) |*cell, x| { const match = match: { if (cell.hyperlink) { - if (row_pin.page.data.lookupHyperlink(cell)) |cell_link_id| { + if (row_pin.node.data.lookupHyperlink(cell)) |cell_link_id| { break :match cell_link_id == link_id; } } @@ -215,7 +215,7 @@ pub const Set = struct { // Expand it to the left. var it = mouse_pin.cellIterator(.left_up, null); while (it.next()) |cell_pin| { - const page = &cell_pin.page.data; + const page: *terminal.Page = &cell_pin.node.data; const rac = cell_pin.rowAndCell(); const cell = rac.cell; @@ -241,7 +241,7 @@ pub const Set = struct { // Expand it to the right it = mouse_pin.cellIterator(.right_down, null); while (it.next()) |cell_pin| { - const page = &cell_pin.page.data; + const page: *terminal.Page = &cell_pin.node.data; const rac = cell_pin.rowAndCell(); const cell = rac.cell; diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index d86ebc765..f1dbc2561 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -69,15 +69,15 @@ pub const MemoryPool = struct { page_alloc: Allocator, preheat: usize, ) !MemoryPool { - var pool = try NodePool.initPreheated(gen_alloc, preheat); - errdefer pool.deinit(); + var node_pool = try NodePool.initPreheated(gen_alloc, preheat); + errdefer node_pool.deinit(); var page_pool = try PagePool.initPreheated(page_alloc, preheat); errdefer page_pool.deinit(); var pin_pool = try PinPool.initPreheated(gen_alloc, 8); errdefer pin_pool.deinit(); return .{ .alloc = gen_alloc, - .nodes = pool, + .nodes = node_pool, .pages = page_pool, .pins = pin_pool, }; @@ -265,7 +265,7 @@ fn initPages( const cap = try std_capacity.adjust(.{ .cols = cols }); var rem = rows; while (rem > 0) { - const page = try pool.nodes.create(); + const node = try pool.nodes.create(); const page_buf = try pool.pages.create(); // no errdefer because the pool deinit will clean these up @@ -276,17 +276,17 @@ fn initPages( // Initialize the first set of pages to contain our viewport so that // the top of the first page is always the active area. - page.* = .{ + node.* = .{ .data = Page.initBuf( OffsetBuf.init(page_buf), Page.layout(cap), ), }; - page.data.size.rows = @min(rem, page.data.capacity.rows); - rem -= page.data.size.rows; + node.data.size.rows = @min(rem, node.data.capacity.rows); + rem -= node.data.size.rows; // Add the page to the list - page_list.append(page); + page_list.append(node); page_size += page_buf.len; } @@ -412,23 +412,23 @@ pub fn clone( while (it.next()) |chunk| { // Clone the page. We have to use createPageExt here because // we don't know if the source page has a standard size. - const page = try createPageExt( + const node = try createPageExt( pool, - chunk.page.data.capacity, + chunk.node.data.capacity, &page_size, ); - assert(page.data.capacity.rows >= chunk.end - chunk.start); - defer page.data.assertIntegrity(); - page.data.size.rows = chunk.end - chunk.start; - try page.data.cloneFrom( - &chunk.page.data, + assert(node.data.capacity.rows >= chunk.end - chunk.start); + defer node.data.assertIntegrity(); + node.data.size.rows = chunk.end - chunk.start; + try node.data.cloneFrom( + &chunk.node.data, chunk.start, chunk.end, ); - page_list.append(page); + page_list.append(node); - total_rows += page.data.size.rows; + total_rows += node.data.size.rows; // Remap our tracked pins by changing the page and // offsetting the Y position based on the chunk start. @@ -436,12 +436,12 @@ pub fn clone( const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { // We're only interested in pins that were within the chunk. - if (p.page != chunk.page or + if (p.node != chunk.node or p.y < chunk.start or p.y >= chunk.end) continue; const new_p = try pool.pins.create(); new_p.* = p.*; - new_p.page = page; + new_p.node = node; new_p.y -= chunk.start; try remap.putNoClobber(p, new_p); try tracked_pins.putNoClobber(pool.alloc, new_p, {}); @@ -626,8 +626,8 @@ fn resizeCols( try dst_cursor.reflowRow(self, row); // Once we're done reflowing a page, destroy it. - if (row.y == row.page.data.size.rows - 1) { - self.destroyPage(row.page); + if (row.y == row.node.data.size.rows - 1) { + self.destroyNode(row.node); } } @@ -716,7 +716,7 @@ const ReflowCursor = struct { list: *PageList, row: Pin, ) !void { - const src_page = &row.page.data; + const src_page: *Page = &row.node.data; const src_row = row.rowAndCell().row; const src_y = row.y; @@ -744,7 +744,7 @@ const ReflowCursor = struct { { const pin_keys = list.tracked_pins.keys(); for (pin_keys) |p| { - if (&p.page.data != src_page or + if (&p.node.data != src_page or p.y != src_y) continue; // If this pin is in the blanks on the right and past the end @@ -794,11 +794,11 @@ const ReflowCursor = struct { { const pin_keys = list.tracked_pins.keys(); for (pin_keys) |p| { - if (&p.page.data != src_page or + if (&p.node.data != src_page or p.y != src_y or p.x != x) continue; - p.page = self.node; + p.node = self.node; p.x = self.x; p.y = self.y; } @@ -1036,7 +1036,7 @@ const ReflowCursor = struct { // then we should remove it from the list. if (old_page.size.rows == 0) { list.pages.remove(old_node); - list.destroyPage(old_node); + list.destroyNode(old_node); } } @@ -1187,7 +1187,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { .lt => { var it = self.pageIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |chunk| { - const page = &chunk.page.data; + const page = &chunk.node.data; defer page.assertIntegrity(); const rows = page.rows.ptr(page.memory); for (0..page.size.rows) |i| { @@ -1307,7 +1307,7 @@ fn resizeWithoutReflowGrowCols( chunk: PageIterator.Chunk, ) !void { assert(cols > self.cols); - const page = &chunk.page.data; + const page = &chunk.node.data; const cap = try page.capacity.adjust(.{ .cols = cols }); // Update our col count @@ -1326,14 +1326,14 @@ fn resizeWithoutReflowGrowCols( // to allocate a page, and copy the old data into it. // On error, we need to undo all the pages we've added. - const prev = chunk.page.prev; + const prev = chunk.node.prev; errdefer { - var current = chunk.page.prev; + var current = chunk.node.prev; while (current) |p| { if (current == prev) break; current = p.prev; self.pages.remove(p); - self.destroyPage(p); + self.destroyNode(p); } } @@ -1391,8 +1391,8 @@ fn resizeWithoutReflowGrowCols( // We need to loop because our col growth may force us // to split pages. while (copied < page.size.rows) { - const new_page = try self.createPage(cap); - defer new_page.data.assertIntegrity(); + const new_node = try self.createPage(cap); + defer new_node.data.assertIntegrity(); // The length we can copy into the new page is at most the number // of rows in our cap. But if we can finish our source page we use that. @@ -1402,11 +1402,11 @@ fn resizeWithoutReflowGrowCols( const y_start = copied; const y_end = copied + len; const src_rows = page.rows.ptr(page.memory)[y_start..y_end]; - const dst_rows = new_page.data.rows.ptr(new_page.data.memory)[0..len]; + const dst_rows = new_node.data.rows.ptr(new_node.data.memory)[0..len]; for (dst_rows, src_rows) |*dst_row, *src_row| { - new_page.data.size.rows += 1; - errdefer new_page.data.size.rows -= 1; - try new_page.data.cloneRowFrom( + new_node.data.size.rows += 1; + errdefer new_node.data.size.rows -= 1; + try new_node.data.cloneRowFrom( page, dst_row, src_row, @@ -1415,15 +1415,15 @@ fn resizeWithoutReflowGrowCols( copied = y_end; // Insert our new page - self.pages.insertBefore(chunk.page, new_page); + self.pages.insertBefore(chunk.node, new_node); // Update our tracked pins that pointed to this previous page. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != chunk.page or + if (p.node != chunk.node or p.y < y_start or p.y >= y_end) continue; - p.page = new_page; + p.node = new_node; p.y -= y_start; } } @@ -1431,8 +1431,8 @@ fn resizeWithoutReflowGrowCols( // Remove the old page. // Deallocate the old page. - self.pages.remove(chunk.page); - self.destroyPage(chunk.page); + self.pages.remove(chunk.node); + self.destroyNode(chunk.node); } /// Returns the number of trailing blank lines, not to exceed max. Max @@ -1485,7 +1485,7 @@ fn trimTrailingBlankRows( // we'd invalidate this pin, as well. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != row_pin.page or + if (p.node != row_pin.node or p.y != row_pin.y) continue; return trimmed; } @@ -1493,11 +1493,11 @@ fn trimTrailingBlankRows( // No text, we can trim this row. Because it has // no text we can also be sure it has no styling // so we don't need to worry about memory. - row_pin.page.data.size.rows -= 1; - if (row_pin.page.data.size.rows == 0) { - self.erasePage(row_pin.page); + row_pin.node.data.size.rows -= 1; + if (row_pin.node.data.size.rows == 0) { + self.erasePage(row_pin.node); } else { - row_pin.page.data.assertIntegrity(); + row_pin.node.data.assertIntegrity(); } trimmed += 1; @@ -1723,8 +1723,8 @@ pub fn grow(self: *PageList) !?*List.Node { // new first page to the top-left. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != first) continue; - p.page = self.pages.first.?; + if (p.node != first) continue; + p.node = self.pages.first.?; p.y = 0; p.x = 0; } @@ -1737,17 +1737,17 @@ pub fn grow(self: *PageList) !?*List.Node { } // We need to allocate a new memory buffer. - const next_page = try self.createPage(try std_capacity.adjust(.{ .cols = self.cols })); + const next_node = try self.createPage(try std_capacity.adjust(.{ .cols = self.cols })); // we don't errdefer this because we've added it to the linked // list and its fine to have dangling unused pages. - self.pages.append(next_page); - next_page.data.size.rows = 1; + self.pages.append(next_node); + next_node.data.size.rows = 1; // We should never be more than our max size here because we've // verified the case above. - next_page.data.assertIntegrity(); + next_node.data.assertIntegrity(); - return next_page; + return next_node; } /// Adjust the capacity of the given page in the list. @@ -1787,12 +1787,14 @@ pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError; /// any requests to decrease will be ignored. pub fn adjustCapacity( self: *PageList, - page: *List.Node, + node: *List.Node, adjustment: AdjustCapacity, ) AdjustCapacityError!*List.Node { + const page: *Page = &node.data; + // We always start with the base capacity of the existing page. This // ensures we never shrink from what we need. - var cap = page.data.capacity; + var cap = page.capacity; // All ceilPowerOfTwo is unreachable because we're always same or less // bit width so maxInt is always possible. @@ -1820,26 +1822,27 @@ pub fn adjustCapacity( log.info("adjusting page capacity={}", .{cap}); // Create our new page and clone the old page into it. - const new_page = try self.createPage(cap); - errdefer self.destroyPage(new_page); - assert(new_page.data.capacity.rows >= page.data.capacity.rows); - new_page.data.size.rows = page.data.size.rows; - try new_page.data.cloneFrom(&page.data, 0, page.data.size.rows); + const new_node = try self.createPage(cap); + errdefer self.destroyNode(new_node); + const new_page: *Page = &new_node.data; + assert(new_page.capacity.rows >= page.capacity.rows); + new_page.size.rows = page.size.rows; + try new_page.cloneFrom(page, 0, page.size.rows); // Fix up all our tracked pins to point to the new page. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != page) continue; - p.page = new_page; + if (p.node != node) continue; + p.node = new_node; } // Insert this page and destroy the old page - self.pages.insertBefore(page, new_page); - self.pages.remove(page); - self.destroyPage(page); + self.pages.insertBefore(node, new_node); + self.pages.remove(node); + self.destroyNode(node); - new_page.data.assertIntegrity(); - return new_page; + new_page.assertIntegrity(); + return new_node; } /// Create a new page node. This does not add it to the list and this @@ -1897,30 +1900,33 @@ fn createPageExt( return page; } -/// Destroy the memory of the given page and return it to the pool. The -/// page is assumed to already be removed from the linked list. -fn destroyPage(self: *PageList, page: *List.Node) void { - destroyPageExt(&self.pool, page, &self.page_size); +/// Destroy the memory of the given node in the PageList linked list +/// and return it to the pool. The node is assumed to already be removed +/// from the linked list. +fn destroyNode(self: *PageList, node: *List.Node) void { + destroyNodeExt(&self.pool, node, &self.page_size); } -fn destroyPageExt( +fn destroyNodeExt( pool: *MemoryPool, - page: *List.Node, + node: *List.Node, total_size: ?*usize, ) void { - // Update our accounting for page size - if (total_size) |v| v.* -= page.data.memory.len; + const page: *Page = &node.data; - if (page.data.memory.len <= std_size) { + // Update our accounting for page size + if (total_size) |v| v.* -= page.memory.len; + + if (page.memory.len <= std_size) { // Reset the memory to zero so it can be reused - @memset(page.data.memory, 0); - pool.pages.destroy(@ptrCast(page.data.memory.ptr)); + @memset(page.memory, 0); + pool.pages.destroy(@ptrCast(page.memory.ptr)); } else { const page_alloc = pool.pages.arena.child_allocator; - page_alloc.free(page.data.memory); + page_alloc.free(page.memory); } - pool.nodes.destroy(page); + pool.nodes.destroy(node); } /// Fast-path function to erase exactly 1 row. Erasing means that the row @@ -1936,32 +1942,32 @@ pub fn eraseRow( ) !void { const pn = self.pin(pt).?; - var page = pn.page; - var rows = page.data.rows.ptr(page.data.memory.ptr); + var node = pn.node; + var rows = node.data.rows.ptr(node.data.memory.ptr); // In order to move the following rows up we rotate the rows array by 1. // The rotate operation turns e.g. [ 0 1 2 3 ] in to [ 1 2 3 0 ], which // works perfectly to move all of our elements where they belong. - fastmem.rotateOnce(Row, rows[pn.y..page.data.size.rows]); + fastmem.rotateOnce(Row, rows[pn.y..node.data.size.rows]); // We adjust the tracked pins in this page, moving up any that were below // the removed row. { const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page == page and p.y > pn.y) p.y -= 1; + if (p.node == node and p.y > pn.y) p.y -= 1; } } { // Set all the rows as dirty in this page - var dirty = page.data.dirtyBitSet(); - dirty.setRangeValue(.{ .start = pn.y, .end = page.data.size.rows }, true); + var dirty = node.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = pn.y, .end = node.data.size.rows }, true); } // We iterate through all of the following pages in order to move their // rows up by 1 as well. - while (page.next) |next| { + while (node.next) |next| { const next_rows = next.data.rows.ptr(next.data.memory.ptr); // We take the top row of the page and clone it in to the bottom @@ -1979,30 +1985,30 @@ pub fn eraseRow( // 5 5 5 | 6 // 6 6 6 | 7 // 7 7 7 <' 4 - try page.data.cloneRowFrom( + try node.data.cloneRowFrom( &next.data, - &rows[page.data.size.rows - 1], + &rows[node.data.size.rows - 1], &next_rows[0], ); - page = next; + node = next; rows = next_rows; - fastmem.rotateOnce(Row, rows[0..page.data.size.rows]); + fastmem.rotateOnce(Row, rows[0..node.data.size.rows]); // Set all the rows as dirty - var dirty = page.data.dirtyBitSet(); - dirty.setRangeValue(.{ .start = 0, .end = page.data.size.rows }, true); + var dirty = node.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = 0, .end = node.data.size.rows }, true); // Our tracked pins for this page need to be updated. // If the pin is in row 0 that means the corresponding row has // been moved to the previous page. Otherwise, move it up by 1. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != page) continue; + if (p.node != node) continue; if (p.y == 0) { - p.page = page.prev.?; - p.y = p.page.data.size.rows - 1; + p.node = node.prev.?; + p.y = p.node.data.size.rows - 1; continue; } p.y -= 1; @@ -2010,7 +2016,7 @@ pub fn eraseRow( } // Clear the final row which was rotated from the top of the page. - page.data.clearCells(&rows[page.data.size.rows - 1], 0, page.data.size.cols); + node.data.clearCells(&rows[node.data.size.rows - 1], 0, node.data.size.cols); } /// A variant of eraseRow that shifts only a bounded number of following @@ -2031,24 +2037,24 @@ pub fn eraseRowBounded( const pn = self.pin(pt).?; - var page = pn.page; - var rows = page.data.rows.ptr(page.data.memory.ptr); + var node: *List.Node = pn.node; + var rows = node.data.rows.ptr(node.data.memory.ptr); // If the row limit is less than the remaining rows before the end of the // page, then we clear the row, rotate it to the end of the boundary limit // and update our pins. - if (page.data.size.rows - pn.y > limit) { - page.data.clearCells(&rows[pn.y], 0, page.data.size.cols); + if (node.data.size.rows - pn.y > limit) { + node.data.clearCells(&rows[pn.y], 0, node.data.size.cols); fastmem.rotateOnce(Row, rows[pn.y..][0 .. limit + 1]); // Set all the rows as dirty - var dirty = page.data.dirtyBitSet(); + var dirty = node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = pn.y, .end = pn.y + limit }, true); // Update pins in the shifted region. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page == page and + if (p.node == node and p.y >= pn.y and p.y <= pn.y + limit) { @@ -2063,24 +2069,24 @@ pub fn eraseRowBounded( return; } - fastmem.rotateOnce(Row, rows[pn.y..page.data.size.rows]); + fastmem.rotateOnce(Row, rows[pn.y..node.data.size.rows]); // All the rows in the page are dirty below the erased row. { - var dirty = page.data.dirtyBitSet(); - dirty.setRangeValue(.{ .start = pn.y, .end = page.data.size.rows }, true); + var dirty = node.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = pn.y, .end = node.data.size.rows }, true); } // We need to keep track of how many rows we've shifted so that we can // determine at what point we need to do a partial shift on subsequent // pages. - var shifted: usize = page.data.size.rows - pn.y; + var shifted: usize = node.data.size.rows - pn.y; // Update tracked pins. { const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page == page and p.y >= pn.y) { + if (p.node == node and p.y >= pn.y) { if (p.y == 0) { p.x = 0; } else { @@ -2090,16 +2096,16 @@ pub fn eraseRowBounded( } } - while (page.next) |next| { + while (node.next) |next| { const next_rows = next.data.rows.ptr(next.data.memory.ptr); - try page.data.cloneRowFrom( + try node.data.cloneRowFrom( &next.data, - &rows[page.data.size.rows - 1], + &rows[node.data.size.rows - 1], &next_rows[0], ); - page = next; + node = next; rows = next_rows; // We check to see if this page contains enough rows to satisfy the @@ -2108,21 +2114,21 @@ pub fn eraseRowBounded( // // The logic here is very similar to the one before the loop. const shifted_limit = limit - shifted; - if (page.data.size.rows > shifted_limit) { - page.data.clearCells(&rows[0], 0, page.data.size.cols); + if (node.data.size.rows > shifted_limit) { + node.data.clearCells(&rows[0], 0, node.data.size.cols); fastmem.rotateOnce(Row, rows[0 .. shifted_limit + 1]); // Set all the rows as dirty - var dirty = page.data.dirtyBitSet(); + var dirty = node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = 0, .end = shifted_limit }, true); // Update pins in the shifted region. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != page or p.y > shifted_limit) continue; + if (p.node != node or p.y > shifted_limit) continue; if (p.y == 0) { - p.page = page.prev.?; - p.y = p.page.data.size.rows - 1; + p.node = node.prev.?; + p.y = p.node.data.size.rows - 1; continue; } p.y -= 1; @@ -2131,22 +2137,22 @@ pub fn eraseRowBounded( return; } - fastmem.rotateOnce(Row, rows[0..page.data.size.rows]); + fastmem.rotateOnce(Row, rows[0..node.data.size.rows]); // Set all the rows as dirty - var dirty = page.data.dirtyBitSet(); - dirty.setRangeValue(.{ .start = 0, .end = page.data.size.rows }, true); + var dirty = node.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = 0, .end = node.data.size.rows }, true); - // Account for the rows shifted in this page. - shifted += page.data.size.rows; + // Account for the rows shifted in this node. + shifted += node.data.size.rows; // Update tracked pins. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != page) continue; + if (p.node != node) continue; if (p.y == 0) { - p.page = page.prev.?; - p.y = p.page.data.size.rows - 1; + p.node = node.prev.?; + p.y = p.node.data.size.rows - 1; continue; } p.y -= 1; @@ -2155,7 +2161,7 @@ pub fn eraseRowBounded( // We reached the end of the page list before the limit, so we clear // the final row since it was rotated down from the top of this page. - page.data.clearCells(&rows[page.data.size.rows - 1], 0, page.data.size.cols); + node.data.clearCells(&rows[node.data.size.rows - 1], 0, node.data.size.cols); } /// Erase the rows from the given top to bottom (inclusive). Erasing @@ -2183,28 +2189,28 @@ pub fn eraseRows( // in our linked list. erasePage requires at least one other // page so to handle this we reinit this page, set it to zero // size which will let us grow our active area back. - if (chunk.page.next == null and chunk.page.prev == null) { - const page = &chunk.page.data; + if (chunk.node.next == null and chunk.node.prev == null) { + const page = &chunk.node.data; erased += page.size.rows; page.reinit(); page.size.rows = 0; break; } - self.erasePage(chunk.page); - erased += chunk.page.data.size.rows; + self.erasePage(chunk.node); + erased += chunk.node.data.size.rows; continue; } // We are modifying our chunk so make sure it is in a good state. - defer chunk.page.data.assertIntegrity(); + defer chunk.node.data.assertIntegrity(); // The chunk is not a full page so we need to move the rows. // This is a cheap operation because we're just moving cell offsets, // not the actual cell contents. assert(chunk.start == 0); - const rows = chunk.page.data.rows.ptr(chunk.page.data.memory); - const scroll_amount = chunk.page.data.size.rows - chunk.end; + const rows = chunk.node.data.rows.ptr(chunk.node.data.memory); + const scroll_amount = chunk.node.data.size.rows - chunk.end; for (0..scroll_amount) |i| { const src: *Row = &rows[i + chunk.end]; const dst: *Row = &rows[i]; @@ -2215,12 +2221,12 @@ pub fn eraseRows( // Clear our remaining cells that we didn't shift or swapped // in case we grow back into them. - for (scroll_amount..chunk.page.data.size.rows) |i| { + for (scroll_amount..chunk.node.data.size.rows) |i| { const row: *Row = &rows[i]; - chunk.page.data.clearCells( + chunk.node.data.clearCells( row, 0, - chunk.page.data.size.cols, + chunk.node.data.size.cols, ); } @@ -2228,7 +2234,7 @@ pub fn eraseRows( // row then we move it to the top of this page. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != chunk.page) continue; + if (p.node != chunk.node) continue; if (p.y >= chunk.end) { p.y -= chunk.end; } else { @@ -2238,12 +2244,12 @@ pub fn eraseRows( } // Our new size is the amount we scrolled - chunk.page.data.size.rows = @intCast(scroll_amount); + chunk.node.data.size.rows = @intCast(scroll_amount); erased += chunk.end; // Set all the rows as dirty - var dirty = chunk.page.data.dirtyBitSet(); - dirty.setRangeValue(.{ .start = 0, .end = chunk.page.data.size.rows }, true); + var dirty = chunk.node.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = 0, .end = chunk.node.data.size.rows }, true); } // If we deleted active, we need to regrow because one of our invariants @@ -2271,7 +2277,7 @@ pub fn eraseRows( // For top, we move back to active if our erasing moved our // top page into the active area. - .top => if (self.pinIsActive(.{ .page = self.pages.first.? })) { + .top => if (self.pinIsActive(.{ .node = self.pages.first.? })) { self.viewport = .{ .active = {} }; }, } @@ -2280,21 +2286,21 @@ pub fn eraseRows( /// Erase a single page, freeing all its resources. The page can be /// anywhere in the linked list but must NOT be the final page in the /// entire list (i.e. must not make the list empty). -fn erasePage(self: *PageList, page: *List.Node) void { - assert(page.next != null or page.prev != null); +fn erasePage(self: *PageList, node: *List.Node) void { + assert(node.next != null or node.prev != null); // Update any tracked pins to move to the next page. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != page) continue; - p.page = page.next orelse page.prev orelse unreachable; + if (p.node != node) continue; + p.node = node.next orelse node.prev orelse unreachable; p.y = 0; p.x = 0; } // Remove the page from the linked list - self.pages.remove(page); - self.destroyPage(page); + self.pages.remove(node); + self.destroyNode(node); } /// Returns the pin for the given point. The pin is NOT tracked so it @@ -2341,10 +2347,10 @@ pub fn countTrackedPins(self: *const PageList) usize { /// worst case. Only for runtime safety/debug. fn pinIsValid(self: *const PageList, p: Pin) bool { var it = self.pages.first; - while (it) |page| : (it = page.next) { - if (page != p.page) continue; - return p.y < page.data.size.rows and - p.x < page.data.size.cols; + while (it) |node| : (it = node.next) { + if (node != p.node) continue; + return p.y < node.data.size.rows and + p.x < node.data.size.cols; } return false; @@ -2356,19 +2362,19 @@ fn pinIsActive(self: *const PageList, p: Pin) bool { // If the pin is in the active page, then we can quickly determine // if we're beyond the end. const active = self.getTopLeft(.active); - if (p.page == active.page) return p.y >= active.y; + if (p.node == active.node) return p.y >= active.y; - var page_ = active.page.next; - while (page_) |page| { + var node_ = active.node.next; + while (node_) |node| { // This loop is pretty fast because the active area is - // never that large so this is at most one, two pages for + // never that large so this is at most one, two nodes for // reasonable terminals (including very large real world // ones). - // A page forward in the active area is our page, so we're + // A node forward in the active area is our node, so we're // definitely in the active area. - if (page == p.page) return true; - page_ = page.next; + if (node == p.node) return true; + node_ = node.next; } return false; @@ -2388,22 +2394,22 @@ pub fn pointFromPin(self: *const PageList, tag: point.Tag, p: Pin) ?point.Point // Count our first page which is special because it may be partial. var coord: point.Coordinate = .{ .x = p.x }; - if (p.page == tl.page) { + if (p.node == tl.node) { // If our top-left is after our y then we're outside the range. if (tl.y > p.y) return null; coord.y = p.y - tl.y; } else { - coord.y += tl.page.data.size.rows - tl.y; - var page_ = tl.page.next; - while (page_) |page| : (page_ = page.next) { - if (page == p.page) { + coord.y += tl.node.data.size.rows - tl.y; + var node_ = tl.node.next; + while (node_) |node| : (node_ = node.next) { + if (node == p.node) { coord.y += p.y; break; } - coord.y += page.data.size.rows; + coord.y += node.data.size.rows; } else { - // We never saw our page, meaning we're outside the range. + // We never saw our node, meaning we're outside the range. return null; } } @@ -2423,9 +2429,9 @@ pub fn pointFromPin(self: *const PageList, tag: point.Tag, p: Pin) ?point.Point /// Warning: this is slow and should not be used in performance critical paths pub fn getCell(self: *const PageList, pt: point.Point) ?Cell { const pt_pin = self.pin(pt) orelse return null; - const rac = pt_pin.page.data.getRowAndCell(pt_pin.x, pt_pin.y); + const rac = pt_pin.node.data.getRowAndCell(pt_pin.x, pt_pin.y); return .{ - .page = pt_pin.page, + .node = pt_pin.node, .row = rac.row, .cell = rac.cell, .row_idx = pt_pin.y, @@ -2463,16 +2469,16 @@ pub fn diagram(self: *const PageList, writer: anytype) !void { var it = self.pageIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |chunk| : (page_index += 1) { - cols = chunk.page.data.size.cols; + cols = chunk.node.data.size.cols; // Whether we've just skipped some number of rows and drawn // an ellipsis row (this is reset when a row is not skipped). var skipped = false; - for (0..chunk.page.data.size.rows) |y| { + for (0..chunk.node.data.size.rows) |y| { // Active header if (!active and - chunk.page == active_pin.page and + chunk.node == active_pin.node and active_pin.y == y) { active = true; @@ -2494,8 +2500,8 @@ pub fn diagram(self: *const PageList, writer: anytype) !void { // Row contents { - const row = chunk.page.data.getRow(y); - const cells = chunk.page.data.getCells(row)[0..cols]; + const row = chunk.node.data.getRow(y); + const cells = chunk.node.data.getCells(row)[0..cols]; var row_has_content = false; @@ -2544,7 +2550,7 @@ pub fn diagram(self: *const PageList, writer: anytype) !void { } try writer.print("{u}", .{cell.codepoint()}); if (cell.hasGrapheme()) { - const grapheme = chunk.page.data.lookupGrapheme(cell).?; + const grapheme = chunk.node.data.lookupGrapheme(cell).?; for (grapheme) |cp| { try writer.print("{u}", .{cp}); } @@ -2572,7 +2578,7 @@ pub fn diagram(self: *const PageList, writer: anytype) !void { var pin_count: usize = 0; const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { - if (p.page != chunk.page) continue; + if (p.node != chunk.node) continue; if (p.y != y) continue; pin_buf[pin_count] = p; pin_count += 1; @@ -2651,7 +2657,7 @@ pub const CellIterator = struct { switch (self.row_it.page_it.direction) { .right_down => { - if (cell.x + 1 < cell.page.data.size.cols) { + if (cell.x + 1 < cell.node.data.size.cols) { // We still have cells in this row, increase x. var copy = cell; copy.x += 1; @@ -2672,7 +2678,7 @@ pub const CellIterator = struct { // We need to move to the previous row and last col if (self.row_it.next()) |next_cell| { var copy = next_cell; - copy.x = next_cell.page.data.size.cols - 1; + copy.x = next_cell.node.data.size.cols - 1; self.cell = copy; } else { self.cell = null; @@ -2711,7 +2717,7 @@ pub const RowIterator = struct { pub fn next(self: *RowIterator) ?Pin { const chunk = self.chunk orelse return null; - const row: Pin = .{ .page = chunk.page, .y = self.offset }; + const row: Pin = .{ .node = chunk.node, .y = self.offset }; switch (self.page_it.direction) { .right_down => { @@ -2798,20 +2804,20 @@ pub const PageIterator = struct { // If we have no limit, then we consume this entire page. Our // next row is the next page. self.row = next: { - const next_page = row.page.next orelse break :next null; - break :next .{ .page = next_page }; + const next_page = row.node.next orelse break :next null; + break :next .{ .node = next_page }; }; break :none .{ - .page = row.page, + .node = row.node, .start = row.y, - .end = row.page.data.size.rows, + .end = row.node.data.size.rows, }; }, .count => |*limit| count: { assert(limit.* > 0); // should be handled already - const len = @min(row.page.data.size.rows - row.y, limit.*); + const len = @min(row.node.data.size.rows - row.y, limit.*); if (len > limit.*) { self.row = row.down(len); limit.* -= len; @@ -2820,7 +2826,7 @@ pub const PageIterator = struct { } break :count .{ - .page = row.page, + .node = row.node, .start = row.y, .end = row.y + len, }; @@ -2829,16 +2835,16 @@ pub const PageIterator = struct { .row => |limit_row| row: { // If this is not the same page as our limit then we // can consume the entire page. - if (limit_row.page != row.page) { + if (limit_row.node != row.node) { self.row = next: { - const next_page = row.page.next orelse break :next null; - break :next .{ .page = next_page }; + const next_page = row.node.next orelse break :next null; + break :next .{ .node = next_page }; }; break :row .{ - .page = row.page, + .node = row.node, .start = row.y, - .end = row.page.data.size.rows, + .end = row.node.data.size.rows, }; } @@ -2847,7 +2853,7 @@ pub const PageIterator = struct { self.row = null; if (row.y > limit_row.y) return null; break :row .{ - .page = row.page, + .node = row.node, .start = row.y, .end = limit_row.y + 1, }; @@ -2864,15 +2870,15 @@ pub const PageIterator = struct { // If we have no limit, then we consume this entire page. Our // next row is the next page. self.row = next: { - const next_page = row.page.prev orelse break :next null; + const next_page = row.node.prev orelse break :next null; break :next .{ - .page = next_page, + .node = next_page, .y = next_page.data.size.rows - 1, }; }; break :none .{ - .page = row.page, + .node = row.node, .start = 0, .end = row.y + 1, }; @@ -2889,7 +2895,7 @@ pub const PageIterator = struct { } break :count .{ - .page = row.page, + .node = row.node, .start = row.y - len, .end = row.y - 1, }; @@ -2898,17 +2904,17 @@ pub const PageIterator = struct { .row => |limit_row| row: { // If this is not the same page as our limit then we // can consume the entire page. - if (limit_row.page != row.page) { + if (limit_row.node != row.node) { self.row = next: { - const next_page = row.page.prev orelse break :next null; + const next_page = row.node.prev orelse break :next null; break :next .{ - .page = next_page, + .node = next_page, .y = next_page.data.size.rows - 1, }; }; break :row .{ - .page = row.page, + .node = row.node, .start = 0, .end = row.y + 1, }; @@ -2919,7 +2925,7 @@ pub const PageIterator = struct { self.row = null; if (row.y < limit_row.y) return null; break :row .{ - .page = row.page, + .node = row.node, .start = limit_row.y, .end = row.y + 1, }; @@ -2928,18 +2934,18 @@ pub const PageIterator = struct { } pub const Chunk = struct { - page: *List.Node, + node: *List.Node, start: size.CellCountInt, end: size.CellCountInt, pub fn rows(self: Chunk) []Row { - const rows_ptr = self.page.data.rows.ptr(self.page.data.memory); + const rows_ptr = self.node.data.rows.ptr(self.node.data.memory); return rows_ptr[self.start..self.end]; } /// Returns true if this chunk represents every row in the page. pub fn fullPage(self: Chunk) bool { - return self.start == 0 and self.end == self.page.data.size.rows; + return self.start == 0 and self.end == self.node.data.size.rows; } }; }; @@ -2986,7 +2992,7 @@ pub fn pageIterator( pub fn getTopLeft(self: *const PageList, tag: point.Tag) Pin { return switch (tag) { // The full screen or history is always just the first page. - .screen, .history => .{ .page = self.pages.first.? }, + .screen, .history => .{ .node = self.pages.first.? }, .viewport => switch (self.viewport) { .active => self.getTopLeft(.active), @@ -3001,13 +3007,13 @@ pub fn getTopLeft(self: *const PageList, tag: point.Tag) Pin { .active => active: { var rem = self.rows; var it = self.pages.last; - while (it) |page| : (it = page.prev) { - if (rem <= page.data.size.rows) break :active .{ - .page = page, - .y = page.data.size.rows - rem, + while (it) |node| : (it = node.prev) { + if (rem <= node.data.size.rows) break :active .{ + .node = node, + .y = node.data.size.rows - rem, }; - rem -= page.data.size.rows; + rem -= node.data.size.rows; } unreachable; // assertion: we always have enough rows for active @@ -3021,11 +3027,11 @@ pub fn getTopLeft(self: *const PageList, tag: point.Tag) Pin { pub fn getBottomRight(self: *const PageList, tag: point.Tag) ?Pin { return switch (tag) { .screen, .active => last: { - const page = self.pages.last.?; + const node = self.pages.last.?; break :last .{ - .page = page, - .y = page.data.size.rows - 1, - .x = page.data.size.cols - 1, + .node = node, + .y = node.data.size.rows - 1, + .x = node.data.size.cols - 1, }; }, @@ -3048,10 +3054,10 @@ pub fn getBottomRight(self: *const PageList, tag: point.Tag) ?Pin { /// rows, so it is not pub. This is only used for testing/debugging. fn totalRows(self: *const PageList) usize { var rows: usize = 0; - var page = self.pages.first; - while (page) |p| { - rows += p.data.size.rows; - page = p.next; + var node_ = self.pages.first; + while (node_) |node| { + rows += node.data.size.rows; + node_ = node.next; } return rows; @@ -3060,10 +3066,10 @@ fn totalRows(self: *const PageList) usize { /// The total number of pages in this list. fn totalPages(self: *const PageList) usize { var pages: usize = 0; - var page = self.pages.first; - while (page) |p| { + var node_ = self.pages.first; + while (node_) |node| { pages += 1; - page = p.next; + node_ = node.next; } return pages; @@ -3126,7 +3132,7 @@ fn markDirty(self: *PageList, pt: point.Point) void { /// all up to date as the pagelist is modified. This isn't cheap so callers /// should limit the number of active pins as much as possible. pub const Pin = struct { - page: *List.Node, + node: *List.Node, y: size.CellCountInt = 0, x: size.CellCountInt = 0, @@ -3134,7 +3140,7 @@ pub const Pin = struct { row: *pagepkg.Row, cell: *pagepkg.Cell, } { - const rac = self.page.data.getRowAndCell(self.x, self.y); + const rac = self.node.data.getRowAndCell(self.x, self.y); return .{ .row = rac.row, .cell = rac.cell }; } @@ -3145,7 +3151,7 @@ pub const Pin = struct { /// inclusive of the x coordinate of the pin. pub fn cells(self: Pin, subset: CellSubset) []pagepkg.Cell { const rac = self.rowAndCell(); - const all = self.page.data.getCells(rac.row); + const all = self.node.data.getCells(rac.row); return switch (subset) { .all => all, .left => all[0 .. self.x + 1], @@ -3156,26 +3162,26 @@ pub const Pin = struct { /// Returns the grapheme codepoints for the given cell. These are only /// the EXTRA codepoints and not the first codepoint. pub fn grapheme(self: Pin, cell: *const pagepkg.Cell) ?[]u21 { - return self.page.data.lookupGrapheme(cell); + return self.node.data.lookupGrapheme(cell); } /// Returns the style for the given cell in this pin. pub fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style { if (cell.style_id == stylepkg.default_id) return .{}; - return self.page.data.styles.get( - self.page.data.memory, + return self.node.data.styles.get( + self.node.data.memory, cell.style_id, ).*; } /// Check if this pin is dirty. pub fn isDirty(self: Pin) bool { - return self.page.data.isRowDirty(self.y); + return self.node.data.isRowDirty(self.y); } /// Mark this pin location as dirty. pub fn markDirty(self: Pin) void { - var set = self.page.data.dirtyBitSet(); + var set = self.node.data.dirtyBitSet(); set.set(self.y); } @@ -3289,7 +3295,7 @@ pub const Pin = struct { // graphics deletion code. pub fn isBetween(self: Pin, top: Pin, bottom: Pin) bool { if (build_config.slow_runtime_safety) { - if (top.page == bottom.page) { + if (top.node == bottom.node) { // If top is bottom, must be ordered. assert(top.y <= bottom.y); if (top.y == bottom.y) { @@ -3297,14 +3303,14 @@ pub const Pin = struct { } } else { // If top is not bottom, top must be before bottom. - var page = top.page.next; - while (page) |p| : (page = p.next) { - if (p == bottom.page) break; + var node_ = top.node.next; + while (node_) |node| : (node_ = node.next) { + if (node == bottom.node) break; } else assert(false); } } - if (self.page == top.page) { + if (self.node == top.node) { // If our pin is the top page and our y is less than the top y // then we can't possibly be between the top and bottom. if (self.y < top.y) return false; @@ -3316,7 +3322,7 @@ pub const Pin = struct { // at least the full top page and since we're the same page // we're in the range. if (self.y > top.y) { - return if (self.page == bottom.page) + return if (self.node == bottom.node) self.y <= bottom.y else true; @@ -3327,7 +3333,7 @@ pub const Pin = struct { assert(self.y == top.y); if (self.x < top.x) return false; } - if (self.page == bottom.page) { + if (self.node == bottom.node) { // Our page is the bottom page so we're between the top and // bottom if our y is less than the bottom y. if (self.y > bottom.y) return false; @@ -3345,11 +3351,11 @@ pub const Pin = struct { // Since our loop starts at top.page.next we need to check that // top != bottom because if they're the same then we can't possibly // be between them. - if (top.page == bottom.page) return false; - var page = top.page.next; - while (page) |p| : (page = p.next) { - if (p == bottom.page) break; - if (p == self.page) return true; + if (top.node == bottom.node) return false; + var node_ = top.node.next; + while (node_) |node| : (node_ = node.next) { + if (node == bottom.node) break; + if (node == self.node) return true; } return false; @@ -3359,22 +3365,22 @@ pub const Pin = struct { /// it requires traversing the linked list of pages. This should not /// be called in performance critical paths. pub fn before(self: Pin, other: Pin) bool { - if (self.page == other.page) { + if (self.node == other.node) { if (self.y < other.y) return true; if (self.y > other.y) return false; return self.x < other.x; } - var page = self.page.next; - while (page) |p| : (page = p.next) { - if (p == other.page) return true; + var node_ = self.node.next; + while (node_) |node| : (node_ = node.next) { + if (node == other.node) return true; } return false; } pub fn eql(self: Pin, other: Pin) bool { - return self.page == other.page and + return self.node == other.node and self.y == other.y and self.x == other.x; } @@ -3389,7 +3395,7 @@ pub const Pin = struct { /// Move the pin right n columns. n must fit within the size. pub fn right(self: Pin, n: usize) Pin { - assert(self.x + n < self.page.data.size.cols); + assert(self.x + n < self.node.data.size.cols); var result = self; result.x +|= std.math.cast(size.CellCountInt, n) orelse std.math.maxInt(size.CellCountInt); @@ -3424,33 +3430,33 @@ pub const Pin = struct { }, } { // Index fits within this page - const rows = self.page.data.size.rows - (self.y + 1); + const rows = self.node.data.size.rows - (self.y + 1); if (n <= rows) return .{ .offset = .{ - .page = self.page, + .node = self.node, .y = std.math.cast(size.CellCountInt, self.y + n) orelse std.math.maxInt(size.CellCountInt), .x = self.x, } }; // Need to traverse page links to find the page - var page: *List.Node = self.page; + var node: *List.Node = self.node; var n_left: usize = n - rows; while (true) { - page = page.next orelse return .{ .overflow = .{ + node = node.next orelse return .{ .overflow = .{ .end = .{ - .page = page, - .y = page.data.size.rows - 1, + .node = node, + .y = node.data.size.rows - 1, .x = self.x, }, .remaining = n_left, } }; - if (n_left <= page.data.size.rows) return .{ .offset = .{ - .page = page, + if (n_left <= node.data.size.rows) return .{ .offset = .{ + .node = node, .y = std.math.cast(size.CellCountInt, n_left - 1) orelse std.math.maxInt(size.CellCountInt), .x = self.x, } }; - n_left -= page.data.size.rows; + n_left -= node.data.size.rows; } } @@ -3465,33 +3471,33 @@ pub const Pin = struct { } { // Index fits within this page if (n <= self.y) return .{ .offset = .{ - .page = self.page, + .node = self.node, .y = std.math.cast(size.CellCountInt, self.y - n) orelse std.math.maxInt(size.CellCountInt), .x = self.x, } }; // Need to traverse page links to find the page - var page: *List.Node = self.page; + var node: *List.Node = self.node; var n_left: usize = n - self.y; while (true) { - page = page.prev orelse return .{ .overflow = .{ - .end = .{ .page = page, .y = 0, .x = self.x }, + node = node.prev orelse return .{ .overflow = .{ + .end = .{ .node = node, .y = 0, .x = self.x }, .remaining = n_left, } }; - if (n_left <= page.data.size.rows) return .{ .offset = .{ - .page = page, - .y = std.math.cast(size.CellCountInt, page.data.size.rows - n_left) orelse + if (n_left <= node.data.size.rows) return .{ .offset = .{ + .node = node, + .y = std.math.cast(size.CellCountInt, node.data.size.rows - n_left) orelse std.math.maxInt(size.CellCountInt), .x = self.x, } }; - n_left -= page.data.size.rows; + n_left -= node.data.size.rows; } } }; const Cell = struct { - page: *List.Node, + node: *List.Node, row: *pagepkg.Row, cell: *pagepkg.Cell, row_idx: size.CellCountInt, @@ -3502,7 +3508,7 @@ const Cell = struct { /// This is not very performant this is primarily used for assertions /// and testing. pub fn isDirty(self: Cell) bool { - return self.page.data.isRowDirty(self.row_idx); + return self.node.data.isRowDirty(self.row_idx); } /// Get the cell style. @@ -3510,8 +3516,8 @@ const Cell = struct { /// Not meant for non-test usage since this is inefficient. pub fn style(self: Cell) stylepkg.Style { if (self.cell.style_id == stylepkg.default_id) return .{}; - return self.page.data.styles.get( - self.page.data.memory, + return self.node.data.styles.get( + self.node.data.memory, self.cell.style_id, ).*; } @@ -3524,10 +3530,10 @@ const Cell = struct { /// carefully if you really need this. pub fn screenPoint(self: Cell) point.Point { var y: size.CellCountInt = self.row_idx; - var page = self.page; - while (page.prev) |prev| { - y += prev.data.size.rows; - page = prev; + var node_ = self.node; + while (node_.prev) |node| { + y += node.data.size.rows; + node_ = node; } return .{ .screen = .{ @@ -3549,7 +3555,7 @@ test "PageList" { // Active area should be the top try testing.expectEqual(Pin{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 0, .x = 0, }, s.getTopLeft(.active)); @@ -3592,7 +3598,7 @@ test "PageList pointFromPin active no history" { .x = 0, }, }, s.pointFromPin(.active, .{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 0, .x = 0, }).?); @@ -3604,7 +3610,7 @@ test "PageList pointFromPin active no history" { .x = 4, }, }, s.pointFromPin(.active, .{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 2, .x = 4, }).?); @@ -3626,7 +3632,7 @@ test "PageList pointFromPin active with history" { .x = 2, }, }, s.pointFromPin(.active, .{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 30, .x = 2, }).?); @@ -3635,7 +3641,7 @@ test "PageList pointFromPin active with history" { // In history, invalid { try testing.expect(s.pointFromPin(.active, .{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 21, .x = 2, }) == null); @@ -3660,7 +3666,7 @@ test "PageList pointFromPin active from prior page" { .x = 2, }, }, s.pointFromPin(.active, .{ - .page = s.pages.last.?, + .node = s.pages.last.?, .y = 0, .x = 2, }).?); @@ -3669,7 +3675,7 @@ test "PageList pointFromPin active from prior page" { // Prior page { try testing.expect(s.pointFromPin(.active, .{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 0, .x = 0, }) == null); @@ -3698,7 +3704,7 @@ test "PageList pointFromPin traverse pages" { .x = 2, }, }, s.pointFromPin(.screen, .{ - .page = s.pages.last.?.prev.?, + .node = s.pages.last.?.prev.?, .y = 5, .x = 2, }).?); @@ -3707,7 +3713,7 @@ test "PageList pointFromPin traverse pages" { // Prior page { try testing.expect(s.pointFromPin(.active, .{ - .page = s.pages.first.?, + .node = s.pages.first.?, .y = 0, .x = 0, }) == null); @@ -4159,7 +4165,7 @@ test "PageList grow allocate" { try testing.expect(last_node.next.? == new); { const cell = s.getCell(.{ .active = .{ .y = s.rows - 1 } }).?; - try testing.expect(cell.page == new); + try testing.expect(cell.node == new); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = last.capacity.rows, @@ -4195,7 +4201,7 @@ test "PageList grow prune scrollback" { // Create a tracked pin in the first page const p = try s.trackPin(s.pin(.{ .screen = .{} }).?); defer s.untrackPin(p); - try testing.expect(p.page == s.pages.first.?); + try testing.expect(p.node == s.pages.first.?); // Next should create a new page, but it should reuse our first // page since we're at max size. @@ -4208,7 +4214,7 @@ test "PageList grow prune scrollback" { try testing.expectEqual(page1_node, s.pages.last.?); // Our tracked pin should point to the top-left of the first page - try testing.expect(p.page == s.pages.first.?); + try testing.expect(p.node == s.pages.first.?); try testing.expect(p.x == 0); try testing.expect(p.y == 0); } @@ -4356,7 +4362,7 @@ test "PageList pageIterator single page" { var it = s.pageIterator(.right_down, .{ .active = .{} }, null); { const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.first.?); + try testing.expect(chunk.node == s.pages.first.?); try testing.expectEqual(@as(usize, 0), chunk.start); try testing.expectEqual(@as(usize, s.rows), chunk.end); } @@ -4384,14 +4390,14 @@ test "PageList pageIterator two pages" { var it = s.pageIterator(.right_down, .{ .active = .{} }, null); { const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.first.?); - const start = chunk.page.data.size.rows - s.rows + 1; + try testing.expect(chunk.node == s.pages.first.?); + const start = chunk.node.data.size.rows - s.rows + 1; try testing.expectEqual(start, chunk.start); - try testing.expectEqual(chunk.page.data.size.rows, chunk.end); + try testing.expectEqual(chunk.node.data.size.rows, chunk.end); } { const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.last.?); + try testing.expect(chunk.node == s.pages.last.?); const start: usize = 0; try testing.expectEqual(start, chunk.start); try testing.expectEqual(start + 1, chunk.end); @@ -4419,7 +4425,7 @@ test "PageList pageIterator history two pages" { { const active_tl = s.getTopLeft(.active); const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.first.?); + try testing.expect(chunk.node == s.pages.first.?); const start: usize = 0; try testing.expectEqual(start, chunk.start); try testing.expectEqual(active_tl.y, chunk.end); @@ -4441,7 +4447,7 @@ test "PageList pageIterator reverse single page" { var it = s.pageIterator(.left_up, .{ .active = .{} }, null); { const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.first.?); + try testing.expect(chunk.node == s.pages.first.?); try testing.expectEqual(@as(usize, 0), chunk.start); try testing.expectEqual(@as(usize, s.rows), chunk.end); } @@ -4470,7 +4476,7 @@ test "PageList pageIterator reverse two pages" { var count: usize = 0; { const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.last.?); + try testing.expect(chunk.node == s.pages.last.?); const start: usize = 0; try testing.expectEqual(start, chunk.start); try testing.expectEqual(start + 1, chunk.end); @@ -4478,10 +4484,10 @@ test "PageList pageIterator reverse two pages" { } { const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.first.?); - const start = chunk.page.data.size.rows - s.rows + 1; + try testing.expect(chunk.node == s.pages.first.?); + const start = chunk.node.data.size.rows - s.rows + 1; try testing.expectEqual(start, chunk.start); - try testing.expectEqual(chunk.page.data.size.rows, chunk.end); + try testing.expectEqual(chunk.node.data.size.rows, chunk.end); count += chunk.end - chunk.start; } try testing.expect(it.next() == null); @@ -4508,7 +4514,7 @@ test "PageList pageIterator reverse history two pages" { { const active_tl = s.getTopLeft(.active); const chunk = it.next().?; - try testing.expect(chunk.page == s.pages.first.?); + try testing.expect(chunk.node == s.pages.first.?); const start: usize = 0; try testing.expectEqual(start, chunk.start); try testing.expectEqual(active_tl.y, chunk.end); @@ -4688,7 +4694,7 @@ test "PageList erase row with tracked pin resets to top-left" { try testing.expectEqual(s.rows, s.totalRows()); // Our pin should move to the first page - try testing.expectEqual(s.pages.first.?, p.page); + try testing.expectEqual(s.pages.first.?, p.node); try testing.expectEqual(@as(usize, 0), p.y); try testing.expectEqual(@as(usize, 0), p.x); } @@ -4709,7 +4715,7 @@ test "PageList erase row with tracked pin shifts" { try testing.expectEqual(s.rows, s.totalRows()); // Our pin should move to the first page - try testing.expectEqual(s.pages.first.?, p.page); + try testing.expectEqual(s.pages.first.?, p.node); try testing.expectEqual(@as(usize, 0), p.y); try testing.expectEqual(@as(usize, 2), p.x); } @@ -4730,7 +4736,7 @@ test "PageList erase row with tracked pin is erased" { try testing.expectEqual(s.rows, s.totalRows()); // Our pin should move to the first page - try testing.expectEqual(s.pages.first.?, p.page); + try testing.expectEqual(s.pages.first.?, p.node); try testing.expectEqual(@as(usize, 0), p.y); try testing.expectEqual(@as(usize, 0), p.x); } @@ -4751,7 +4757,7 @@ test "PageList erase resets viewport to active if moves within active" { // Move our viewport to the top s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); try testing.expect(s.viewport == .pin); - try testing.expect(s.viewport_pin.page == s.pages.first.?); + try testing.expect(s.viewport_pin.node == s.pages.first.?); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); @@ -4774,12 +4780,12 @@ test "PageList erase resets viewport if inside erased page but not active" { // Move our viewport to the top s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); try testing.expect(s.viewport == .pin); - try testing.expect(s.viewport_pin.page == s.pages.first.?); + try testing.expect(s.viewport_pin.node == s.pages.first.?); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, .{ .history = .{ .y = 2 } }); try testing.expect(s.viewport == .pin); - try testing.expect(s.viewport_pin.page == s.pages.first.?); + try testing.expect(s.viewport_pin.node == s.pages.first.?); } test "PageList erase resets viewport to active if top is inside active" { @@ -4868,15 +4874,15 @@ test "PageList eraseRowBounded less than full row" { try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 7 } })); try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 8 } })); - try testing.expectEqual(s.pages.first.?, p_top.page); + try testing.expectEqual(s.pages.first.?, p_top.node); try testing.expectEqual(@as(usize, 4), p_top.y); try testing.expectEqual(@as(usize, 0), p_top.x); - try testing.expectEqual(s.pages.first.?, p_bot.page); + try testing.expectEqual(s.pages.first.?, p_bot.node); try testing.expectEqual(@as(usize, 7), p_bot.y); try testing.expectEqual(@as(usize, 0), p_bot.x); - try testing.expectEqual(s.pages.first.?, p_out.page); + try testing.expectEqual(s.pages.first.?, p_out.node); try testing.expectEqual(@as(usize, 9), p_out.y); try testing.expectEqual(@as(usize, 0), p_out.x); } @@ -4902,7 +4908,7 @@ test "PageList eraseRowBounded with pin at top" { try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); - try testing.expectEqual(s.pages.first.?, p_top.page); + try testing.expectEqual(s.pages.first.?, p_top.node); try testing.expectEqual(@as(usize, 0), p_top.y); try testing.expectEqual(@as(usize, 0), p_top.x); } @@ -4932,11 +4938,11 @@ test "PageList eraseRowBounded full rows single page" { } })); // Our pin should move to the first page - try testing.expectEqual(s.pages.first.?, p_in.page); + try testing.expectEqual(s.pages.first.?, p_in.node); try testing.expectEqual(@as(usize, 6), p_in.y); try testing.expectEqual(@as(usize, 0), p_in.x); - try testing.expectEqual(s.pages.first.?, p_out.page); + try testing.expectEqual(s.pages.first.?, p_out.node); try testing.expectEqual(@as(usize, 8), p_out.y); try testing.expectEqual(@as(usize, 0), p_out.x); } @@ -4968,19 +4974,19 @@ test "PageList eraseRowBounded full rows two pages" { defer s.untrackPin(p_out); { - try testing.expectEqual(s.pages.last.?.prev.?, p_first.page); - try testing.expectEqual(@as(usize, p_first.page.data.size.rows - 1), p_first.y); + try testing.expectEqual(s.pages.last.?.prev.?, p_first.node); + try testing.expectEqual(@as(usize, p_first.node.data.size.rows - 1), p_first.y); try testing.expectEqual(@as(usize, 0), p_first.x); - try testing.expectEqual(s.pages.last.?.prev.?, p_first_out.page); - try testing.expectEqual(@as(usize, p_first_out.page.data.size.rows - 2), p_first_out.y); + try testing.expectEqual(s.pages.last.?.prev.?, p_first_out.node); + try testing.expectEqual(@as(usize, p_first_out.node.data.size.rows - 2), p_first_out.y); try testing.expectEqual(@as(usize, 0), p_first_out.x); - try testing.expectEqual(s.pages.last.?, p_in.page); + try testing.expectEqual(s.pages.last.?, p_in.node); try testing.expectEqual(@as(usize, 3), p_in.y); try testing.expectEqual(@as(usize, 0), p_in.x); - try testing.expectEqual(s.pages.last.?, p_out.page); + try testing.expectEqual(s.pages.last.?, p_out.node); try testing.expectEqual(@as(usize, 4), p_out.y); try testing.expectEqual(@as(usize, 0), p_out.x); } @@ -4996,22 +5002,22 @@ test "PageList eraseRowBounded full rows two pages" { } })); // In page in first page is shifted - try testing.expectEqual(s.pages.last.?.prev.?, p_first.page); - try testing.expectEqual(@as(usize, p_first.page.data.size.rows - 2), p_first.y); + try testing.expectEqual(s.pages.last.?.prev.?, p_first.node); + try testing.expectEqual(@as(usize, p_first.node.data.size.rows - 2), p_first.y); try testing.expectEqual(@as(usize, 0), p_first.x); // Out page in first page should not be shifted - try testing.expectEqual(s.pages.last.?.prev.?, p_first_out.page); - try testing.expectEqual(@as(usize, p_first_out.page.data.size.rows - 2), p_first_out.y); + try testing.expectEqual(s.pages.last.?.prev.?, p_first_out.node); + try testing.expectEqual(@as(usize, p_first_out.node.data.size.rows - 2), p_first_out.y); try testing.expectEqual(@as(usize, 0), p_first_out.x); // In page is shifted - try testing.expectEqual(s.pages.last.?, p_in.page); + try testing.expectEqual(s.pages.last.?, p_in.node); try testing.expectEqual(@as(usize, 2), p_in.y); try testing.expectEqual(@as(usize, 0), p_in.x); // Out page is not shifted - try testing.expectEqual(s.pages.last.?, p_out.page); + try testing.expectEqual(s.pages.last.?, p_out.node); try testing.expectEqual(@as(usize, 4), p_out.y); try testing.expectEqual(@as(usize, 0), p_out.x); } @@ -5669,7 +5675,7 @@ test "PageList resize (no reflow) less cols" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } @@ -5693,7 +5699,7 @@ test "PageList resize (no reflow) less cols pin in trimmed cols" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } @@ -5729,7 +5735,7 @@ test "PageList resize (no reflow) less cols clears graphemes" { var it = s.pageIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |chunk| { - try testing.expectEqual(@as(usize, 0), chunk.page.data.graphemeCount()); + try testing.expectEqual(@as(usize, 0), chunk.node.data.graphemeCount()); } } @@ -5748,7 +5754,7 @@ test "PageList resize (no reflow) more cols" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 10), cells.len); } } @@ -5921,7 +5927,7 @@ test "PageList resize (no reflow) less cols then more cols" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } @@ -5941,7 +5947,7 @@ test "PageList resize (no reflow) less rows and cols" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } @@ -5962,7 +5968,7 @@ test "PageList resize (no reflow) more rows and less cols" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); } } @@ -6002,7 +6008,7 @@ test "PageList resize (no reflow) empty screen" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 10), cells.len); } } @@ -6041,7 +6047,7 @@ test "PageList resize (no reflow) more cols forces smaller cap" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, cap2.cols), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); } @@ -6146,7 +6152,7 @@ test "PageList resize reflow more cols no wrapped rows" { var it = s.rowIterator(.right_down, .{ .screen = .{} }, null); while (it.next()) |offset| { const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 10), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); } @@ -6197,7 +6203,7 @@ test "PageList resize reflow more cols wrapped rows" { // First row should be unwrapped const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 4), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); @@ -6453,7 +6459,7 @@ test "PageList resize reflow more cols wrap across page boundary cursor in secon // Put a tracked pin in wrapped row on the last page const p = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 9 } }).?); defer s.untrackPin(p); - try testing.expect(p.page == s.pages.last.?); + try testing.expect(p.node == s.pages.last.?); // We expect one fewer rows since we unwrapped a row. const end_rows = s.totalRows() - 1; @@ -6537,7 +6543,7 @@ test "PageList resize reflow less cols wrap across page boundary cursor in secon // Put a tracked pin in wrapped row on the last page const p = try s.trackPin(s.pin(.{ .active = .{ .x = 2, .y = 5 } }).?); defer s.untrackPin(p); - try testing.expect(p.page == s.pages.last.?); + try testing.expect(p.node == s.pages.last.?); try testing.expect(p.y == 0); // PageList.diagram -> @@ -7235,7 +7241,7 @@ test "PageList resize reflow less cols no wrapped rows" { var offset_copy = offset; offset_copy.x = @intCast(x); const rac = offset_copy.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); try testing.expectEqual(@as(u21, @intCast(x)), cells[x].content.codepoint); } @@ -7279,7 +7285,7 @@ test "PageList resize reflow less cols wrapped rows" { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); @@ -7287,7 +7293,7 @@ test "PageList resize reflow less cols wrapped rows" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); @@ -7296,7 +7302,7 @@ test "PageList resize reflow less cols wrapped rows" { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); @@ -7304,7 +7310,7 @@ test "PageList resize reflow less cols wrapped rows" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); @@ -7355,7 +7361,7 @@ test "PageList resize reflow less cols wrapped rows with graphemes" { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); @@ -7363,7 +7369,7 @@ test "PageList resize reflow less cols wrapped rows with graphemes" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expect(rac.row.grapheme); try testing.expectEqual(@as(usize, 2), cells.len); @@ -7377,7 +7383,7 @@ test "PageList resize reflow less cols wrapped rows with graphemes" { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); @@ -7385,7 +7391,7 @@ test "PageList resize reflow less cols wrapped rows with graphemes" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expect(rac.row.grapheme); try testing.expectEqual(@as(usize, 2), cells.len); @@ -7721,7 +7727,7 @@ test "PageList resize reflow less cols blank lines" { // First row should be wrapped const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); @@ -7729,7 +7735,7 @@ test "PageList resize reflow less cols blank lines" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); @@ -7777,7 +7783,7 @@ test "PageList resize reflow less cols blank lines between" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); @@ -7785,7 +7791,7 @@ test "PageList resize reflow less cols blank lines between" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 2), cells[0].content.codepoint); @@ -7824,7 +7830,7 @@ test "PageList resize reflow less cols blank lines between no scrollback" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 'A'), cells[0].content.codepoint); @@ -7832,13 +7838,13 @@ test "PageList resize reflow less cols blank lines between no scrollback" { { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expectEqual(@as(u21, 0), cells[0].content.codepoint); } { const offset = it.next().?; const rac = offset.rowAndCell(); - const cells = offset.page.data.getCells(rac.row); + const cells = offset.node.data.getCells(rac.row); try testing.expect(!rac.row.wrap); try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(u21, 'C'), cells[0].content.codepoint); @@ -7931,8 +7937,8 @@ test "PageList resize reflow less cols copy style" { const style_id = rac.cell.style_id; try testing.expect(style_id != 0); - const style = offset.page.data.styles.get( - offset.page.data.memory, + const style = offset.node.data.styles.get( + offset.node.data.memory, style_id, ); try testing.expect(style.flags.bold); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index e9d2fccd8..4f3fe270e 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -203,7 +203,7 @@ pub fn init( errdefer pages.deinit(); // Create our tracked pin for the cursor. - const page_pin = try pages.trackPin(.{ .page = pages.pages.first.? }); + const page_pin = try pages.trackPin(.{ .node = pages.pages.first.? }); errdefer pages.untrackPin(page_pin); const page_rac = page_pin.rowAndCell(); @@ -331,7 +331,7 @@ pub fn clonePool( }; } - const page_pin = try pages.trackPin(.{ .page = pages.pages.first.? }); + const page_pin = try pages.trackPin(.{ .node = pages.pages.first.? }); const page_rac = page_pin.rowAndCell(); break :cursor .{ .x = 0, @@ -376,7 +376,7 @@ pub fn clonePool( if (!sel.contains(self, clone_top)) break :sel null; } - break :start try pages.trackPin(.{ .page = pages.pages.first.? }); + break :start try pages.trackPin(.{ .node = pages.pages.first.? }); }; const end_pin = pin_remap.get(ordered.br) orelse end: { @@ -414,21 +414,21 @@ pub fn clonePool( /// cursor page. pub fn adjustCapacity( self: *Screen, - page: *PageList.List.Node, + node: *PageList.List.Node, adjustment: PageList.AdjustCapacity, ) PageList.AdjustCapacityError!*PageList.List.Node { // If the page being modified isn't our cursor page then // this is a quick operation because we have no additional // accounting. - if (page != self.cursor.page_pin.page) { - return try self.pages.adjustCapacity(page, adjustment); + if (node != self.cursor.page_pin.node) { + return try self.pages.adjustCapacity(node, adjustment); } // We're modifying the cursor page. When we adjust the // capacity below it will be short the ref count on our // current style and hyperlink, so we need to init those. - const node = try self.pages.adjustCapacity(page, adjustment); - const new_page = &node.data; + const new_node = try self.pages.adjustCapacity(node, adjustment); + const new_page: *Page = &new_node.data; // All additions below have unreachable catches because when // we adjust cap we should have enough memory to fit the @@ -460,7 +460,7 @@ pub fn adjustCapacity( // So our page row/cell and so on are all off. self.cursorReload(); - return node; + return new_node; } pub fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { @@ -636,13 +636,14 @@ pub fn cursorDownScroll(self: *Screen) !void { // so our cursor is in the correct place we just have to clear // the cells. if (self.pages.rows == 1) { + const page: *Page = &self.cursor.page_pin.node.data; self.clearCells( - &self.cursor.page_pin.page.data, + page, self.cursor.page_row, - self.cursor.page_pin.page.data.getCells(self.cursor.page_row), + page.getCells(self.cursor.page_row), ); - var dirty = self.cursor.page_pin.page.data.dirtyBitSet(); + var dirty = page.dirtyBitSet(); dirty.set(0); } else { // eraseRow will shift everything below it up. @@ -684,7 +685,7 @@ pub fn cursorDownScroll(self: *Screen) !void { // was on was pruned. In this case, grow() moves the pin to // the top-left of the new page. This effectively moves it by // one already, we just need to fix up the x value. - const page_pin = if (old_pin.page == self.cursor.page_pin.page) + const page_pin = if (old_pin.node == self.cursor.page_pin.node) self.cursor.page_pin.down(1).? else reuse: { var pin = self.cursor.page_pin.*; @@ -714,10 +715,11 @@ pub fn cursorDownScroll(self: *Screen) !void { // Clear the new row so it gets our bg color. We only do this // if we have a bg color at all. if (self.cursor.style.bg_color != .none) { + const page: *Page = &page_pin.node.data; self.clearCells( - &page_pin.page.data, + page, self.cursor.page_row, - page_pin.page.data.getCells(self.cursor.page_row), + page.getCells(self.cursor.page_row), ); } } @@ -753,15 +755,15 @@ pub fn cursorScrollAbove(self: *Screen) !void { } else { // In this case, it means grow() didn't allocate a new page. - if (self.cursor.page_pin.page == self.pages.pages.last) { + if (self.cursor.page_pin.node == self.pages.pages.last) { // If we're on the last page we can do a very fast path because // all the rows we need to move around are within a single page. - assert(old_pin.page == self.cursor.page_pin.page); + assert(old_pin.node == self.cursor.page_pin.node); self.cursor.page_pin.* = self.cursor.page_pin.down(1).?; const pin = self.cursor.page_pin; - const page = &self.cursor.page_pin.page.data; + const page: *Page = &self.cursor.page_pin.node.data; // Rotate the rows so that the newly created empty row is at the // beginning. e.g. [ 0 1 2 3 ] in to [ 3 0 1 2 ]. @@ -822,7 +824,7 @@ fn cursorScrollAboveRotate(self: *Screen) !void { // Go through each of the pages following our pin, shift all rows // down by one, and copy the last row of the previous page. var current = self.pages.pages.last.?; - while (current != self.cursor.page_pin.page) : (current = current.prev.?) { + while (current != self.cursor.page_pin.node) : (current = current.prev.?) { const prev = current.prev.?; const prev_page = &prev.data; const cur_page = ¤t.data; @@ -846,7 +848,7 @@ fn cursorScrollAboveRotate(self: *Screen) !void { // Our current is our cursor page, we need to rotate down from // our cursor and clear our row. - assert(current == self.cursor.page_pin.page); + assert(current == self.cursor.page_pin.node); const cur_page = ¤t.data; const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr); fastmem.rotateOnceR(Row, cur_rows[self.cursor.page_pin.y..cur_page.size.rows]); @@ -922,7 +924,7 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct { // If the other cursor had a hyperlink, add it to ours. if (opts.hyperlink and other.hyperlink_id != 0) { // Get the hyperlink from the other cursor's page. - const other_page = &other.page_pin.page.data; + const other_page = &other.page_pin.node.data; const other_link = other_page.hyperlink_set.get(other_page.memory, other.hyperlink_id); const uri = other_link.uri.offset.ptr(other_page.memory)[0..other_link.uri.len]; @@ -957,7 +959,7 @@ fn cursorChangePin(self: *Screen, new: Pin) void { // If our pin is on the same page, then we can just update the pin. // We don't need to migrate any state. - if (self.cursor.page_pin.page == new.page) { + if (self.cursor.page_pin.node == new.node) { self.cursor.page_pin.* = new; return; } @@ -974,7 +976,7 @@ fn cursorChangePin(self: *Screen, new: Pin) void { // If we have a hyperlink then we need to release it from the old page. if (self.cursor.hyperlink != null) { - const old_page = &self.cursor.page_pin.page.data; + const old_page: *Page = &self.cursor.page_pin.node.data; old_page.hyperlink_set.release(old_page.memory, self.cursor.hyperlink_id); } @@ -1049,12 +1051,12 @@ pub fn cursorResetWrap(self: *Screen) void { // If the last cell in the row is a spacer head we need to clear it. const cells = self.cursor.page_pin.cells(.all); - const cell = cells[self.cursor.page_pin.page.data.size.cols - 1]; + const cell = cells[self.cursor.page_pin.node.data.size.cols - 1]; if (cell.wide == .spacer_head) { self.clearCells( - &self.cursor.page_pin.page.data, + &self.cursor.page_pin.node.data, page_row, - cells[self.cursor.page_pin.page.data.size.cols - 1 ..][0..1], + cells[self.cursor.page_pin.node.data.size.cols - 1 ..][0..1], ); } } @@ -1144,22 +1146,22 @@ pub fn clearRows( var it = self.pages.pageIterator(.right_down, tl, bl); while (it.next()) |chunk| { // Mark everything in this chunk as dirty - var dirty = chunk.page.data.dirtyBitSet(); + var dirty = chunk.node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = chunk.start, .end = chunk.end }, true); for (chunk.rows()) |*row| { const cells_offset = row.cells; - const cells_multi: [*]Cell = row.cells.ptr(chunk.page.data.memory); + const cells_multi: [*]Cell = row.cells.ptr(chunk.node.data.memory); const cells = cells_multi[0..self.pages.cols]; // Clear all cells if (protected) { - self.clearUnprotectedCells(&chunk.page.data, row, cells); + self.clearUnprotectedCells(&chunk.node.data, row, cells); // We need to preserve other row attributes since we only // cleared unprotected cells. row.cells = cells_offset; } else { - self.clearCells(&chunk.page.data, row, cells); + self.clearCells(&chunk.node.data, row, cells); row.* = .{ .cells = cells_offset }; } } @@ -1294,8 +1296,8 @@ pub fn clearPrompt(self: *Screen) void { var clear_it = top.rowIterator(.right_down, null); while (clear_it.next()) |p| { const row = p.rowAndCell().row; - p.page.data.clearCells(row, 0, p.page.data.size.cols); - p.page.data.assertIntegrity(); + p.node.data.clearCells(row, 0, p.node.data.size.cols); + p.node.data.assertIntegrity(); } } } @@ -1336,12 +1338,12 @@ pub fn splitCellBoundary( self: *Screen, x: size.CellCountInt, ) void { - const page = &self.cursor.page_pin.page.data; + const page = &self.cursor.page_pin.node.data; page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false); - const cols = self.cursor.page_pin.page.data.size.cols; + const cols = self.cursor.page_pin.node.data.size.cols; // `x` may be up to an INCLUDING `cols`, since that signifies splitting // the boundary to the right of the final cell in the row. @@ -1385,12 +1387,12 @@ pub fn splitCellBoundary( if (self.cursor.page_pin.up(1)) |p_row| { const p_rac = p_row.rowAndCell(); const p_cells = p_row.cells(.all); - const p_cell = p_cells[p_row.page.data.size.cols - 1]; + const p_cell = p_cells[p_row.node.data.size.cols - 1]; if (p_cell.wide == .spacer_head) { self.clearCells( - &p_row.page.data, + &p_row.node.data, p_rac.row, - p_cells[p_row.page.data.size.cols - 1 ..][0..1], + p_cells[p_row.node.data.size.cols - 1 ..][0..1], ); } } @@ -1506,7 +1508,7 @@ fn resizeInternal( if (self.cursor.hyperlink_id != 0) { // Note we do NOT use endHyperlink because we want to keep // our allocated self.cursor.hyperlink valid. - var page = &self.cursor.page_pin.page.data; + var page = &self.cursor.page_pin.node.data; page.hyperlink_set.release(page.memory, self.cursor.hyperlink_id); self.cursor.hyperlink_id = 0; self.cursor.hyperlink = null; @@ -1701,7 +1703,7 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { /// Call this whenever you manually change the cursor style. pub fn manualStyleUpdate(self: *Screen) !void { - var page = &self.cursor.page_pin.page.data; + var page: *Page = &self.cursor.page_pin.node.data; // std.log.warn("active styles={}", .{page.styles.count()}); @@ -1730,7 +1732,7 @@ pub fn manualStyleUpdate(self: *Screen) !void { // and double the style capacity for it if it was // full. const node = try self.adjustCapacity( - self.cursor.page_pin.page, + self.cursor.page_pin.node, switch (err) { error.OutOfMemory => .{ .styles = page.capacity.styles * 2 }, error.NeedsRehash => .{}, @@ -1749,8 +1751,8 @@ pub fn manualStyleUpdate(self: *Screen) !void { /// Append a grapheme to the given cell within the current cursor row. pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { - defer self.cursor.page_pin.page.data.assertIntegrity(); - self.cursor.page_pin.page.data.appendGrapheme( + defer self.cursor.page_pin.node.data.assertIntegrity(); + self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, cell, cp, @@ -1768,7 +1770,7 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { // Adjust our capacity. This will update our cursor page pin and // force us to reload. - const original_node = self.cursor.page_pin.page; + const original_node = self.cursor.page_pin.node; const new_bytes = original_node.data.capacity.grapheme_bytes * 2; _ = try self.adjustCapacity( original_node, @@ -1783,7 +1785,7 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { .gt => self.cursorCellRight(@intCast(cell_idx - self.cursor.x)), }; - try self.cursor.page_pin.page.data.appendGrapheme( + try self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, reloaded_cell, cp, @@ -1827,19 +1829,19 @@ pub fn startHyperlink( // strings table is out of memory, adjust it up error.StringsOutOfMemory => _ = try self.adjustCapacity( - self.cursor.page_pin.page, - .{ .string_bytes = self.cursor.page_pin.page.data.capacity.string_bytes * 2 }, + self.cursor.page_pin.node, + .{ .string_bytes = self.cursor.page_pin.node.data.capacity.string_bytes * 2 }, ), // hyperlink set is out of memory, adjust it up error.SetOutOfMemory => _ = try self.adjustCapacity( - self.cursor.page_pin.page, - .{ .hyperlink_bytes = self.cursor.page_pin.page.data.capacity.hyperlink_bytes * 2 }, + self.cursor.page_pin.node, + .{ .hyperlink_bytes = self.cursor.page_pin.node.data.capacity.hyperlink_bytes * 2 }, ), // hyperlink set is too full, rehash it error.SetNeedsRehash => _ = try self.adjustCapacity( - self.cursor.page_pin.page, + self.cursor.page_pin.node, .{}, ), } @@ -1866,7 +1868,7 @@ fn startHyperlinkOnce( errdefer link.deinit(self.alloc); // Insert the hyperlink into page memory - var page = &self.cursor.page_pin.page.data; + var page = &self.cursor.page_pin.node.data; const id: hyperlink.Id = try page.insertHyperlink(link.*); // Save it all @@ -1893,7 +1895,7 @@ pub fn endHyperlink(self: *Screen) void { // how RefCountedSet works). This causes some memory fragmentation but // is fine because if it is ever pruned the context deleted callback // will be called. - var page = &self.cursor.page_pin.page.data; + var page: *Page = &self.cursor.page_pin.node.data; page.hyperlink_set.release(page.memory, self.cursor.hyperlink_id); self.cursor.hyperlink.?.deinit(self.alloc); self.alloc.destroy(self.cursor.hyperlink.?); @@ -1905,7 +1907,7 @@ pub fn endHyperlink(self: *Screen) void { pub fn cursorSetHyperlink(self: *Screen) !void { assert(self.cursor.hyperlink_id != 0); - var page = &self.cursor.page_pin.page.data; + var page = &self.cursor.page_pin.node.data; if (page.setHyperlink( self.cursor.page_row, self.cursor.page_cell, @@ -1918,7 +1920,7 @@ pub fn cursorSetHyperlink(self: *Screen) !void { // hyperlink_map is out of space, realloc the page to be larger error.HyperlinkMapOutOfMemory => { _ = try self.adjustCapacity( - self.cursor.page_pin.page, + self.cursor.page_pin.node, .{ .hyperlink_bytes = page.capacity.hyperlink_bytes * 2 }, ); @@ -2021,7 +2023,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! while (page_it.next()) |chunk| { const rows = chunk.rows(); for (rows, chunk.start..) |row, y| { - const cells_ptr = row.cells.ptr(chunk.page.data.memory); + const cells_ptr = row.cells.ptr(chunk.node.data.memory); const start_x = if (row_count == 0 or sel_ordered.rectangle) sel_start.x @@ -2048,20 +2050,20 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! try strbuilder.appendSlice(buf[0..encode_len]); if (mapbuilder) |*b| { for (0..encode_len) |_| try b.append(.{ - .page = chunk.page, + .node = chunk.node, .y = @intCast(y), .x = @intCast(x), }); } } if (cell.hasGrapheme()) { - const cps = chunk.page.data.lookupGrapheme(cell).?; + const cps = chunk.node.data.lookupGrapheme(cell).?; for (cps) |cp| { const encode_len = try std.unicode.utf8Encode(cp, &buf); try strbuilder.appendSlice(buf[0..encode_len]); if (mapbuilder) |*b| { for (0..encode_len) |_| try b.append(.{ - .page = chunk.page, + .node = chunk.node, .y = @intCast(y), .x = @intCast(x), }); @@ -2070,16 +2072,16 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! } } - const is_final_row = chunk.page == sel_end.page and y == sel_end.y; + const is_final_row = chunk.node == sel_end.node and y == sel_end.y; if (!is_final_row and (!row.wrap or sel_ordered.rectangle)) { try strbuilder.append('\n'); if (mapbuilder) |*b| try b.append(.{ - .page = chunk.page, + .node = chunk.node, .y = @intCast(y), - .x = chunk.page.data.size.cols - 1, + .x = chunk.node.data.size.cols - 1, }); } @@ -2209,14 +2211,14 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection { const current_prompt = row.semantic_prompt.promptOrInput(); if (current_prompt != v) { var prev = p.up(1).?; - prev.x = p.page.data.size.cols - 1; + prev.x = p.node.data.size.cols - 1; break :end_pin prev; } } if (!row.wrap) { var copy = p; - copy.x = p.page.data.size.cols - 1; + copy.x = p.node.data.size.cols - 1; break :end_pin copy; } } @@ -2417,7 +2419,7 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { // If we are going to the next row and it isn't wrapped, we // return the previous. - if (p.x == p.page.data.size.cols - 1 and !rac.row.wrap) { + if (p.x == p.node.data.size.cols - 1 and !rac.row.wrap) { break :end p; } @@ -2437,7 +2439,7 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { // If we are going to the next row and it isn't wrapped, we // return the previous. - if (p.x == p.page.data.size.cols - 1 and !rac.row.wrap) { + if (p.x == p.node.data.size.cols - 1 and !rac.row.wrap) { break :start prev; } @@ -2491,7 +2493,7 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection { switch (row.semantic_prompt) { .input, .prompt_continuation, .prompt => { var copy = it_prev; - copy.x = it_prev.page.data.size.cols - 1; + copy.x = it_prev.node.data.size.cols - 1; break :boundary copy; }, else => {}, @@ -2504,10 +2506,10 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection { it = it_prev.rowIterator(.left_up, null); while (it.next()) |p| { const row = p.rowAndCell().row; - const cells = p.page.data.getCells(row); + const cells = p.node.data.getCells(row); if (Cell.hasTextAny(cells)) { var copy = p; - copy.x = p.page.data.size.cols - 1; + copy.x = p.node.data.size.cols - 1; break :boundary copy; } } @@ -2598,7 +2600,7 @@ pub fn selectPrompt(self: *Screen, pin: Pin) ?Selection { const end: Pin = end: { var it = pin.rowIterator(.right_down, null); var it_prev = it.next().?; - it_prev.x = it_prev.page.data.size.cols - 1; + it_prev.x = it_prev.node.data.size.cols - 1; while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { @@ -2610,7 +2612,7 @@ pub fn selectPrompt(self: *Screen, pin: Pin) ?Selection { } it_prev = p; - it_prev.x = it_prev.page.data.size.cols - 1; + it_prev.x = it_prev.node.data.size.cols - 1; } break :end it_prev; @@ -2764,7 +2766,7 @@ pub fn dumpString( .codepoint_grapheme => { try writer.print("{u}", .{cell.content.codepoint}); - const cps = row_offset.page.data.lookupGrapheme(cell).?; + const cps = row_offset.node.data.lookupGrapheme(cell).?; for (cps) |cp| { try writer.print("{u}", .{cp}); } @@ -2843,7 +2845,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { break :cell cell; }; - try self.cursor.page_pin.page.data.appendGrapheme( + try self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, cell, c, @@ -2872,7 +2874,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { // If we have a ref-counted style, increase. if (self.cursor.style_id != style.default_id) { - const page = self.cursor.page_pin.page.data; + const page = self.cursor.page_pin.node.data; page.styles.use(page.memory, self.cursor.style_id); self.cursor.page_row.styled = true; } @@ -2914,7 +2916,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { // If we have a ref-counted style, increase twice. if (self.cursor.style_id != style.default_id) { - const page = self.cursor.page_pin.page.data; + const page = self.cursor.page_pin.node.data; page.styles.use(page.memory, self.cursor.style_id); page.styles.use(page.memory, self.cursor.style_id); self.cursor.page_row.styled = true; @@ -3054,7 +3056,7 @@ test "Screen cursorCopy style deref" { var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; // Bold should create our style try s2.setAttribute(.{ .bold = {} }); @@ -3110,12 +3112,12 @@ test "Screen cursorCopy style deref new page" { // +-------------+ // This should be PAGE 1 - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; // It should be the last page in the list. try testing.expectEqual(&s2.pages.pages.last.?.data, page); // It should have a previous page. - try testing.expect(s2.cursor.page_pin.page.prev != null); + try testing.expect(s2.cursor.page_pin.node.prev != null); // The cursor should be at 2, 9 try testing.expect(s2.cursor.x == 2); @@ -3132,7 +3134,7 @@ test "Screen cursorCopy style deref new page" { try testing.expect(!s2.cursor.style.flags.bold); try testing.expectEqual(@as(usize, 0), page.styles.count()); // The page after the page the cursor is now in should be page 1. - try testing.expectEqual(page, &s2.cursor.page_pin.page.next.?.data); + try testing.expectEqual(page, &s2.cursor.page_pin.node.next.?.data); // The cursor should be at 0, 0 try testing.expect(s2.cursor.x == 0); try testing.expect(s2.cursor.y == 0); @@ -3148,7 +3150,7 @@ test "Screen cursorCopy style copy" { var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; try s2.cursorCopy(s.cursor, .{}); try testing.expect(s2.cursor.style.flags.bold); try testing.expectEqual(@as(usize, 1), page.styles.count()); @@ -3163,7 +3165,7 @@ test "Screen cursorCopy hyperlink deref" { var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; // Create a hyperlink for the cursor. try s2.startHyperlink("https://example.com/", null); @@ -3219,12 +3221,12 @@ test "Screen cursorCopy hyperlink deref new page" { // +-------------+ // This should be PAGE 1 - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; // It should be the last page in the list. try testing.expectEqual(&s2.pages.pages.last.?.data, page); // It should have a previous page. - try testing.expect(s2.cursor.page_pin.page.prev != null); + try testing.expect(s2.cursor.page_pin.node.prev != null); // The cursor should be at 2, 9 try testing.expect(s2.cursor.x == 2); @@ -3241,7 +3243,7 @@ test "Screen cursorCopy hyperlink deref new page" { try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0); // The page after the page the cursor is now in should be page 1. - try testing.expectEqual(page, &s2.cursor.page_pin.page.next.?.data); + try testing.expectEqual(page, &s2.cursor.page_pin.node.next.?.data); // The cursor should be at 0, 0 try testing.expect(s2.cursor.x == 0); try testing.expect(s2.cursor.y == 0); @@ -3256,12 +3258,12 @@ test "Screen cursorCopy hyperlink copy" { // Create a hyperlink for the cursor. try s.startHyperlink("https://example.com/", null); - try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.page.data.hyperlink_set.count()); + try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.node.data.hyperlink_set.count()); try testing.expect(s.cursor.hyperlink_id != 0); var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0); @@ -3281,12 +3283,12 @@ test "Screen cursorCopy hyperlink copy disabled" { // Create a hyperlink for the cursor. try s.startHyperlink("https://example.com/", null); - try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.page.data.hyperlink_set.count()); + try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.node.data.hyperlink_set.count()); try testing.expect(s.cursor.hyperlink_id != 0); var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); - const page = &s2.cursor.page_pin.page.data; + const page = &s2.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0); @@ -3303,7 +3305,7 @@ test "Screen style basics" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); // Set a new style @@ -3325,7 +3327,7 @@ test "Screen style reset to default" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); // Set a new style @@ -3345,7 +3347,7 @@ test "Screen style reset with unset" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); // Set a new style @@ -3402,7 +3404,7 @@ test "Screen clearRows active styled line" { try s.setAttribute(.{ .unset = {} }); // We should have one style - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); s.clearRows(.{ .active = .{} }, null, false); @@ -3628,21 +3630,21 @@ test "Screen: cursorDown across pages preserves style" { // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expect(start_page != page); } // Scroll back to the previous page s.cursorUp(1); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expect(start_page == page); } // Go back up, set a style try s.setAttribute(.{ .bold = {} }); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, @@ -3653,7 +3655,7 @@ test "Screen: cursorDown across pages preserves style" { // Go back down into the next page and we should have that style s.cursorDown(1); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, @@ -3678,14 +3680,14 @@ test "Screen: cursorUp across pages preserves style" { // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expect(start_page != page); } // Go back up, set a style try s.setAttribute(.{ .bold = {} }); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, @@ -3696,7 +3698,7 @@ test "Screen: cursorUp across pages preserves style" { // Go back down into the prev page and we should have that style s.cursorUp(1); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expect(start_page == page); const styleval = page.styles.get( @@ -3723,14 +3725,14 @@ test "Screen: cursorAbsolute across pages preserves style" { // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expect(start_page != page); } // Go back up, set a style try s.setAttribute(.{ .bold = {} }); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, @@ -3741,7 +3743,7 @@ test "Screen: cursorAbsolute across pages preserves style" { // Go back down into the prev page and we should have that style s.cursorAbsolute(1, 1); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expect(start_page == page); const styleval = page.styles.get( @@ -4345,7 +4347,7 @@ test "Screen: scroll above same page but cursor on previous page" { s.pages.clearDirty(); // Ensure we're still on the first page and have a second - try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?); + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); try testing.expect(s.pages.pages.first.?.next != null); // At this point: @@ -4403,7 +4405,7 @@ test "Screen: scroll above same page but cursor on previous page last row" { s.pages.clearDirty(); // Ensure we're still on the first page and have a second - try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?); + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); try testing.expect(s.pages.pages.first.?.next != null); // At this point: @@ -4478,7 +4480,7 @@ test "Screen: scroll above creates new page" { s.pages.clearDirty(); // Ensure we're still on the first page - try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?); + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); try s.cursorScrollAbove(); { @@ -8150,7 +8152,7 @@ test "Screen: selectionString with zero width joiner" { const cell = pin.rowAndCell().cell; try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = pin.page.data.lookupGrapheme(cell).?; + const cps = pin.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } @@ -8381,21 +8383,21 @@ test "Screen: hyperlink start/end" { defer s.deinit(); try testing.expect(s.cursor.hyperlink_id == 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); } try s.startHyperlink("http://example.com", null); try testing.expect(s.cursor.hyperlink_id != 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } s.endHyperlink(); try testing.expect(s.cursor.hyperlink_id == 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); } } @@ -8409,7 +8411,7 @@ test "Screen: hyperlink reuse" { try testing.expect(s.cursor.hyperlink_id == 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); } @@ -8422,14 +8424,14 @@ test "Screen: hyperlink reuse" { try s.startHyperlink("http://example.com", null); try testing.expectEqual(id, s.cursor.hyperlink_id); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } s.endHyperlink(); try testing.expect(s.cursor.hyperlink_id == 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); } } @@ -8449,7 +8451,7 @@ test "Screen: hyperlink cursor state on resize" { try s.startHyperlink("http://example.com", null); try testing.expect(s.cursor.hyperlink_id != 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } @@ -8457,14 +8459,14 @@ test "Screen: hyperlink cursor state on resize" { try s.resize(10, 10); try testing.expect(s.cursor.hyperlink_id != 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } s.endHyperlink(); try testing.expect(s.cursor.hyperlink_id == 0); { - const page = &s.cursor.page_pin.page.data; + const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); } } @@ -8489,8 +8491,8 @@ test "Screen: adjustCapacity cursor style ref count" { // This forces the page to change. _ = try s.adjustCapacity( - s.cursor.page_pin.page, - .{ .grapheme_bytes = s.cursor.page_pin.page.data.capacity.grapheme_bytes * 2 }, + s.cursor.page_pin.node, + .{ .grapheme_bytes = s.cursor.page_pin.node.data.capacity.grapheme_bytes * 2 }, ); // Our ref counts should still be the same diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index 452643f92..6fdb921e7 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -372,7 +372,7 @@ pub fn adjust( var current = end_pin.*; while (current.down(1)) |next| : (current = next) { const rac = next.rowAndCell(); - const cells = next.page.data.getCells(rac.row); + const cells = next.node.data.getCells(rac.row); if (page.Cell.hasTextAny(cells)) { end_pin.* = next; break; @@ -434,7 +434,7 @@ pub fn adjust( ); while (it.next()) |next| { const rac = next.rowAndCell(); - const cells = next.page.data.getCells(rac.row); + const cells = next.node.data.getCells(rac.row); if (page.Cell.hasTextAny(cells)) { end_pin.* = next; end_pin.x = @intCast(cells.len - 1); @@ -445,7 +445,7 @@ pub fn adjust( .beginning_of_line => end_pin.x = 0, - .end_of_line => end_pin.x = end_pin.page.data.size.cols - 1, + .end_of_line => end_pin.x = end_pin.node.data.size.cols - 1, } } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index a2bf6d50e..f5784b6ab 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -313,7 +313,7 @@ pub fn print(self: *Terminal, c: u21) !void { var state: unicode.GraphemeBreakState = .{}; var cp1: u21 = prev.cell.content.codepoint; if (prev.cell.hasGrapheme()) { - const cps = self.screen.cursor.page_pin.page.data.lookupGrapheme(prev.cell).?; + const cps = self.screen.cursor.page_pin.node.data.lookupGrapheme(prev.cell).?; for (cps) |cp2| { // log.debug("cp1={x} cp2={x}", .{ cp1, cp2 }); assert(!unicode.graphemeBreak(cp1, cp2, &state)); @@ -567,7 +567,7 @@ fn printCell( const spacer_cell = self.screen.cursorCellRight(1); self.screen.clearCells( - &self.screen.cursor.page_pin.page.data, + &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, spacer_cell[0..1], ); @@ -588,7 +588,7 @@ fn printCell( const wide_cell = self.screen.cursorCellLeft(1); self.screen.clearCells( - &self.screen.cursor.page_pin.page.data, + &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, wide_cell[0..1], ); @@ -607,7 +607,7 @@ fn printCell( // If the prior value had graphemes, clear those if (cell.hasGrapheme()) { - self.screen.cursor.page_pin.page.data.clearGrapheme( + self.screen.cursor.page_pin.node.data.clearGrapheme( self.screen.cursor.page_row, cell, ); @@ -617,7 +617,7 @@ fn printCell( // cell's new style will be different after writing. const style_changed = cell.style_id != self.screen.cursor.style_id; if (style_changed) { - var page = &self.screen.cursor.page_pin.page.data; + var page = &self.screen.cursor.page_pin.node.data; // Release the old style. if (cell.style_id != style.default_id) { @@ -639,7 +639,7 @@ fn printCell( }; if (style_changed) { - var page = &self.screen.cursor.page_pin.page.data; + var page = &self.screen.cursor.page_pin.node.data; // Use the new style. if (cell.style_id != style.default_id) { @@ -664,7 +664,7 @@ fn printCell( }; } else if (had_hyperlink) { // If the previous cell had a hyperlink then we need to clear it. - var page = &self.screen.cursor.page_pin.page.data; + var page = &self.screen.cursor.page_pin.node.data; page.clearHyperlink(self.screen.cursor.page_row, cell); } } @@ -1500,8 +1500,8 @@ pub fn insertLines(self: *Terminal, count: usize) void { const off_rac = off_p.rowAndCell(); const off_row: *Row = off_rac.row; - self.rowWillBeShifted(&cur_p.page.data, cur_row); - self.rowWillBeShifted(&off_p.page.data, off_row); + self.rowWillBeShifted(&cur_p.node.data, cur_row); + self.rowWillBeShifted(&off_p.node.data, off_row); // If our scrolling region is full width, then we unset wrap. if (!left_right) { @@ -1518,19 +1518,19 @@ pub fn insertLines(self: *Terminal, count: usize) void { // If our page doesn't match, then we need to do a copy from // one page to another. This is the slow path. - if (src_p.page != dst_p.page) { - dst_p.page.data.clonePartialRowFrom( - &src_p.page.data, + if (src_p.node != dst_p.node) { + dst_p.node.data.clonePartialRowFrom( + &src_p.node.data, dst_row, src_row, self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { - const cap = dst_p.page.data.capacity; + const cap = dst_p.node.data.capacity; // Adjust our page capacity to make // room for we didn't have space for _ = self.screen.adjustCapacity( - dst_p.page, + dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, @@ -1589,11 +1589,11 @@ pub fn insertLines(self: *Terminal, count: usize) void { src_row.* = dst; // Ensure what we did didn't corrupt the page - cur_p.page.data.assertIntegrity(); + cur_p.node.data.assertIntegrity(); } else { // Left/right scroll margins we have to // copy cells, which is much slower... - const page = &cur_p.page.data; + const page = &cur_p.node.data; page.moveCells( src_row, self.scrolling_region.left, @@ -1605,7 +1605,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { } } else { // Clear the cells for this row, it has been shifted. - const page = &cur_p.page.data; + const page = &cur_p.node.data; const cells = page.getCells(cur_row); self.screen.clearCells( page, @@ -1698,8 +1698,8 @@ pub fn deleteLines(self: *Terminal, count: usize) void { const off_rac = off_p.rowAndCell(); const off_row: *Row = off_rac.row; - self.rowWillBeShifted(&cur_p.page.data, cur_row); - self.rowWillBeShifted(&off_p.page.data, off_row); + self.rowWillBeShifted(&cur_p.node.data, cur_row); + self.rowWillBeShifted(&off_p.node.data, off_row); // If our scrolling region is full width, then we unset wrap. if (!left_right) { @@ -1716,19 +1716,19 @@ pub fn deleteLines(self: *Terminal, count: usize) void { // If our page doesn't match, then we need to do a copy from // one page to another. This is the slow path. - if (src_p.page != dst_p.page) { - dst_p.page.data.clonePartialRowFrom( - &src_p.page.data, + if (src_p.node != dst_p.node) { + dst_p.node.data.clonePartialRowFrom( + &src_p.node.data, dst_row, src_row, self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { - const cap = dst_p.page.data.capacity; + const cap = dst_p.node.data.capacity; // Adjust our page capacity to make // room for we didn't have space for _ = self.screen.adjustCapacity( - dst_p.page, + dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, @@ -1782,11 +1782,11 @@ pub fn deleteLines(self: *Terminal, count: usize) void { src_row.* = dst; // Ensure what we did didn't corrupt the page - cur_p.page.data.assertIntegrity(); + cur_p.node.data.assertIntegrity(); } else { // Left/right scroll margins we have to // copy cells, which is much slower... - const page = &cur_p.page.data; + const page = &cur_p.node.data; page.moveCells( src_row, self.scrolling_region.left, @@ -1798,7 +1798,7 @@ pub fn deleteLines(self: *Terminal, count: usize) void { } } else { // Clear the cells for this row, it's from out of bounds. - const page = &cur_p.page.data; + const page = &cur_p.node.data; const cells = page.getCells(cur_row); self.screen.clearCells( page, @@ -1843,7 +1843,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // left is just the cursor position but as a multi-pointer const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell); - var page = &self.screen.cursor.page_pin.page.data; + var page = &self.screen.cursor.page_pin.node.data; // If our X is a wide spacer tail then we need to erase the // previous cell too so we don't split a multi-cell character. @@ -1914,7 +1914,7 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void { // left is just the cursor position but as a multi-pointer const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell); - var page = &self.screen.cursor.page_pin.page.data; + var page = &self.screen.cursor.page_pin.node.data; // Remaining cols from our cursor to the right margin. const rem = self.scrolling_region.right - self.screen.cursor.x + 1; @@ -1995,7 +1995,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { // mode was not ISO we also always ignore protection attributes. if (self.screen.protected_mode != .iso) { self.screen.clearCells( - &self.screen.cursor.page_pin.page.data, + &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, cells[0..end], ); @@ -2003,7 +2003,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { } self.screen.clearUnprotectedCells( - &self.screen.cursor.page_pin.page.data, + &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, cells[0..end], ); @@ -2075,7 +2075,7 @@ pub fn eraseLine( // to fill the entire line. if (!protected) { self.screen.clearCells( - &self.screen.cursor.page_pin.page.data, + &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, cells[start..end], ); @@ -2083,7 +2083,7 @@ pub fn eraseLine( } self.screen.clearUnprotectedCells( - &self.screen.cursor.page_pin.page.data, + &self.screen.cursor.page_pin.node.data, self.screen.cursor.page_row, cells[start..end], ); @@ -2257,7 +2257,7 @@ pub fn decaln(self: *Terminal) !void { // Fill with Es by moving the cursor but reset it after. while (true) { - const page = &self.screen.cursor.page_pin.page.data; + const page = &self.screen.cursor.page_pin.node.data; const row = self.screen.cursor.page_row; const cells_multi: [*]Cell = row.cells.ptr(page.memory); const cells = cells_multi[0..page.size.cols]; @@ -2986,7 +2986,7 @@ test "Terminal: print over wide char with bold" { try t.print(0x1F600); // Smiley face // verify we have styles in our style map { - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); } @@ -2997,7 +2997,7 @@ test "Terminal: print over wide char with bold" { // verify our style is gone { - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); } @@ -3016,7 +3016,7 @@ test "Terminal: print over wide char with bg color" { try t.print(0x1F600); // Smiley face // verify we have styles in our style map { - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); } @@ -3027,7 +3027,7 @@ test "Terminal: print over wide char with bg color" { // verify our style is gone { - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); } @@ -3058,7 +3058,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } { @@ -3067,7 +3067,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); - try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } { const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; @@ -3075,7 +3075,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(@as(u21, 0x1F469), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } { @@ -3084,7 +3084,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); - try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } { const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; @@ -3092,7 +3092,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(@as(u21, 0x1F467), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } { const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?; @@ -3100,7 +3100,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); - try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); + try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); @@ -3128,7 +3128,7 @@ test "Terminal: VS16 doesn't make character with 2027 disabled" { try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } } @@ -3221,7 +3221,7 @@ test "Terminal: print multicodepoint grapheme, mode 2027" { try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 4), cps.len); } { @@ -3288,7 +3288,7 @@ test "Terminal: VS15 to make narrow character" { try testing.expectEqual(@as(u21, 0x26C8), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } } @@ -3319,7 +3319,7 @@ test "Terminal: VS16 to make wide character with mode 2027" { try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } } @@ -3350,7 +3350,7 @@ test "Terminal: VS16 repeated with mode 2027" { try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } { @@ -3359,7 +3359,7 @@ test "Terminal: VS16 repeated with mode 2027" { try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); - const cps = list_cell.page.data.lookupGrapheme(cell).?; + const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } } @@ -3482,7 +3482,7 @@ test "Terminal: overwrite multicodepoint grapheme clears grapheme data" { try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); // We should have one cell with graphemes - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount()); // Move back and overwrite wide @@ -3522,7 +3522,7 @@ test "Terminal: overwrite multicodepoint grapheme tail clears grapheme data" { try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); // We should have one cell with graphemes - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount()); // Move back and overwrite wide @@ -3971,7 +3971,7 @@ test "Terminal: print with hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } @@ -3998,7 +3998,7 @@ test "Terminal: print over cell with same hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } @@ -4025,7 +4025,7 @@ test "Terminal: print and end hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } for (3..6) |x| { @@ -4060,7 +4060,7 @@ test "Terminal: print and change hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } for (3..6) |x| { @@ -4070,7 +4070,7 @@ test "Terminal: print and change hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 2), id); } @@ -4094,7 +4094,7 @@ test "Terminal: overwrite hyperlink" { .x = @intCast(x), .y = 0, } }).?; - const page = &list_cell.page.data; + const page = &list_cell.node.data; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; @@ -4865,7 +4865,7 @@ test "Terminal: insertLines handles style refs" { try t.setAttribute(.{ .unset = {} }); // verify we have styles in our style map - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); t.setCursorPos(2, 2); @@ -5233,9 +5233,9 @@ test "Terminal: scrollUp moves hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (0..3) |x| { @@ -5247,7 +5247,7 @@ test "Terminal: scrollUp moves hyperlink" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -5284,7 +5284,7 @@ test "Terminal: scrollUp clears hyperlink" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -5386,7 +5386,7 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (1..4) |x| { @@ -5398,9 +5398,9 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (4..6) |x| { @@ -5410,7 +5410,7 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -5426,9 +5426,9 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (1..4) |x| { @@ -5438,7 +5438,7 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (4..6) |x| { @@ -5450,9 +5450,9 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } } @@ -5596,9 +5596,9 @@ test "Terminal: scrollDown hyperlink moves" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (0..3) |x| { @@ -5610,7 +5610,7 @@ test "Terminal: scrollDown hyperlink moves" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -5720,9 +5720,9 @@ test "Terminal: scrollDown left/right scroll region hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (1..4) |x| { @@ -5732,7 +5732,7 @@ test "Terminal: scrollDown left/right scroll region hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (4..6) |x| { @@ -5744,9 +5744,9 @@ test "Terminal: scrollDown left/right scroll region hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } } @@ -5760,7 +5760,7 @@ test "Terminal: scrollDown left/right scroll region hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (1..4) |x| { @@ -5772,9 +5772,9 @@ test "Terminal: scrollDown left/right scroll region hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (4..6) |x| { @@ -5784,7 +5784,7 @@ test "Terminal: scrollDown left/right scroll region hyperlink" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -6018,7 +6018,7 @@ test "Terminal: eraseChars handles refcounted styles" { try t.print('C'); // verify we have styles in our style map - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); t.setCursorPos(1, 1); @@ -6095,7 +6095,7 @@ test "Terminal: eraseChars wide char boundary conditions" { t.setCursorPos(1, 2); t.eraseChars(3); - t.screen.cursor.page_pin.page.data.assertIntegrity(); + t.screen.cursor.page_pin.node.data.assertIntegrity(); { const str = try t.plainString(alloc); @@ -6122,7 +6122,7 @@ test "Terminal: eraseChars wide char wrap boundary conditions" { t.setCursorPos(2, 2); t.eraseChars(3); - t.screen.cursor.page_pin.page.data.assertIntegrity(); + t.screen.cursor.page_pin.node.data.assertIntegrity(); { const str = try t.plainString(alloc); @@ -6425,7 +6425,7 @@ test "Terminal: index scrolling with hyperlink" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } { @@ -6437,7 +6437,7 @@ test "Terminal: index scrolling with hyperlink" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -6599,7 +6599,7 @@ test "Terminal: index bottom of scroll region with hyperlinks" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } { @@ -6611,7 +6611,7 @@ test "Terminal: index bottom of scroll region with hyperlinks" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -6648,9 +6648,9 @@ test "Terminal: index bottom of scroll region clear hyperlinks" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); - const page = &list_cell.page.data; + const page = &list_cell.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); } } @@ -7972,7 +7972,7 @@ test "Terminal: bold style" { const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expect(cell.style_id != 0); - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 1); } } @@ -7996,7 +7996,7 @@ test "Terminal: garbage collect overwritten" { } // verify we have no styles in our style map - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); } @@ -8018,7 +8018,7 @@ test "Terminal: do not garbage collect old styles in use" { } // verify we have no styles in our style map - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); } @@ -8390,7 +8390,7 @@ test "Terminal: insertBlanks deleting graphemes" { try t.print(0x1F467); // We should have one cell with graphemes - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount()); t.setCursorPos(1, 1); @@ -8426,7 +8426,7 @@ test "Terminal: insertBlanks shift graphemes" { try t.print(0x1F467); // We should have one cell with graphemes - const page = &t.screen.cursor.page_pin.page.data; + const page = &t.screen.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount()); t.setCursorPos(1, 1); @@ -8494,7 +8494,7 @@ test "Terminal: insertBlanks shifts hyperlinks" { try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell).?; + const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } for (0..2) |x| { @@ -8504,7 +8504,7 @@ test "Terminal: insertBlanks shifts hyperlinks" { } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -8534,7 +8534,7 @@ test "Terminal: insertBlanks pushes hyperlink off end completely" { try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); - const id = list_cell.page.data.lookupHyperlink(cell); + const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } } @@ -9036,7 +9036,7 @@ test "Terminal: deleteChars wide char boundary conditions" { t.setCursorPos(1, 2); t.deleteChars(3); - t.screen.cursor.page_pin.page.data.assertIntegrity(); + t.screen.cursor.page_pin.node.data.assertIntegrity(); { const str = try t.plainString(alloc); @@ -9088,7 +9088,7 @@ test "Terminal: deleteChars wide char wrap boundary conditions" { t.setCursorPos(2, 2); t.deleteChars(3); - t.screen.cursor.page_pin.page.data.assertIntegrity(); + t.screen.cursor.page_pin.node.data.assertIntegrity(); { const str = try t.plainString(alloc); @@ -9127,7 +9127,7 @@ test "Terminal: deleteChars wide char across right margin" { t.setCursorPos(1, 2); t.deleteChars(1); - t.screen.cursor.page_pin.page.data.assertIntegrity(); + t.screen.cursor.page_pin.node.data.assertIntegrity(); // NOTE: This behavior is slightly inconsistent with xterm. xterm // _visually_ splits the wide character (half the wide character shows From a436bd0af62a4bdc5af14774b955f7b46ccd9deb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 7 Nov 2024 14:38:54 -0800 Subject: [PATCH 23/46] move datastructures to dedicated "datastruct" package --- src/App.zig | 2 +- src/{ => datastruct}/blocking_queue.zig | 0 src/{ => datastruct}/cache_table.zig | 2 +- src/{ => datastruct}/circ_buf.zig | 2 +- src/{ => datastruct}/lru.zig | 0 src/datastruct/main.zig | 17 +++++++++++++++++ src/{ => datastruct}/segmented_pool.zig | 0 src/font/shaper/Cache.zig | 2 +- src/inspector/key.zig | 2 +- src/inspector/termio.zig | 2 +- src/main_ghostty.zig | 9 +-------- src/os/cf_release_thread.zig | 2 +- src/renderer/Thread.zig | 2 +- src/termio/Exec.zig | 2 +- src/termio/Termio.zig | 1 - src/termio/Thread.zig | 2 +- src/termio/backend.zig | 1 - src/termio/mailbox.zig | 2 +- 18 files changed, 29 insertions(+), 21 deletions(-) rename src/{ => datastruct}/blocking_queue.zig (100%) rename src/{ => datastruct}/cache_table.zig (99%) rename src/{ => datastruct}/circ_buf.zig (99%) rename src/{ => datastruct}/lru.zig (100%) create mode 100644 src/datastruct/main.zig rename src/{ => datastruct}/segmented_pool.zig (100%) diff --git a/src/App.zig b/src/App.zig index cc8277c52..271ba2043 100644 --- a/src/App.zig +++ b/src/App.zig @@ -13,7 +13,7 @@ const Surface = @import("Surface.zig"); const tracy = @import("tracy"); const input = @import("input.zig"); const Config = @import("config.zig").Config; -const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue; +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"); diff --git a/src/blocking_queue.zig b/src/datastruct/blocking_queue.zig similarity index 100% rename from src/blocking_queue.zig rename to src/datastruct/blocking_queue.zig diff --git a/src/cache_table.zig b/src/datastruct/cache_table.zig similarity index 99% rename from src/cache_table.zig rename to src/datastruct/cache_table.zig index 7a1a10b2b..40d36cc24 100644 --- a/src/cache_table.zig +++ b/src/datastruct/cache_table.zig @@ -1,4 +1,4 @@ -const fastmem = @import("./fastmem.zig"); +const fastmem = @import("../fastmem.zig"); const std = @import("std"); const assert = std.debug.assert; diff --git a/src/circ_buf.zig b/src/datastruct/circ_buf.zig similarity index 99% rename from src/circ_buf.zig rename to src/datastruct/circ_buf.zig index 4157fd0a4..ccee41801 100644 --- a/src/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -1,7 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const fastmem = @import("fastmem.zig"); +const fastmem = @import("../fastmem.zig"); /// Returns a circular buffer containing type T. pub fn CircBuf(comptime T: type, comptime default: T) type { diff --git a/src/lru.zig b/src/datastruct/lru.zig similarity index 100% rename from src/lru.zig rename to src/datastruct/lru.zig diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig new file mode 100644 index 000000000..70a7ce97f --- /dev/null +++ b/src/datastruct/main.zig @@ -0,0 +1,17 @@ +//! The datastruct package contains data structures or anything closely +//! related to data structures. + +const blocking_queue = @import("blocking_queue.zig"); +const cache_table = @import("cache_table.zig"); +const circ_buf = @import("circ_buf.zig"); +const segmented_pool = @import("segmented_pool.zig"); + +pub const lru = @import("lru.zig"); +pub const BlockingQueue = blocking_queue.BlockingQueue; +pub const CacheTable = cache_table.CacheTable; +pub const CircBuf = circ_buf.CircBuf; +pub const SegmentedPool = segmented_pool.SegmentedPool; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/segmented_pool.zig b/src/datastruct/segmented_pool.zig similarity index 100% rename from src/segmented_pool.zig rename to src/datastruct/segmented_pool.zig diff --git a/src/font/shaper/Cache.zig b/src/font/shaper/Cache.zig index 2a1424118..672845bfd 100644 --- a/src/font/shaper/Cache.zig +++ b/src/font/shaper/Cache.zig @@ -14,7 +14,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const font = @import("../main.zig"); -const CacheTable = @import("../../cache_table.zig").CacheTable; +const CacheTable = @import("../../datastruct/main.zig").CacheTable; const log = std.log.scoped(.font_shaper_cache); diff --git a/src/inspector/key.zig b/src/inspector/key.zig index 9e1d6eacb..e28bd5d4a 100644 --- a/src/inspector/key.zig +++ b/src/inspector/key.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const input = @import("../input.zig"); -const CircBuf = @import("../circ_buf.zig").CircBuf; +const CircBuf = @import("../datastruct/main.zig").CircBuf; const cimgui = @import("cimgui"); /// Circular buffer of key events. diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index bae07bcfe..78b35e19b 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const cimgui = @import("cimgui"); const terminal = @import("../terminal/main.zig"); -const CircBuf = @import("../circ_buf.zig").CircBuf; +const CircBuf = @import("../datastruct/main.zig").CircBuf; const Surface = @import("../Surface.zig"); /// The stream handler for our inspector. diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index a435d8772..071d4d530 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -168,7 +168,6 @@ pub const std_options: std.Options = .{ }; test { - _ = @import("circ_buf.zig"); _ = @import("pty.zig"); _ = @import("Command.zig"); _ = @import("font/main.zig"); @@ -180,17 +179,11 @@ test { _ = @import("surface_mouse.zig"); // Libraries - _ = @import("segmented_pool.zig"); _ = @import("crash/main.zig"); + _ = @import("datastruct/main.zig"); _ = @import("inspector/main.zig"); _ = @import("terminal/main.zig"); _ = @import("terminfo/main.zig"); _ = @import("simd/main.zig"); _ = @import("unicode/main.zig"); - - // TODO - _ = @import("blocking_queue.zig"); - _ = @import("cache_table.zig"); - _ = @import("config.zig"); - _ = @import("lru.zig"); } diff --git a/src/os/cf_release_thread.zig b/src/os/cf_release_thread.zig index 32069163b..5001441e0 100644 --- a/src/os/cf_release_thread.zig +++ b/src/os/cf_release_thread.zig @@ -9,7 +9,7 @@ const builtin = @import("builtin"); const xev = @import("xev"); const macos = @import("macos"); -const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; +const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; const Allocator = std.mem.Allocator; const log = std.log.scoped(.cf_release_thread); diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 35679e994..94a1280d9 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -9,7 +9,7 @@ const crash = @import("../crash/main.zig"); const renderer = @import("../renderer.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); -const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; +const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; const App = @import("../App.zig"); const Allocator = std.mem.Allocator; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 5018ced33..3bf288cb7 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -20,7 +20,7 @@ const shell_integration = @import("shell_integration.zig"); const terminal = @import("../terminal/main.zig"); const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); -const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; +const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool; const ptypkg = @import("../pty.zig"); const Pty = ptypkg.Pty; const EnvMap = std.process.EnvMap; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index f28eb118e..f2cdfc770 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -15,7 +15,6 @@ const posix = std.posix; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); const Pty = @import("../pty.zig").Pty; -const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const StreamHandler = @import("stream_handler.zig").StreamHandler; const terminal = @import("../terminal/main.zig"); const terminfo = @import("../terminfo/main.zig"); diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 0f9cd782e..3d316e399 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -17,7 +17,7 @@ const builtin = @import("builtin"); const xev = @import("xev"); const crash = @import("../crash/main.zig"); const termio = @import("../termio.zig"); -const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; +const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; const Allocator = std.mem.Allocator; const log = std.log.scoped(.io_thread); diff --git a/src/termio/backend.zig b/src/termio/backend.zig index 0080e7628..68b283a00 100644 --- a/src/termio/backend.zig +++ b/src/termio/backend.zig @@ -12,7 +12,6 @@ const shell_integration = @import("shell_integration.zig"); const terminal = @import("../terminal/main.zig"); const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); -const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const Pty = @import("../pty.zig").Pty; // The preallocation size for the write request pool. This should be big diff --git a/src/termio/mailbox.zig b/src/termio/mailbox.zig index 85471009d..cac453a1c 100644 --- a/src/termio/mailbox.zig +++ b/src/termio/mailbox.zig @@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator; const xev = @import("xev"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); -const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; +const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; const log = std.log.scoped(.io_writer); From 3aff43b2e88e0f6cab702de7873e787f06685933 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 7 Nov 2024 14:57:46 -0800 Subject: [PATCH 24/46] datastruct: add intrusive doubly linked list --- src/datastruct/intrusive_linked_list.zig | 177 +++++++++++++++++++++++ src/datastruct/main.zig | 2 + 2 files changed, 179 insertions(+) create mode 100644 src/datastruct/intrusive_linked_list.zig diff --git a/src/datastruct/intrusive_linked_list.zig b/src/datastruct/intrusive_linked_list.zig new file mode 100644 index 000000000..02279457f --- /dev/null +++ b/src/datastruct/intrusive_linked_list.zig @@ -0,0 +1,177 @@ +const std = @import("std"); +const testing = std.testing; + +/// An intrusive doubly-linked list. The type T must have a "next" and "prev" +/// field pointing to itself. +/// +/// This is an adaptation of the DoublyLinkedList from the Zig standard +/// library, which is MIT licensed. I've removed functionality that I don't +/// need. +pub fn DoublyLinkedList(comptime T: type) type { + return struct { + const Self = @This(); + + first: ?*T = null, + last: ?*T = null, + + /// Insert a new node after an existing one. + /// + /// Arguments: + /// node: Pointer to a node in the list. + /// new_node: Pointer to the new node to insert. + pub fn insertAfter(list: *Self, node: *T, new_node: *T) void { + new_node.prev = node; + if (node.next) |next_node| { + // Intermediate node. + new_node.next = next_node; + next_node.prev = new_node; + } else { + // Last element of the list. + new_node.next = null; + list.last = new_node; + } + node.next = new_node; + } + + /// Insert a new node before an existing one. + /// + /// Arguments: + /// node: Pointer to a node in the list. + /// new_node: Pointer to the new node to insert. + pub fn insertBefore(list: *Self, node: *T, new_node: *T) void { + new_node.next = node; + if (node.prev) |prev_node| { + // Intermediate node. + new_node.prev = prev_node; + prev_node.next = new_node; + } else { + // First element of the list. + new_node.prev = null; + list.first = new_node; + } + node.prev = new_node; + } + + /// Insert a new node at the end of the list. + /// + /// Arguments: + /// new_node: Pointer to the new node to insert. + pub fn append(list: *Self, new_node: *T) void { + if (list.last) |last| { + // Insert after last. + list.insertAfter(last, new_node); + } else { + // Empty list. + list.prepend(new_node); + } + } + + /// Insert a new node at the beginning of the list. + /// + /// Arguments: + /// new_node: Pointer to the new node to insert. + pub fn prepend(list: *Self, new_node: *T) void { + if (list.first) |first| { + // Insert before first. + list.insertBefore(first, new_node); + } else { + // Empty list. + list.first = new_node; + list.last = new_node; + new_node.prev = null; + new_node.next = null; + } + } + + /// Remove a node from the list. + /// + /// Arguments: + /// node: Pointer to the node to be removed. + pub fn remove(list: *Self, node: *T) void { + if (node.prev) |prev_node| { + // Intermediate node. + prev_node.next = node.next; + } else { + // First element of the list. + list.first = node.next; + } + + if (node.next) |next_node| { + // Intermediate node. + next_node.prev = node.prev; + } else { + // Last element of the list. + list.last = node.prev; + } + } + + /// Remove and return the last node in the list. + /// + /// Returns: + /// A pointer to the last node in the list. + pub fn pop(list: *Self) ?*T { + const last = list.last orelse return null; + list.remove(last); + return last; + } + + /// Remove and return the first node in the list. + /// + /// Returns: + /// A pointer to the first node in the list. + pub fn popFirst(list: *Self) ?*T { + const first = list.first orelse return null; + list.remove(first); + return first; + } + }; +} + +test "basic DoublyLinkedList test" { + const Node = struct { + data: u32, + prev: ?*@This() = null, + next: ?*@This() = null, + }; + const L = DoublyLinkedList(Node); + var list: L = .{}; + + var one: Node = .{ .data = 1 }; + var two: Node = .{ .data = 2 }; + var three: Node = .{ .data = 3 }; + var four: Node = .{ .data = 4 }; + var five: Node = .{ .data = 5 }; + + list.append(&two); // {2} + list.append(&five); // {2, 5} + list.prepend(&one); // {1, 2, 5} + list.insertBefore(&five, &four); // {1, 2, 4, 5} + list.insertAfter(&two, &three); // {1, 2, 3, 4, 5} + + // Traverse forwards. + { + var it = list.first; + var index: u32 = 1; + while (it) |node| : (it = node.next) { + try testing.expect(node.data == index); + index += 1; + } + } + + // Traverse backwards. + { + var it = list.last; + var index: u32 = 1; + while (it) |node| : (it = node.prev) { + try testing.expect(node.data == (6 - index)); + index += 1; + } + } + + _ = list.popFirst(); // {2, 3, 4, 5} + _ = list.pop(); // {2, 3, 4} + list.remove(&three); // {2, 4} + + try testing.expect(list.first.?.data == 2); + try testing.expect(list.last.?.data == 4); +} diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 70a7ce97f..4f45f9483 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -4,12 +4,14 @@ const blocking_queue = @import("blocking_queue.zig"); const cache_table = @import("cache_table.zig"); const circ_buf = @import("circ_buf.zig"); +const intrusive_linked_list = @import("intrusive_linked_list.zig"); const segmented_pool = @import("segmented_pool.zig"); pub const lru = @import("lru.zig"); 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 SegmentedPool = segmented_pool.SegmentedPool; test { From 1335af3e4adb76b841d4d592a79377e53bf2ffc2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 7 Nov 2024 15:05:13 -0800 Subject: [PATCH 25/46] terminal: change pagelist linked list to an intrusive linked list --- src/datastruct/intrusive_linked_list.zig | 22 +++++++++++++--------- src/terminal/PageList.zig | 17 ++++++++++++++--- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/datastruct/intrusive_linked_list.zig b/src/datastruct/intrusive_linked_list.zig index 02279457f..61bf8157c 100644 --- a/src/datastruct/intrusive_linked_list.zig +++ b/src/datastruct/intrusive_linked_list.zig @@ -11,15 +11,19 @@ pub fn DoublyLinkedList(comptime T: type) type { return struct { const Self = @This(); - first: ?*T = null, - last: ?*T = null, + /// The type of the node in the list. This makes it easy to get the + /// node type from the list type. + pub const Node = T; + + first: ?*Node = null, + last: ?*Node = null, /// Insert a new node after an existing one. /// /// Arguments: /// node: Pointer to a node in the list. /// new_node: Pointer to the new node to insert. - pub fn insertAfter(list: *Self, node: *T, new_node: *T) void { + pub fn insertAfter(list: *Self, node: *Node, new_node: *Node) void { new_node.prev = node; if (node.next) |next_node| { // Intermediate node. @@ -38,7 +42,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// Arguments: /// node: Pointer to a node in the list. /// new_node: Pointer to the new node to insert. - pub fn insertBefore(list: *Self, node: *T, new_node: *T) void { + pub fn insertBefore(list: *Self, node: *Node, new_node: *Node) void { new_node.next = node; if (node.prev) |prev_node| { // Intermediate node. @@ -56,7 +60,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Arguments: /// new_node: Pointer to the new node to insert. - pub fn append(list: *Self, new_node: *T) void { + pub fn append(list: *Self, new_node: *Node) void { if (list.last) |last| { // Insert after last. list.insertAfter(last, new_node); @@ -70,7 +74,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Arguments: /// new_node: Pointer to the new node to insert. - pub fn prepend(list: *Self, new_node: *T) void { + pub fn prepend(list: *Self, new_node: *Node) void { if (list.first) |first| { // Insert before first. list.insertBefore(first, new_node); @@ -87,7 +91,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Arguments: /// node: Pointer to the node to be removed. - pub fn remove(list: *Self, node: *T) void { + pub fn remove(list: *Self, node: *Node) void { if (node.prev) |prev_node| { // Intermediate node. prev_node.next = node.next; @@ -109,7 +113,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Returns: /// A pointer to the last node in the list. - pub fn pop(list: *Self) ?*T { + pub fn pop(list: *Self) ?*Node { const last = list.last orelse return null; list.remove(last); return last; @@ -119,7 +123,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Returns: /// A pointer to the first node in the list. - pub fn popFirst(list: *Self) ?*T { + pub fn popFirst(list: *Self) ?*Node { const first = list.first orelse return null; list.remove(first); return first; diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index f1dbc2561..70f972ebe 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -7,8 +7,9 @@ const std = @import("std"); const build_config = @import("../build_config.zig"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const color = @import("color.zig"); const fastmem = @import("../fastmem.zig"); +const DoublyLinkedList = @import("../datastruct/main.zig").IntrusiveDoublyLinkedList; +const color = @import("color.zig"); const kitty = @import("kitty.zig"); const point = @import("point.zig"); const pagepkg = @import("page.zig"); @@ -33,7 +34,16 @@ const page_preheat = 4; /// The list of pages in the screen. These are expected to be in order /// where the first page is the topmost page (scrollback) and the last is /// the bottommost page (the current active page). -pub const List = std.DoublyLinkedList(Page); +pub const List = DoublyLinkedList(Node); + +/// A single node within the PageList linked list. +/// +/// This isn't pub because you can access the type via List.Node. +const Node = struct { + prev: ?*Node = null, + next: ?*Node = null, + data: Page, +}; /// The memory pool we get page nodes from. const NodePool = std.heap.MemoryPool(List.Node); @@ -1699,7 +1709,8 @@ pub fn grow(self: *PageList) !?*List.Node { // reuses the popped page. It is possible to have a single page and // exceed the max size if that page was adjusted to be larger after // initial allocation. - if (self.pages.len > 1 and + if (self.pages.first != null and + self.pages.first != self.pages.last and self.page_size + PagePool.item_size > self.maxSize()) prune: { // If we need to add more memory to ensure our active area is From 84707932d27df65198ede1ba4b0b963f17c0f120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 7 Nov 2024 18:17:51 -0500 Subject: [PATCH 26/46] os/hostname: fix mac address handling when last section starts with '0' I hit an edge case when using Private Wi-Fi addressing on macOS where the last section of the randomized mac address starts with a '0'. The hostname parsing for the shell integration didn't handle this case, so issue #2512 reappeared. This fixes that by explicitly handling port numbers < 10. --- src/os/hostname.zig | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 6956ed71f..22f29ceff 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -36,15 +36,16 @@ pub fn bufPrintHostnameFromFileUri( // it's not a partial MAC-address. const port = uri.port orelse return host; - // If the port is not a 2-digit number we're not looking at a partial + // If the port is not a 1 or 2-digit number we're not looking at a partial // MAC-address, and instead just a regular port so we return the plain // URI hostname. - if (port < 10 or port > 99) return host; + if (port > 99) return host; var fbs = std.io.fixedBufferStream(buf); try std.fmt.format( fbs.writer(), - "{s}:{d}", + // Make sure "port" is always 2 digits, prefixed with a 0 when "port" is a 1-digit number. + "{s}:{d:0>2}", .{ host, port }, ); @@ -85,6 +86,14 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); } +test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" { + const uri = try std.Uri.parse("file://12:34:56:78:90:05"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + try std.testing.expectEqualStrings("12:34:56:78:90:05", actual); +} + test "bufPrintHostnameFromFileUri returns only hostname when there is a port component in the URI" { // First: try with a non-2-digit port, to test general port handling. const four_port_uri = try std.Uri.parse("file://has-a-port:1234"); From 74bda5a6ebd14d5a9d6891f3fc21953879a36d16 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Fri, 8 Nov 2024 20:26:21 +0100 Subject: [PATCH 27/46] feat: implement configurable freetype load flags --- src/build/fish_completions.zig | 2 +- src/build/mdgen/mdgen.zig | 2 +- src/config/Config.zig | 29 +++++++++++++++++++++++++++++ src/font/face.zig | 3 +++ src/font/face/freetype.zig | 8 +++++++- src/renderer/Metal.zig | 12 +++++++++++- src/renderer/OpenGL.zig | 13 ++++++++++++- 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index 0ff0a2163..64fbea44e 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions(); fn comptimeGenerateFishCompletions() []const u8 { comptime { - @setEvalBranchQuota(17000); + @setEvalBranchQuota(18000); var counter = std.io.countingWriter(std.io.null_writer); try writeFishCompletions(&counter.writer()); diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index 2e2884f1a..7e05596d7 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -26,7 +26,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void { \\ ); - @setEvalBranchQuota(2000); + @setEvalBranchQuota(3000); inline for (@typeInfo(Config).Struct.fields) |field| { if (field.name[0] == '_') continue; diff --git a/src/config/Config.zig b/src/config/Config.zig index e6b9d35ab..908935a5d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -287,6 +287,26 @@ const c = @cImport({ /// terminals. Only new terminals will use the new configuration. @"grapheme-width-method": GraphemeWidthMethod = .unicode, +/// Freetype load flags to enable. The format of this is a list of flags to +/// enable separated by commas. If you prefix a flag with `no-` then it is +/// disabled. If you omit a flag, it's default value is used, so you must +/// explicitely disable flags you don't want. You can also use `true` or `false` +/// to turn all flags on or off. +/// +/// Available flags: +/// +/// * `hinting` - Enable or disable hinting, enabled by default. +/// * `bitmap` - Enable or disable loading of any pre-rendered bitmap strikes, +/// enabled by default +/// * `force-autohint` - Use the freetype auto-hinter rather than the font's +/// native hinter. Enabled by default. +/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering. +/// This option doesn't impact the hinter. Enabled by default. +/// * `autohint` - Use the freetype auto-hinter. Enabled by default. +/// +/// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` +@"freetype-load-flag": FreetypeLoadFlags = .{}, + /// A theme to use. If the theme is an absolute pathname, Ghostty will attempt /// to load that file as a theme. If that file does not exist or is inaccessible, /// an error will be logged and no other directories will be searched. @@ -4565,6 +4585,15 @@ pub const GraphemeWidthMethod = enum { unicode, }; +/// See freetype-load-flag +pub const FreetypeLoadFlags = packed struct { + hinting: bool = true, + bitmap: bool = true, + @"force-autohint": bool = false, + monochrome: bool = false, + autohint: bool = true, +}; + /// See linux-cgroup pub const LinuxCgroup = enum { never, diff --git a/src/font/face.zig b/src/font/face.zig index 8bcfb8209..77fb9e45b 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const options = @import("main.zig").options; pub const Metrics = @import("face/Metrics.zig"); +const FreetypeLoadFlags = @import("../config/Config.zig").FreetypeLoadFlags; const freetype = @import("face/freetype.zig"); const coretext = @import("face/coretext.zig"); pub const web_canvas = @import("face/web_canvas.zig"); @@ -90,6 +91,8 @@ pub const RenderOptions = struct { /// /// This only works with CoreText currently. thicken: bool = false, + + load_flags: FreetypeLoadFlags, }; test { diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 7bb9ecbab..e3ad5322b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -318,7 +318,13 @@ pub const Face = struct { // // This must be enabled for color faces though because those are // often colored bitmaps, which we support. - .no_bitmap = !self.face.hasColor(), + .no_bitmap = !self.face.hasColor() or !opts.load_flags.bitmap, + + // use options from config + .no_hinting = !opts.load_flags.hinting, + .force_autohint = !opts.load_flags.@"force-autohint", + .monochrome = !opts.load_flags.monochrome, + .no_autohint = !opts.load_flags.autohint, }); const glyph = self.face.handle.*.glyph; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 742dfbcd4..561168b90 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -354,6 +354,7 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, + load_flags: configpkg.Config.FreetypeLoadFlags, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_opacity: f64, @@ -399,11 +400,14 @@ pub const DerivedConfig = struct { const cursor_invert = config.@"cursor-invert-fg-bg"; + const load_flags = config.@"freetype-load-flag"; + return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features, .font_styles = font_styles, + .load_flags = load_flags, .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -2719,6 +2723,7 @@ fn addUnderline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ); @@ -2751,6 +2756,7 @@ fn addOverline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ); @@ -2783,6 +2789,7 @@ fn addStrikethrough( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ); @@ -2822,6 +2829,7 @@ fn addGlyph( .{ .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, + .load_flags = self.config.load_flags, }, ); @@ -2901,6 +2909,7 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -2916,6 +2925,7 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -2956,7 +2966,7 @@ fn addPreeditCell( @intCast(cp.codepoint), .regular, .text, - .{ .grid_metrics = self.grid_metrics }, + .{ .grid_metrics = self.grid_metrics, .load_flags = self.config.load_flags }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 5313315b1..7cb391bcc 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -11,6 +11,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const link = @import("link.zig"); const isCovering = @import("cell.zig").isCovering; const fgMode = @import("cell.zig").fgMode; +const freetype = @import("freetype").Face; const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); @@ -288,6 +289,7 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, + load_flags: configpkg.Config.FreetypeLoadFlags, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_text: ?terminal.color.RGB, @@ -332,11 +334,14 @@ pub const DerivedConfig = struct { const cursor_invert = config.@"cursor-invert-fg-bg"; + const load_flags = config.@"freetype-load-flag"; + return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features, .font_styles = font_styles, + .load_flags = load_flags, .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -1765,7 +1770,7 @@ fn addPreeditCell( @intCast(cp.codepoint), .regular, .text, - .{ .grid_metrics = self.grid_metrics }, + .{ .grid_metrics = self.grid_metrics, .load_flags = self.config.load_flags }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; @@ -1866,6 +1871,7 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -1881,6 +1887,7 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -1943,6 +1950,7 @@ fn addUnderline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ); @@ -1984,6 +1992,7 @@ fn addOverline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ); @@ -2025,6 +2034,7 @@ fn addStrikethrough( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, + .load_flags = self.config.load_flags, }, ); @@ -2073,6 +2083,7 @@ fn addGlyph( .{ .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, + .load_flags = self.config.load_flags, }, ); From 945a715b08d9955b759e06f0c7ce6726d4e2604f Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 00:41:55 +0100 Subject: [PATCH 28/46] refactor: handle freetype load flags in face instead of renderer --- src/config/Config.zig | 4 ++-- src/font/Collection.zig | 3 +++ src/font/SharedGridSet.zig | 7 +++++++ src/font/face.zig | 3 +-- src/font/face/freetype.zig | 15 ++++++++++----- src/renderer/Metal.zig | 12 +----------- src/renderer/OpenGL.zig | 12 +----------- 7 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 908935a5d..aef589571 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -305,7 +305,7 @@ const c = @cImport({ /// * `autohint` - Use the freetype auto-hinter. Enabled by default. /// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` -@"freetype-load-flag": FreetypeLoadFlags = .{}, +@"freetype-load-flags": FreetypeLoadFlags = .{}, /// A theme to use. If the theme is an absolute pathname, Ghostty will attempt /// to load that file as a theme. If that file does not exist or is inaccessible, @@ -4586,7 +4586,7 @@ pub const GraphemeWidthMethod = enum { }; /// See freetype-load-flag -pub const FreetypeLoadFlags = packed struct { +pub const FreetypeLoadFlags = packed struct { hinting: bool = true, bitmap: bool = true, @"force-autohint": bool = false, diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 476787749..25615bde5 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -452,6 +452,8 @@ pub const LoadOptions = struct { /// for this is owned by the user and is not freed by the collection. metric_modifiers: Metrics.ModifierSet = .{}, + freetype_load_flags: config.Config.FreetypeLoadFlags = .{}, + pub fn deinit(self: *LoadOptions, alloc: Allocator) void { _ = self; _ = alloc; @@ -462,6 +464,7 @@ pub const LoadOptions = struct { return .{ .size = self.size, .metric_modifiers = &self.metric_modifiers, + .freetype_load_flags = self.freetype_load_flags, }; } }; diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index c3067fa6d..d4ad49a74 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -168,6 +168,7 @@ fn collection( .library = self.font_lib, .size = size, .metric_modifiers = key.metric_modifiers, + .freetype_load_flags = config.@"freetype-load-flags", }; var c = Collection.init(); @@ -427,6 +428,7 @@ pub const DerivedConfig = struct { @"adjust-strikethrough-position": ?Metrics.Modifier, @"adjust-strikethrough-thickness": ?Metrics.Modifier, @"adjust-cursor-thickness": ?Metrics.Modifier, + @"freetype-load-flags": configpkg.Config.FreetypeLoadFlags, /// Initialize a DerivedConfig. The config should be either a /// config.Config or another DerivedConfig to clone from. @@ -461,6 +463,7 @@ pub const DerivedConfig = struct { .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", + .@"freetype-load-flags" = config.@"freetype-load-flags", // This must be last so the arena contains all our allocations // from above since Zig does assignment in order. @@ -500,6 +503,8 @@ pub const Key = struct { /// font grid. font_size: DesiredSize = .{ .points = 12 }, + load_flags: configpkg.Config.FreetypeLoadFlags = .{}, + const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; @@ -618,6 +623,7 @@ pub const Key = struct { .codepoint_map = codepoint_map, .metric_modifiers = metric_modifiers, .font_size = font_size, + .load_flags = config.@"freetype-load-flags", }; } @@ -647,6 +653,7 @@ pub const Key = struct { for (self.descriptors) |d| d.hash(hasher); self.codepoint_map.hash(hasher); autoHash(hasher, self.metric_modifiers.count()); + autoHash(hasher, self.load_flags); if (self.metric_modifiers.count() > 0) { inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { const key = @field(Metrics.Key, field.name); diff --git a/src/font/face.zig b/src/font/face.zig index 77fb9e45b..d3fd89aa5 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -31,6 +31,7 @@ pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; pub const Options = struct { size: DesiredSize, metric_modifiers: ?*const Metrics.ModifierSet = null, + freetype_load_flags: FreetypeLoadFlags, }; /// The desired size for loading a font. @@ -91,8 +92,6 @@ pub const RenderOptions = struct { /// /// This only works with CoreText currently. thicken: bool = false, - - load_flags: FreetypeLoadFlags, }; test { diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index e3ad5322b..ac173d6dd 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -18,6 +18,7 @@ const Library = font.Library; const convert = @import("freetype_convert.zig"); const fastmem = @import("../../fastmem.zig"); const quirks = @import("../../quirks.zig"); +const FreetypeLoadFlags = @import("../../config/Config.zig").FreetypeLoadFlags; const log = std.log.scoped(.font_face); @@ -34,6 +35,9 @@ pub const Face = struct { /// Metrics for this font face. These are useful for renderers. metrics: font.face.Metrics, + /// Metrics for this font face. These are useful for renderers. + load_flags: FreetypeLoadFlags, + /// Set quirks.disableDefaultFontFeatures quirks_disable_default_font_features: bool = false, @@ -77,6 +81,7 @@ pub const Face = struct { .face = face, .hb_font = hb_font, .metrics = calcMetrics(face, opts.metric_modifiers), + .load_flags = opts.freetype_load_flags, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -318,13 +323,13 @@ pub const Face = struct { // // This must be enabled for color faces though because those are // often colored bitmaps, which we support. - .no_bitmap = !self.face.hasColor() or !opts.load_flags.bitmap, + .no_bitmap = !self.face.hasColor() or !self.load_flags.bitmap, // use options from config - .no_hinting = !opts.load_flags.hinting, - .force_autohint = !opts.load_flags.@"force-autohint", - .monochrome = !opts.load_flags.monochrome, - .no_autohint = !opts.load_flags.autohint, + .no_hinting = !self.load_flags.hinting, + .force_autohint = !self.load_flags.@"force-autohint", + .monochrome = !self.load_flags.monochrome, + .no_autohint = !self.load_flags.autohint, }); const glyph = self.face.handle.*.glyph; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 561168b90..742dfbcd4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -354,7 +354,6 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, - load_flags: configpkg.Config.FreetypeLoadFlags, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_opacity: f64, @@ -400,14 +399,11 @@ pub const DerivedConfig = struct { const cursor_invert = config.@"cursor-invert-fg-bg"; - const load_flags = config.@"freetype-load-flag"; - return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features, .font_styles = font_styles, - .load_flags = load_flags, .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -2723,7 +2719,6 @@ fn addUnderline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ); @@ -2756,7 +2751,6 @@ fn addOverline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ); @@ -2789,7 +2783,6 @@ fn addStrikethrough( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ); @@ -2829,7 +2822,6 @@ fn addGlyph( .{ .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, - .load_flags = self.config.load_flags, }, ); @@ -2909,7 +2901,6 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -2925,7 +2916,6 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -2966,7 +2956,7 @@ fn addPreeditCell( @intCast(cp.codepoint), .regular, .text, - .{ .grid_metrics = self.grid_metrics, .load_flags = self.config.load_flags }, + .{ .grid_metrics = self.grid_metrics }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 7cb391bcc..9d1c1a27d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -289,7 +289,6 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, - load_flags: configpkg.Config.FreetypeLoadFlags, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_text: ?terminal.color.RGB, @@ -334,14 +333,11 @@ pub const DerivedConfig = struct { const cursor_invert = config.@"cursor-invert-fg-bg"; - const load_flags = config.@"freetype-load-flag"; - return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", .font_features = font_features, .font_styles = font_styles, - .load_flags = load_flags, .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -1770,7 +1766,7 @@ fn addPreeditCell( @intCast(cp.codepoint), .regular, .text, - .{ .grid_metrics = self.grid_metrics, .load_flags = self.config.load_flags }, + .{ .grid_metrics = self.grid_metrics }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; @@ -1871,7 +1867,6 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -1887,7 +1882,6 @@ fn addCursor( .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); @@ -1950,7 +1944,6 @@ fn addUnderline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ); @@ -1992,7 +1985,6 @@ fn addOverline( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ); @@ -2034,7 +2026,6 @@ fn addStrikethrough( .{ .cell_width = 1, .grid_metrics = self.grid_metrics, - .load_flags = self.config.load_flags, }, ); @@ -2083,7 +2074,6 @@ fn addGlyph( .{ .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, - .load_flags = self.config.load_flags, }, ); From 290857a87171d82e5c1e206fce490a8069f5dacc Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 00:44:19 +0100 Subject: [PATCH 29/46] chore: remove unused import --- src/renderer/OpenGL.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 9d1c1a27d..5313315b1 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -11,7 +11,6 @@ const ArenaAllocator = std.heap.ArenaAllocator; const link = @import("link.zig"); const isCovering = @import("cell.zig").isCovering; const fgMode = @import("cell.zig").fgMode; -const freetype = @import("freetype").Face; const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); From c0b24ee60d65bee135dea7c99ad9446e9649a574 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 01:35:39 +0100 Subject: [PATCH 30/46] refactor: make freetype flags void for non-freetype backend This is an attempt to use `void` as type for Freetype Load Flags when backend does not use these flags. --- src/config.zig | 14 ++++++++++++++ src/font/Collection.zig | 13 ++++++++++++- src/font/SharedGridSet.zig | 28 +++++++++++++++++++++++++--- src/font/face.zig | 4 ++-- src/font/face/freetype.zig | 4 ++-- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/config.zig b/src/config.zig index b9f214fc9..f2d4876ae 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,6 +1,8 @@ const builtin = @import("builtin"); const formatter = @import("config/formatter.zig"); +const font = @import("font/main.zig"); +const options = font.options; pub const Config = @import("config/Config.zig"); pub const string = @import("config/string.zig"); pub const edit = @import("config/edit.zig"); @@ -9,6 +11,18 @@ pub const url = @import("config/url.zig"); pub const FileFormatter = formatter.FileFormatter; pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; +pub const FreetypeLoadFlags = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => Config.FreetypeLoadFlags, + + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + .web_canvas, + => void, +}; // Field types pub const ClipboardAccess = Config.ClipboardAccess; diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 25615bde5..b65b4bd2e 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -452,7 +452,18 @@ pub const LoadOptions = struct { /// for this is owned by the user and is not freed by the collection. metric_modifiers: Metrics.ModifierSet = .{}, - freetype_load_flags: config.Config.FreetypeLoadFlags = .{}, + freetype_load_flags: config.FreetypeLoadFlags = switch (font.options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => .{}, + + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + .web_canvas, + => {}, + }, pub fn deinit(self: *LoadOptions, alloc: Allocator) void { _ = self; diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index d4ad49a74..49886c8f2 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -428,7 +428,7 @@ pub const DerivedConfig = struct { @"adjust-strikethrough-position": ?Metrics.Modifier, @"adjust-strikethrough-thickness": ?Metrics.Modifier, @"adjust-cursor-thickness": ?Metrics.Modifier, - @"freetype-load-flags": configpkg.Config.FreetypeLoadFlags, + @"freetype-load-flags": configpkg.FreetypeLoadFlags, /// Initialize a DerivedConfig. The config should be either a /// config.Config or another DerivedConfig to clone from. @@ -463,7 +463,18 @@ pub const DerivedConfig = struct { .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", - .@"freetype-load-flags" = config.@"freetype-load-flags", + .@"freetype-load-flags" = switch (font.options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => config.@"freetype-load-flags", + + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + .web_canvas, + => {}, + }, // This must be last so the arena contains all our allocations // from above since Zig does assignment in order. @@ -503,7 +514,18 @@ pub const Key = struct { /// font grid. font_size: DesiredSize = .{ .points = 12 }, - load_flags: configpkg.Config.FreetypeLoadFlags = .{}, + load_flags: configpkg.FreetypeLoadFlags = switch (font.options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => .{}, + + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + .web_canvas, + => {}, + }, const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; diff --git a/src/font/face.zig b/src/font/face.zig index d3fd89aa5..663a86672 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const options = @import("main.zig").options; pub const Metrics = @import("face/Metrics.zig"); -const FreetypeLoadFlags = @import("../config/Config.zig").FreetypeLoadFlags; +const config = @import("../config.zig"); const freetype = @import("face/freetype.zig"); const coretext = @import("face/coretext.zig"); pub const web_canvas = @import("face/web_canvas.zig"); @@ -31,7 +31,7 @@ pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; pub const Options = struct { size: DesiredSize, metric_modifiers: ?*const Metrics.ModifierSet = null, - freetype_load_flags: FreetypeLoadFlags, + freetype_load_flags: config.FreetypeLoadFlags, }; /// The desired size for loading a font. diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index ac173d6dd..2715d664a 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -18,7 +18,7 @@ const Library = font.Library; const convert = @import("freetype_convert.zig"); const fastmem = @import("../../fastmem.zig"); const quirks = @import("../../quirks.zig"); -const FreetypeLoadFlags = @import("../../config/Config.zig").FreetypeLoadFlags; +const config = @import("../../config.zig"); const log = std.log.scoped(.font_face); @@ -36,7 +36,7 @@ pub const Face = struct { metrics: font.face.Metrics, /// Metrics for this font face. These are useful for renderers. - load_flags: FreetypeLoadFlags, + load_flags: config.FreetypeLoadFlags, /// Set quirks.disableDefaultFontFeatures quirks_disable_default_font_features: bool = false, From e7f286d83fc5d43be519abd0e21a03fadb43377f Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 01:40:39 +0100 Subject: [PATCH 31/46] docs: describe `load_flags` field in `Face` struct --- src/font/face/freetype.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 2715d664a..5bbbb246b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -35,7 +35,7 @@ pub const Face = struct { /// Metrics for this font face. These are useful for renderers. metrics: font.face.Metrics, - /// Metrics for this font face. These are useful for renderers. + /// Freetype load flags for this font face. load_flags: config.FreetypeLoadFlags, /// Set quirks.disableDefaultFontFeatures From b353ddf46dafe2bec4fa397ab8fb69f59fb5f911 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 8 Nov 2024 22:16:44 -0600 Subject: [PATCH 32/46] core/gtk: unify libadwaita/adwaita options in the code Fixes #2574 --- build.zig | 8 ++++---- src/apprt/gtk/adwaita.zig | 4 ++-- src/apprt/gtk/c.zig | 2 +- src/build_config.zig | 6 +++--- src/cli/version.zig | 2 +- src/config/Config.zig | 10 +++++----- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build.zig b/build.zig index 2cce41a31..c50932b63 100644 --- a/build.zig +++ b/build.zig @@ -92,10 +92,10 @@ pub fn build(b: *std.Build) !void { "The app runtime to use. Not all values supported on all platforms.", ) orelse renderer.Impl.default(target.result, wasm_target); - config.libadwaita = b.option( + config.adwaita = b.option( bool, - "gtk-libadwaita", - "Enables the use of libadwaita when using the gtk rendering backend.", + "gtk-adwaita", + "Enables the use of Adwaita when using the GTK rendering backend.", ) orelse true; const conformance = b.option( @@ -1321,7 +1321,7 @@ fn addDeps( .gtk => { step.linkSystemLibrary2("gtk4", dynamic_link_opts); - if (config.libadwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); + if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); { const gresource = @import("src/apprt/gtk/gresource.zig"); diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig index ac9c59674..2c28bc39b 100644 --- a/src/apprt/gtk/adwaita.zig +++ b/src/apprt/gtk/adwaita.zig @@ -12,7 +12,7 @@ const Config = @import("../../config.zig").Config; /// This must be `inline` so that the comptime check noops conditional /// paths that are not enabled. pub inline fn enabled(config: *const Config) bool { - return build_options.libadwaita and + return build_options.adwaita and config.@"gtk-adwaita"; } @@ -30,7 +30,7 @@ pub fn versionAtLeast( comptime minor: u16, comptime micro: u16, ) bool { - if (comptime !build_options.libadwaita) return false; + if (comptime !build_options.adwaita) return false; // If our header has lower versions than the given version, // we can return false immediately. This prevents us from diff --git a/src/apprt/gtk/c.zig b/src/apprt/gtk/c.zig index e8788afee..63801250e 100644 --- a/src/apprt/gtk/c.zig +++ b/src/apprt/gtk/c.zig @@ -1,7 +1,7 @@ /// Imported C API directly from header files pub const c = @cImport({ @cInclude("gtk/gtk.h"); - if (@import("build_options").libadwaita) { + if (@import("build_options").adwaita) { @cInclude("libadwaita-1/adwaita.h"); } diff --git a/src/build_config.zig b/src/build_config.zig index 48391ef4f..715552e03 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -21,7 +21,7 @@ const WasmTarget = @import("os/wasm/target.zig").Target; pub const BuildConfig = struct { version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, flatpak: bool = false, - libadwaita: bool = false, + adwaita: bool = false, app_runtime: apprt.Runtime = .none, renderer: rendererpkg.Impl = .opengl, font_backend: font.Backend = .freetype, @@ -40,7 +40,7 @@ pub const BuildConfig = struct { // We need to break these down individual because addOption doesn't // support all types. step.addOption(bool, "flatpak", self.flatpak); - step.addOption(bool, "libadwaita", self.libadwaita); + step.addOption(bool, "adwaita", self.adwaita); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(rendererpkg.Impl, "renderer", self.renderer); @@ -67,7 +67,7 @@ pub const BuildConfig = struct { return .{ .version = options.app_version, .flatpak = options.flatpak, - .libadwaita = options.libadwaita, + .adwaita = options.adwaita, .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, .renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?, diff --git a/src/cli/version.zig b/src/cli/version.zig index 6212b1743..26d5dcc74 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -42,7 +42,7 @@ pub fn run(alloc: Allocator) !u8 { gtk.gtk_get_minor_version(), gtk.gtk_get_micro_version(), }); - if (comptime build_options.libadwaita) { + if (comptime build_options.adwaita) { try stdout.print(" - libadwaita : enabled\n", .{}); try stdout.print(" build : {s}\n", .{ gtk.ADW_VERSION_S, diff --git a/src/config/Config.zig b/src/config/Config.zig index e6b9d35ab..f2622d726 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -940,7 +940,7 @@ keybind: Keybinds = .{}, /// * `dark` - Use the dark theme regardless of system theme. /// * `ghostty` - Use the background and foreground colors specified in the /// Ghostty configuration. This is only supported on Linux builds with -/// libadwaita and `gtk-adwaita` enabled. +/// Adwaita and `gtk-adwaita` enabled. /// /// On macOS, if `macos-titlebar-style` is "tabs", the window theme will be /// automatically set based on the luminosity of the terminal background color. @@ -1618,12 +1618,12 @@ keybind: Keybinds = .{}, /// Determines the side of the screen that the GTK tab bar will stick to. /// Top, bottom, left, and right are supported. The default is top. /// -/// If this option has value `left` or `right` when using `libadwaita`, it falls +/// If this option has value `left` or `right` when using Adwaita, it falls /// back to `top`. @"gtk-tabs-location": GtkTabsLocation = .top, /// Determines the appearance of the top and bottom bars when using the -/// adwaita tab bar. This requires `gtk-adwaita` to be enabled (it is +/// Adwaita tab bar. This requires `gtk-adwaita` to be enabled (it is /// by default). /// /// Valid values are: @@ -1642,7 +1642,7 @@ keybind: Keybinds = .{}, /// which is the old style. @"gtk-wide-tabs": bool = true, -/// If `true` (default), Ghostty will enable libadwaita theme support. This +/// If `true` (default), Ghostty will enable Adwaita theme support. This /// will make `window-theme` work properly and will also allow Ghostty to /// properly respond to system theme changes, light/dark mode changing, etc. /// This requires a GTK4 desktop with a GTK4 theme. @@ -1653,7 +1653,7 @@ keybind: Keybinds = .{}, /// expected. /// /// This configuration only has an effect if Ghostty was built with -/// libadwaita support. +/// Adwaita support. @"gtk-adwaita": bool = true, /// If `true` (default), applications running in the terminal can show desktop From ca844ca3c064cfdd51a9c29a20dcbfe314c0d712 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 2 Nov 2024 23:30:21 -0500 Subject: [PATCH 33/46] core: list valid options if an invalid value is detected parsing an enum --- src/cli/args.zig | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/cli/args.zig b/src/cli/args.zig index bfd40c633..9a8d1ae42 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -133,7 +133,29 @@ pub fn parse( error.OutOfMemory => return err, error.InvalidField => "unknown field", error.ValueRequired => "value required", - error.InvalidValue => "invalid value", + error.InvalidValue => msg: { + var buf = std.ArrayList(u8).init(arena_alloc); + errdefer buf.deinit(); + const writer = buf.writer(); + try writer.print("invalid value \"{?s}\"", .{value}); + const typeinfo = @typeInfo(T); + inline for (typeinfo.Struct.fields) |f| { + if (std.mem.eql(u8, key, f.name)) { + switch (@typeInfo(f.type)) { + .Enum => |e| { + try writer.print(", valid values are: ", .{}); + inline for (e.fields, 0..) |field, i| { + if (i != 0) try writer.print(", ", .{}); + try writer.print("{s}", .{field.name}); + } + }, + else => {}, + } + break; + } + } + break :msg try buf.toOwnedSliceSentinel(0); + }, else => try std.fmt.allocPrintZ( arena_alloc, "unknown error {}", From 3c493f2d0d1565a71e2fc7d935617a7d85ce08e2 Mon Sep 17 00:00:00 2001 From: Pepper Lebeck-Jobe Date: Sat, 9 Nov 2024 10:10:29 +0100 Subject: [PATCH 34/46] Fix copying the theme name Prior to this change both C and c would copy the path to the theme even though the help screen claimed that c would copy the theme name. There is a bug in libvaxis that results in both of these matches matching c: `key.matches('c', .{})` `key.matches('c', .{ .shift = true })` Tested: Before the change: 'c' copies path and 'C' copies path After the change: 'c' copies the name and 'C' copies the path --- src/cli/list_themes.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 92cb57be2..0903fab94 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -382,8 +382,8 @@ const Preview = struct { self.tty.anyWriter(), self.themes[self.filtered.items[self.current]].theme, alloc, - ); - if (key.matches('c', .{ .shift = true })) + ) + else if (key.matches('c', .{ .shift = true })) try self.vx.copyToSystemClipboard( self.tty.anyWriter(), self.themes[self.filtered.items[self.current]].path, From 83c4d0077b7a06d487057fca3bddcc8a3685ed03 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 11:55:29 +0100 Subject: [PATCH 35/46] refactor: define `FreetypeLoadFlags` struct and default in `font.face` --- src/config.zig | 12 ------------ src/font/Collection.zig | 13 +------------ src/font/SharedGridSet.zig | 15 ++------------- src/font/face.zig | 16 +++++++++++++++- src/font/face/freetype.zig | 2 +- 5 files changed, 19 insertions(+), 39 deletions(-) diff --git a/src/config.zig b/src/config.zig index f2d4876ae..08d93a6a3 100644 --- a/src/config.zig +++ b/src/config.zig @@ -11,18 +11,6 @@ pub const url = @import("config/url.zig"); pub const FileFormatter = formatter.FileFormatter; pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; -pub const FreetypeLoadFlags = switch (options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => Config.FreetypeLoadFlags, - - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - .web_canvas, - => void, -}; // Field types pub const ClipboardAccess = Config.ClipboardAccess; diff --git a/src/font/Collection.zig b/src/font/Collection.zig index b65b4bd2e..478c39ded 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -452,18 +452,7 @@ pub const LoadOptions = struct { /// for this is owned by the user and is not freed by the collection. metric_modifiers: Metrics.ModifierSet = .{}, - freetype_load_flags: config.FreetypeLoadFlags = switch (font.options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => .{}, - - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - .web_canvas, - => {}, - }, + freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, pub fn deinit(self: *LoadOptions, alloc: Allocator) void { _ = self; diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 49886c8f2..6459435a1 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -428,7 +428,7 @@ pub const DerivedConfig = struct { @"adjust-strikethrough-position": ?Metrics.Modifier, @"adjust-strikethrough-thickness": ?Metrics.Modifier, @"adjust-cursor-thickness": ?Metrics.Modifier, - @"freetype-load-flags": configpkg.FreetypeLoadFlags, + @"freetype-load-flags": font.face.FreetypeLoadFlags, /// Initialize a DerivedConfig. The config should be either a /// config.Config or another DerivedConfig to clone from. @@ -514,18 +514,7 @@ pub const Key = struct { /// font grid. font_size: DesiredSize = .{ .points = 12 }, - load_flags: configpkg.FreetypeLoadFlags = switch (font.options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => .{}, - - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - .web_canvas, - => {}, - }, + load_flags: configpkg.FreetypeLoadFlags = font.face.freetype_load_flags_default, const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; diff --git a/src/font/face.zig b/src/font/face.zig index 663a86672..24c9b0422 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -27,11 +27,25 @@ pub const Face = switch (options.backend) { /// using whatever platform method you can. pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; +pub const FreetypeLoadFlags = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => config.Config.FreetypeLoadFlags, + + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + .web_canvas, + => void, +}; +pub const freetype_load_flags_default = if (options.backend.hasFreetype()) .{} else {}; + /// Options for initializing a font face. pub const Options = struct { size: DesiredSize, metric_modifiers: ?*const Metrics.ModifierSet = null, - freetype_load_flags: config.FreetypeLoadFlags, + freetype_load_flags: FreetypeLoadFlags, }; /// The desired size for loading a font. diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 5bbbb246b..8a1465d7e 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -36,7 +36,7 @@ pub const Face = struct { metrics: font.face.Metrics, /// Freetype load flags for this font face. - load_flags: config.FreetypeLoadFlags, + load_flags: font.face.FreetypeLoadFlags, /// Set quirks.disableDefaultFontFeatures quirks_disable_default_font_features: bool = false, From 0e0751ad5b82c9926dd6fabd127edb086a915a9e Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 12:34:45 +0100 Subject: [PATCH 36/46] docs: write documentation for `freetype_load_flags` field --- src/font/Collection.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 478c39ded..f79c80936 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -452,6 +452,10 @@ pub const LoadOptions = struct { /// for this is owned by the user and is not freed by the collection. metric_modifiers: Metrics.ModifierSet = .{}, + /// Freetype Load Flags to use when loading glyphs. This is a list of + /// bitfield constants that controls operations to perform during glyph + /// loading. Only a subset is exposed for configuration, for the whole set + /// of flags see `pkg.freetype.face.LoadFlags`. freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, pub fn deinit(self: *LoadOptions, alloc: Allocator) void { From 08720a6d23d71c9b4fd8ae5324a205122e6809b9 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 12:49:53 +0100 Subject: [PATCH 37/46] chore: fix typo --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index aef589571..4cc943859 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -290,7 +290,7 @@ const c = @cImport({ /// Freetype load flags to enable. The format of this is a list of flags to /// enable separated by commas. If you prefix a flag with `no-` then it is /// disabled. If you omit a flag, it's default value is used, so you must -/// explicitely disable flags you don't want. You can also use `true` or `false` +/// explicitly disable flags you don't want. You can also use `true` or `false` /// to turn all flags on or off. /// /// Available flags: From 4c086882758f0e855e2cfecddea44a105d575b84 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 12:50:51 +0100 Subject: [PATCH 38/46] refactor: remove unused imports --- src/config.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config.zig b/src/config.zig index 08d93a6a3..b9f214fc9 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,8 +1,6 @@ const builtin = @import("builtin"); const formatter = @import("config/formatter.zig"); -const font = @import("font/main.zig"); -const options = font.options; pub const Config = @import("config/Config.zig"); pub const string = @import("config/string.zig"); pub const edit = @import("config/edit.zig"); From 69aa579ee3bc4a42ba7c3f4f86d83d4c46b52711 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 12:51:28 +0100 Subject: [PATCH 39/46] fix: use ternary if expression and correct types --- src/font/SharedGridSet.zig | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 6459435a1..72bca7277 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -463,18 +463,7 @@ pub const DerivedConfig = struct { .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", - .@"freetype-load-flags" = switch (font.options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => config.@"freetype-load-flags", - - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - .web_canvas, - => {}, - }, + .@"freetype-load-flags" = if (comptime font.options.backend.hasFreetype()) config.@"freetype-load-flags" else {}, // This must be last so the arena contains all our allocations // from above since Zig does assignment in order. @@ -514,7 +503,7 @@ pub const Key = struct { /// font grid. font_size: DesiredSize = .{ .points = 12 }, - load_flags: configpkg.FreetypeLoadFlags = font.face.freetype_load_flags_default, + load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; From 67966cb09189a05afa169e486f23e6cc1a57f7e6 Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 13:06:36 +0100 Subject: [PATCH 40/46] refactor: add default value for `freetype_load_flags' --- src/font/face.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/font/face.zig b/src/font/face.zig index 24c9b0422..991bfb7e0 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -45,7 +45,7 @@ pub const freetype_load_flags_default = if (options.backend.hasFreetype()) .{} e pub const Options = struct { size: DesiredSize, metric_modifiers: ?*const Metrics.ModifierSet = null, - freetype_load_flags: FreetypeLoadFlags, + freetype_load_flags: FreetypeLoadFlags = freetype_load_flags_default, }; /// The desired size for loading a font. From 4def80ce1670f522ee87932e53eb9238a59f0caf Mon Sep 17 00:00:00 2001 From: Nadir Fejzic Date: Sat, 9 Nov 2024 13:09:15 +0100 Subject: [PATCH 41/46] refactor: use if expression instead of switch --- src/font/face.zig | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index 991bfb7e0..7b51d660c 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -27,18 +27,7 @@ pub const Face = switch (options.backend) { /// using whatever platform method you can. pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; -pub const FreetypeLoadFlags = switch (options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => config.Config.FreetypeLoadFlags, - - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - .web_canvas, - => void, -}; +pub const FreetypeLoadFlags = if (options.backend.hasFreetype()) config.Config.FreetypeLoadFlags else void; pub const freetype_load_flags_default = if (options.backend.hasFreetype()) .{} else {}; /// Options for initializing a font face. From 783852ff48141c95d8c4dc19ec080ec1713328fe Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 9 Nov 2024 12:23:53 -0600 Subject: [PATCH 42/46] ci: fix adwaita build --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 409d6b075..68ab0b48a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -324,10 +324,10 @@ jobs: run: nix develop -c zig build -Dapp-runtime=none test - name: Test GTK Build - run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-libadwaita=true -Demit-docs + run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs - name: Test GTK Build (No Libadwaita) - run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-libadwaita=false -Demit-docs + run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=false -Demit-docs - name: Test GLFW Build run: nix develop -c zig build -Dapp-runtime=glfw From 3eef6d205e508557d05a3e619400689a81f9aacf Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 9 Nov 2024 12:49:40 -0600 Subject: [PATCH 43/46] core: address review comments - break formatting values out into a function so that we can catch errors and never fail - eliminate the use of toOwnedSentinelSlice since we are using an arena to clean up memory --- src/cli/args.zig | 74 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/src/cli/args.zig b/src/cli/args.zig index 9a8d1ae42..5fdaf6d8b 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -132,30 +132,8 @@ pub fn parse( // track more error messages. error.OutOfMemory => return err, error.InvalidField => "unknown field", - error.ValueRequired => "value required", - error.InvalidValue => msg: { - var buf = std.ArrayList(u8).init(arena_alloc); - errdefer buf.deinit(); - const writer = buf.writer(); - try writer.print("invalid value \"{?s}\"", .{value}); - const typeinfo = @typeInfo(T); - inline for (typeinfo.Struct.fields) |f| { - if (std.mem.eql(u8, key, f.name)) { - switch (@typeInfo(f.type)) { - .Enum => |e| { - try writer.print(", valid values are: ", .{}); - inline for (e.fields, 0..) |field, i| { - if (i != 0) try writer.print(", ", .{}); - try writer.print("{s}", .{field.name}); - } - }, - else => {}, - } - break; - } - } - break :msg try buf.toOwnedSliceSentinel(0); - }, + error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required", + error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value", else => try std.fmt.allocPrintZ( arena_alloc, "unknown error {}", @@ -173,6 +151,54 @@ pub fn parse( } } +fn formatValueRequired( + comptime T: type, + arena_alloc: std.mem.Allocator, + key: []const u8, +) std.mem.Allocator.Error![:0]const u8 { + var buf = std.ArrayList(u8).init(arena_alloc); + errdefer buf.deinit(); + const writer = buf.writer(); + try writer.print("value required", .{}); + try formatValues(T, key, writer); + try writer.writeByte(0); + return buf.items[0 .. buf.items.len - 1 :0]; +} + +fn formatInvalidValue( + comptime T: type, + arena_alloc: std.mem.Allocator, + key: []const u8, + value: ?[]const u8, +) std.mem.Allocator.Error![:0]const u8 { + var buf = std.ArrayList(u8).init(arena_alloc); + errdefer buf.deinit(); + const writer = buf.writer(); + try writer.print("invalid value \"{?s}\"", .{value}); + try formatValues(T, key, writer); + try writer.writeByte(0); + return buf.items[0 .. buf.items.len - 1 :0]; +} + +fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void { + const typeinfo = @typeInfo(T); + inline for (typeinfo.Struct.fields) |f| { + if (std.mem.eql(u8, key, f.name)) { + switch (@typeInfo(f.type)) { + .Enum => |e| { + try writer.print(", valid values are: ", .{}); + inline for (e.fields, 0..) |field, i| { + if (i != 0) try writer.print(", ", .{}); + try writer.print("{s}", .{field.name}); + } + }, + else => {}, + } + break; + } + } +} + /// Returns true if this type can track diagnostics. fn canTrackDiags(comptime T: type) bool { return @hasField(T, "_diagnostics"); From 3ee6577154b8b78e4113dbaec4c153ee1535e073 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2024 09:37:03 -0800 Subject: [PATCH 44/46] some tweaks --- src/config.zig | 1 + src/config/Config.zig | 25 +++++++++++++++---------- src/font/SharedGridSet.zig | 15 ++++++++++----- src/font/face.zig | 9 +++++++-- src/font/face/freetype.zig | 7 ++++++- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/config.zig b/src/config.zig index b9f214fc9..5af7832dd 100644 --- a/src/config.zig +++ b/src/config.zig @@ -16,6 +16,7 @@ pub const CopyOnSelect = Config.CopyOnSelect; pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const FontSyntheticStyle = Config.FontSyntheticStyle; pub const FontStyle = Config.FontStyle; +pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; pub const NonNativeFullscreen = Config.NonNativeFullscreen; diff --git a/src/config/Config.zig b/src/config/Config.zig index 4cc943859..d8f007435 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -287,21 +287,24 @@ const c = @cImport({ /// terminals. Only new terminals will use the new configuration. @"grapheme-width-method": GraphemeWidthMethod = .unicode, -/// Freetype load flags to enable. The format of this is a list of flags to +/// FreeType load flags to enable. The format of this is a list of flags to /// enable separated by commas. If you prefix a flag with `no-` then it is /// disabled. If you omit a flag, it's default value is used, so you must /// explicitly disable flags you don't want. You can also use `true` or `false` /// to turn all flags on or off. /// +/// This configuration only applies to Ghostty builds that use FreeType. +/// This is usually the case only for Linux builds. macOS uses CoreText +/// and does not have an equivalent configuration. +/// /// Available flags: /// /// * `hinting` - Enable or disable hinting, enabled by default. -/// * `bitmap` - Enable or disable loading of any pre-rendered bitmap strikes, -/// enabled by default -/// * `force-autohint` - Use the freetype auto-hinter rather than the font's -/// native hinter. Enabled by default. -/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering. -/// This option doesn't impact the hinter. Enabled by default. +/// * `force-autohint` - Use the freetype auto-hinter rather than the +/// font's native hinter. Enabled by default. +/// * `monochrome` - Instructs renderer to use 1-bit monochrome +/// rendering. This option doesn't impact the hinter. +/// Enabled by default. /// * `autohint` - Use the freetype auto-hinter. Enabled by default. /// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` @@ -4587,10 +4590,12 @@ pub const GraphemeWidthMethod = enum { /// See freetype-load-flag pub const FreetypeLoadFlags = packed struct { + // The defaults here at the time of writing this match the defaults + // for Freetype itself. Ghostty hasn't made any opinionated changes + // to these defaults. hinting: bool = true, - bitmap: bool = true, - @"force-autohint": bool = false, - monochrome: bool = false, + @"force-autohint": bool = true, + monochrome: bool = true, autohint: bool = true, }; diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 72bca7277..ac2fcbf8a 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -168,7 +168,7 @@ fn collection( .library = self.font_lib, .size = size, .metric_modifiers = key.metric_modifiers, - .freetype_load_flags = config.@"freetype-load-flags", + .freetype_load_flags = key.freetype_load_flags, }; var c = Collection.init(); @@ -463,7 +463,7 @@ pub const DerivedConfig = struct { .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", - .@"freetype-load-flags" = if (comptime font.options.backend.hasFreetype()) config.@"freetype-load-flags" else {}, + .@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {}, // This must be last so the arena contains all our allocations // from above since Zig does assignment in order. @@ -503,7 +503,9 @@ pub const Key = struct { /// font grid. font_size: DesiredSize = .{ .points = 12 }, - load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, + /// The freetype load flags configuration, only non-void if the + /// freetype backend is enabled. + freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); const StyleOffsets = [style_offsets_len]usize; @@ -623,7 +625,10 @@ pub const Key = struct { .codepoint_map = codepoint_map, .metric_modifiers = metric_modifiers, .font_size = font_size, - .load_flags = config.@"freetype-load-flags", + .freetype_load_flags = if (font.face.FreetypeLoadFlags != void) + config.@"freetype-load-flags" + else + font.face.freetype_load_flags_default, }; } @@ -653,7 +658,7 @@ pub const Key = struct { for (self.descriptors) |d| d.hash(hasher); self.codepoint_map.hash(hasher); autoHash(hasher, self.metric_modifiers.count()); - autoHash(hasher, self.load_flags); + autoHash(hasher, self.freetype_load_flags); if (self.metric_modifiers.count() > 0) { inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { const key = @field(Metrics.Key, field.name); diff --git a/src/font/face.zig b/src/font/face.zig index 7b51d660c..9f80c5637 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -27,8 +27,13 @@ pub const Face = switch (options.backend) { /// using whatever platform method you can. pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; -pub const FreetypeLoadFlags = if (options.backend.hasFreetype()) config.Config.FreetypeLoadFlags else void; -pub const freetype_load_flags_default = if (options.backend.hasFreetype()) .{} else {}; +/// These are the flags to customize how freetype loads fonts. This is +/// only non-void if the freetype backend is enabled. +pub const FreetypeLoadFlags = if (options.backend.hasFreetype()) + config.FreetypeLoadFlags +else + void; +pub const freetype_load_flags_default = if (FreetypeLoadFlags != void) .{} else {}; /// Options for initializing a font face. pub const Options = struct { diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 8a1465d7e..683f80cc8 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -23,6 +23,11 @@ const config = @import("../../config.zig"); const log = std.log.scoped(.font_face); pub const Face = struct { + comptime { + // If we have the freetype backend, we should have load flags. + assert(font.face.FreetypeLoadFlags != void); + } + /// Our freetype library lib: freetype.Library, @@ -323,7 +328,7 @@ pub const Face = struct { // // This must be enabled for color faces though because those are // often colored bitmaps, which we support. - .no_bitmap = !self.face.hasColor() or !self.load_flags.bitmap, + .no_bitmap = !self.face.hasColor(), // use options from config .no_hinting = !self.load_flags.hinting, From f6ea15dd21e976af7cdf4ee7236447479ab8fef8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Nov 2024 10:12:04 -0800 Subject: [PATCH 45/46] macos: allow additional modifiers through for ctrl+enter The bug: ctrl+shift+enter on macOS 15 shows a context menu and doesn't encode to the terminal. This avoids a system-wide keybind that shows a context menu in macOS 15+. In general Ghostty doesn't try to override system-wide keybinds but this one is particularly annoying and not useful to terminal users. We've discussed making this logic configurable for all system level keybinds but for now this is a quick fix specifically for ctrl+shift+enter. --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 872ce17ec..512a5239b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -704,12 +704,6 @@ extension Ghostty { /// Special case handling for some control keys override func performKeyEquivalent(with event: NSEvent) -> Bool { - // Only process keys when Control is the only modifier - if (!event.modifierFlags.contains(.control) || - !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { - return false - } - // Only process key down events if (event.type != .keyDown) { return false @@ -722,11 +716,23 @@ extension Ghostty { return false } + // Only process keys when Control is active. All known issues we're + // resolving happen only in this scenario. This probably isn't fully robust + // but we can broaden the scope as we find more cases. + if (!event.modifierFlags.contains(.control)) { + return false + } + let equivalent: String switch (event.charactersIgnoringModifiers) { case "/": // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep // sound and we don't like the beep sound. + if (!event.modifierFlags.contains(.control) || + !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { + return false + } + equivalent = "_" case "\r": @@ -742,7 +748,7 @@ extension Ghostty { let newEvent = NSEvent.keyEvent( with: .keyDown, location: event.locationInWindow, - modifierFlags: .control, + modifierFlags: event.modifierFlags, timestamp: event.timestamp, windowNumber: event.windowNumber, context: nil, From dfe33dc9bb0f766953203c41c1fa3546758f5a82 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Nov 2024 19:23:34 -0800 Subject: [PATCH 46/46] build.zig: add -Dpie option for position independent executables This is required by some packaging ecosytems. --- build.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.zig b/build.zig index c50932b63..f93e18b7b 100644 --- a/build.zig +++ b/build.zig @@ -98,6 +98,12 @@ pub fn build(b: *std.Build) !void { "Enables the use of Adwaita when using the GTK rendering backend.", ) orelse true; + const pie = b.option( + bool, + "pie", + "Build a Position Independent Executable", + ) orelse false; + const conformance = b.option( []const u8, "conformance", @@ -282,6 +288,9 @@ pub fn build(b: *std.Build) !void { // Exe if (exe_) |exe| { + // Set PIE if requested + if (pie) exe.pie = true; + // Add the shared dependencies _ = try addDeps(b, exe, config);