From 70bc29f815ad25b954e66481c7f2d4ba91caff0d Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 27 Jul 2025 16:30:55 -0400 Subject: [PATCH 001/319] Initial testing including uucode --- build.zig.zon | 4 ++++ src/build/SharedDeps.zig | 26 ++++++++++++++++++++++++++ src/global.zig | 6 ++++++ 3 files changed, 36 insertions(+) diff --git a/build.zig.zon b/build.zig.zon index cb077768d..addfacb16 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -41,6 +41,10 @@ .hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", .lazy = true, }, + .uucode = .{ + .url = "https://github.com/jacobsandlund/uucode/archive/fdcd8582f050e3054091d3ce3c60bf7a78bc8830.tar.gz", + .hash = "uucode-0.0.0-ZZjBPpnZOgCLQD3BsYbQ4N6dGqR3CRKlOngOBB3YEU_j", + }, .zig_wayland = .{ // codeberg ifreund/zig-wayland .url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index c03746a48..543c87df2 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -15,6 +15,7 @@ help_strings: HelpStrings, metallib: ?*MetallibStep, unicode_tables: UnicodeTables, framedata: GhosttyFrameData, +uucode_table_data: std.Build.LazyPath, /// Used to keep track of a list of file sources. pub const LazyPathList = std.ArrayList(std.Build.LazyPath); @@ -25,6 +26,24 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { .help_strings = try .init(b, cfg), .unicode_tables = try .init(b), .framedata = try .init(b), + .uucode_table_data = b.dependency("uucode", .{ + .@"table_configs.zig" = @as( + []const u8, + \\const types = @import("types.zig"); + \\const config = @import("config.zig"); + \\ + \\pub const configs = [_]types.TableConfig{ + \\ .override(&config.default, .{ + \\ .fields = &.{"case_folding_simple"}, + \\ }), + \\ .override(&config.default, .{ + \\ .fields = &.{"alphabetic","lowercase","uppercase"}, + \\ }), + \\}; + \\ + , + ), + }).namedLazyPath("table_data.zig"), // Setup by retarget .options = undefined, @@ -415,6 +434,13 @@ pub fn add( })) |dep| { step.root_module.addImport("ziglyph", dep.module("ziglyph")); } + if (b.lazyDependency("uucode", .{ + .target = target, + .optimize = optimize, + .@"table_data.zig" = self.uucode_table_data, + })) |dep| { + step.root_module.addImport("uucode", dep.module("uucode")); + } if (b.lazyDependency("zf", .{ .target = target, .optimize = optimize, diff --git a/src/global.zig b/src/global.zig index e68ec7f74..6d0aa655d 100644 --- a/src/global.zig +++ b/src/global.zig @@ -10,6 +10,7 @@ const oni = @import("oniguruma"); const crash = @import("crash/main.zig"); const renderer = @import("renderer.zig"); const apprt = @import("apprt.zig"); +const uucode = @import("uucode"); /// We export the xev backend we want to use so that the rest of /// Ghostty can import this once and have access to the proper @@ -54,6 +55,11 @@ pub const GlobalState = struct { // std.log.err("[global init time] start={}us duration={}ns", .{ start_micro, end.since(start) / std.time.ns_per_us }); // } + std.log.err("XXX Uucode testing: {d}, {}\n", .{ + uucode.case_folding_simple(65), + uucode.alphabetic(97), + }); + // Initialize ourself to nothing so we don't have any extra state. // IMPORTANT: this MUST be initialized before any log output because // the log function uses the global state. From fb2cab5aeefad229246306efc9db3a224347e633 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 27 Jul 2025 18:25:05 -0400 Subject: [PATCH 002/319] Using uucode in a few places where it's easy. --- build.zig.zon | 4 ++-- src/build/SharedDeps.zig | 8 ++++---- src/font/CodepointResolver.zig | 4 ++-- src/font/shaper/web_canvas.zig | 6 +++--- src/global.zig | 6 ------ src/renderer/cell.zig | 7 +++++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index addfacb16..6cd2201e0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/fdcd8582f050e3054091d3ce3c60bf7a78bc8830.tar.gz", - .hash = "uucode-0.0.0-ZZjBPpnZOgCLQD3BsYbQ4N6dGqR3CRKlOngOBB3YEU_j", + .url = "https://github.com/jacobsandlund/uucode/archive/539c710408cda93d2fa28825d67cb5685d963fc1.tar.gz", + .hash = "uucode-0.0.0-ZZjBPpz5OgBrcQOasHVZYLtDHcLPx9al7RH4QXJZ8XCK", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 543c87df2..8875c98a7 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -34,10 +34,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { \\ \\pub const configs = [_]types.TableConfig{ \\ .override(&config.default, .{ - \\ .fields = &.{"case_folding_simple"}, - \\ }), - \\ .override(&config.default, .{ - \\ .fields = &.{"alphabetic","lowercase","uppercase"}, + \\ .fields = &.{ + \\ "general_category", + \\ "has_emoji_presentation", + \\ }, \\ }), \\}; \\ diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index ba74065ab..ec318abe5 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -13,7 +13,7 @@ const CodepointResolver = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; -const ziglyph = @import("ziglyph"); +const uucode = @import("uucode"); const font = @import("main.zig"); const Atlas = font.Atlas; const CodepointMap = font.CodepointMap; @@ -150,7 +150,7 @@ pub fn getIndex( // we'll do this multiple times if we recurse, but this is a cached function // call higher up (GroupCache) so this should be rare. const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp))) + .default = if (uucode.hasEmojiPresentation(@intCast(cp))) .emoji else .text, diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index 4ed4b7db6..e0f0e1a00 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -1,9 +1,9 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const ziglyph = @import("ziglyph"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const unicode = @import("../../unicode/main.zig"); const log = std.log.scoped(.font_shaper); @@ -111,7 +111,7 @@ pub const Shaper = struct { // font ligatures. However, we do support grapheme clustering. // This means we can render things like skin tone emoji but // we can't render things like single glyph "=>". - var break_state: u3 = 0; + var break_state: unicode.GraphemeBreakState = .{}; var cp1: u21 = @intCast(codepoints[0]); var start: usize = 0; @@ -126,7 +126,7 @@ pub const Shaper = struct { const cp2: u21 = @intCast(codepoints[i]); defer cp1 = cp2; - break :blk ziglyph.graphemeBreak( + break :blk unicode.graphemeBreak( cp1, cp2, &break_state, diff --git a/src/global.zig b/src/global.zig index 6d0aa655d..e68ec7f74 100644 --- a/src/global.zig +++ b/src/global.zig @@ -10,7 +10,6 @@ const oni = @import("oniguruma"); const crash = @import("crash/main.zig"); const renderer = @import("renderer.zig"); const apprt = @import("apprt.zig"); -const uucode = @import("uucode"); /// We export the xev backend we want to use so that the rest of /// Ghostty can import this once and have access to the proper @@ -55,11 +54,6 @@ pub const GlobalState = struct { // std.log.err("[global init time] start={}us duration={}ns", .{ start_micro, end.since(start) / std.time.ns_per_us }); // } - std.log.err("XXX Uucode testing: {d}, {}\n", .{ - uucode.case_folding_simple(65), - uucode.alphabetic(97), - }); - // Initialize ourself to nothing so we don't have any extra state. // IMPORTANT: this MUST be initialized before any log output because // the log function uses the global state. diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index b1ce4523c..d72e5965c 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const ziglyph = @import("ziglyph"); +const uucode = @import("uucode"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); @@ -235,7 +236,8 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const cell = cell_pin.rowAndCell().cell; const cp = cell.codepoint(); - if (!ziglyph.general_category.isPrivateUse(cp) and + // If not a Co (Private Use) and not a Dingbats, use grid width. + if (uucode.generalCategory(cp) != .Co and !ziglyph.blocks.isDingbats(cp)) { return cell.gridWidth(); @@ -259,7 +261,8 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // We consider powerline glyphs whitespace. if (isPowerline(prev_cp)) break :prev; - if (ziglyph.general_category.isPrivateUse(prev_cp)) { + // If it's Private Use (Co) use 1 as the width. + if (uucode.generalCategory(prev_cp) == .Co) { return 1; } } From 16c7ebad1d691fee0610176498ecc32914f51fc9 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 27 Jul 2025 18:48:39 -0400 Subject: [PATCH 003/319] Add TODO about configuration --- src/build/SharedDeps.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 8875c98a7..669fa5892 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -27,6 +27,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { .unicode_tables = try .init(b), .framedata = try .init(b), .uucode_table_data = b.dependency("uucode", .{ + // TODO: i'll add a nicer option to configure the tables rather + // than needing to type out the zig code as a string. .@"table_configs.zig" = @as( []const u8, \\const types = @import("types.zig"); From 807d128e206e1ff7d743e938e7eda5b270919b85 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 30 Jul 2025 11:22:35 -0400 Subject: [PATCH 004/319] Use `table_0_fields` build option --- build.zig.zon | 4 ++-- src/build/SharedDeps.zig | 30 ++++++++---------------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 6cd2201e0..7963e7818 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/539c710408cda93d2fa28825d67cb5685d963fc1.tar.gz", - .hash = "uucode-0.0.0-ZZjBPpz5OgBrcQOasHVZYLtDHcLPx9al7RH4QXJZ8XCK", + .url = "https://github.com/jacobsandlund/uucode/archive/35c0a6b7d653fa91a46c5d0ca07f9742a466f0ad.tar.gz", + .hash = "uucode-0.0.0-ZZjBPn4dOwCKhxEJJqsTGrnmITqhnQvvISkhOXvSHcRE", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 669fa5892..46866085f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -15,7 +15,7 @@ help_strings: HelpStrings, metallib: ?*MetallibStep, unicode_tables: UnicodeTables, framedata: GhosttyFrameData, -uucode_table_data: std.Build.LazyPath, +uucode_tables_zig: std.Build.LazyPath, /// Used to keep track of a list of file sources. pub const LazyPathList = std.ArrayList(std.Build.LazyPath); @@ -26,26 +26,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { .help_strings = try .init(b, cfg), .unicode_tables = try .init(b), .framedata = try .init(b), - .uucode_table_data = b.dependency("uucode", .{ - // TODO: i'll add a nicer option to configure the tables rather - // than needing to type out the zig code as a string. - .@"table_configs.zig" = @as( - []const u8, - \\const types = @import("types.zig"); - \\const config = @import("config.zig"); - \\ - \\pub const configs = [_]types.TableConfig{ - \\ .override(&config.default, .{ - \\ .fields = &.{ - \\ "general_category", - \\ "has_emoji_presentation", - \\ }, - \\ }), - \\}; - \\ - , - ), - }).namedLazyPath("table_data.zig"), + .uucode_tables_zig = b.dependency("uucode", .{ + .table_0_fields = @as([]const []const u8, &[_][]const u8{ + "general_category", + "has_emoji_presentation", + }), + }).namedLazyPath("tables.zig"), // Setup by retarget .options = undefined, @@ -439,7 +425,7 @@ pub fn add( if (b.lazyDependency("uucode", .{ .target = target, .optimize = optimize, - .@"table_data.zig" = self.uucode_table_data, + .@"tables.zig" = self.uucode_tables_zig, })) |dep| { step.root_module.addImport("uucode", dep.module("uucode")); } From 8dec520b41ea5a3ff58b858e004407d16a65f2ba Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 4 Aug 2025 10:02:41 -0400 Subject: [PATCH 005/319] testing uucode.x --- build.zig.zon | 8 ++++++-- src/build/SharedDeps.zig | 18 ++++++++++++------ src/build/uucode_build_config.zig | 14 ++++++++++++++ src/build/uucode_x.zig | 8 ++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/build/uucode_build_config.zig create mode 100644 src/build/uucode_x.zig diff --git a/build.zig.zon b/build.zig.zon index 76097ffb5..86ae38786 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,12 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/35c0a6b7d653fa91a46c5d0ca07f9742a466f0ad.tar.gz", - .hash = "uucode-0.0.0-ZZjBPn4dOwCKhxEJJqsTGrnmITqhnQvvISkhOXvSHcRE", + .url = "https://github.com/jacobsandlund/uucode/archive/7d851864a21d1d700d41230762de66f7b7fa5941.tar.gz", + .hash = "uucode-0.0.0-ZZjBPqk4OwDcGSplCRYO-zLKcs3FZ-jQGj9rfSEf6VZr", + }, + .uucode_x = .{ + .url = "https://github.com/jacobsandlund/uucode.x/archive/779492c4565ed282a7c989ffc7f52cbe060f17a3.tar.gz", + .hash = "uucode_x-0.0.0-5_D0j-gIAAC4SzTTunb6O4sqwj0kojbjV0rm2bj9QgyA", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 46866085f..b52482385 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -21,17 +21,22 @@ uucode_tables_zig: std.Build.LazyPath, pub const LazyPathList = std.ArrayList(std.Build.LazyPath); pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { + const uucode_x = b.dependency("uucode_x", .{}); + const uucode_x_config = uucode_x.module("uucode.x.config"); + + const uucode = b.dependency("uucode", .{ + .build_config_path = b.path("src/build/uucode_build_config.zig"), + }); + uucode.module("build_config").addImport("uucode.x.config", uucode_x_config); + uucode_x_config.addImport("config.zig", uucode.module("config.zig")); + uucode_x_config.addImport("types.zig", uucode.module("types.zig")); + var result: SharedDeps = .{ .config = cfg, .help_strings = try .init(b, cfg), .unicode_tables = try .init(b), .framedata = try .init(b), - .uucode_tables_zig = b.dependency("uucode", .{ - .table_0_fields = @as([]const []const u8, &[_][]const u8{ - "general_category", - "has_emoji_presentation", - }), - }).namedLazyPath("tables.zig"), + .uucode_tables_zig = uucode.namedLazyPath("tables.zig"), // Setup by retarget .options = undefined, @@ -426,6 +431,7 @@ pub fn add( .target = target, .optimize = optimize, .@"tables.zig" = self.uucode_tables_zig, + .x_root_path = b.path("src/build/uucode_x.zig"), })) |dep| { step.root_module.addImport("uucode", dep.module("uucode")); } diff --git a/src/build/uucode_build_config.zig b/src/build/uucode_build_config.zig new file mode 100644 index 000000000..4c87dfdc4 --- /dev/null +++ b/src/build/uucode_build_config.zig @@ -0,0 +1,14 @@ +const config = @import("config.zig"); +const x = @import("uucode.x.config"); +const d = config.default; + +pub const tables = [_]config.Table{ + .{ + .extensions = &.{x.width}, + .fields = &.{ + x.width.field("width"), + d.field("general_category"), + d.field("has_emoji_presentation"), + }, + }, +}; diff --git a/src/build/uucode_x.zig b/src/build/uucode_x.zig new file mode 100644 index 000000000..ce305e3cc --- /dev/null +++ b/src/build/uucode_x.zig @@ -0,0 +1,8 @@ +const get = @import("get.zig"); +const tableFor = get.tableFor; +const data = get.data; + +pub fn width(cp: u21) u2 { + const table = comptime tableFor("width"); + return data(table, cp).width; +} From 0c393299b0053964ece7e83a40cf18a4c8f4acbf Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 5 Aug 2025 23:59:30 -0400 Subject: [PATCH 006/319] using just `get` --- build.zig.zon | 8 ++++---- src/build/SharedDeps.zig | 20 +++++++++---------- src/build/UnicodeTables.zig | 9 ++++++++- ...ode_build_config.zig => uucode_config.zig} | 4 ++-- src/build/uucode_x.zig | 8 -------- src/font/CodepointResolver.zig | 2 +- src/renderer/cell.zig | 4 ++-- src/unicode/props.zig | 5 +++-- 8 files changed, 29 insertions(+), 31 deletions(-) rename src/build/{uucode_build_config.zig => uucode_config.zig} (78%) delete mode 100644 src/build/uucode_x.zig diff --git a/build.zig.zon b/build.zig.zon index 86ae38786..4027e0252 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,12 +42,12 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/7d851864a21d1d700d41230762de66f7b7fa5941.tar.gz", - .hash = "uucode-0.0.0-ZZjBPqk4OwDcGSplCRYO-zLKcs3FZ-jQGj9rfSEf6VZr", + .url = "https://github.com/jacobsandlund/uucode/archive/c5300b75cf2a1f11fe00f492875a8db9f8d346f0.tar.gz", + .hash = "uucode-0.0.0-ZZjBPsGrOwBSzsufKmTo-bwSYyjogdt7HW0sBPkE24uc", }, .uucode_x = .{ - .url = "https://github.com/jacobsandlund/uucode.x/archive/779492c4565ed282a7c989ffc7f52cbe060f17a3.tar.gz", - .hash = "uucode_x-0.0.0-5_D0j-gIAAC4SzTTunb6O4sqwj0kojbjV0rm2bj9QgyA", + .url = "https://github.com/jacobsandlund/uucode.x/archive/db7678dd8af009971021f8479fb2ac07db00e698.tar.gz", + .hash = "uucode_x-0.0.0-5_D0jxkhAACi1yTMBn7Rat1telHy82oD65B7ywtj4W4j", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index b52482385..c77a8ba0b 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -21,22 +21,21 @@ uucode_tables_zig: std.Build.LazyPath, pub const LazyPathList = std.ArrayList(std.Build.LazyPath); pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { - const uucode_x = b.dependency("uucode_x", .{}); - const uucode_x_config = uucode_x.module("uucode.x.config"); + const uucode_tables_zig = blk: { + const uucode = b.dependency("uucode", .{ + .build_config_path = b.path("src/build/uucode_config.zig"), + }); + @import("uucode_x").connectBuild(b.dependency("uucode_x", .{}), uucode); - const uucode = b.dependency("uucode", .{ - .build_config_path = b.path("src/build/uucode_build_config.zig"), - }); - uucode.module("build_config").addImport("uucode.x.config", uucode_x_config); - uucode_x_config.addImport("config.zig", uucode.module("config.zig")); - uucode_x_config.addImport("types.zig", uucode.module("types.zig")); + break :blk uucode.namedLazyPath("tables.zig"); + }; var result: SharedDeps = .{ .config = cfg, .help_strings = try .init(b, cfg), - .unicode_tables = try .init(b), + .unicode_tables = try .init(b, uucode_tables_zig), .framedata = try .init(b), - .uucode_tables_zig = uucode.namedLazyPath("tables.zig"), + .uucode_tables_zig = uucode_tables_zig, // Setup by retarget .options = undefined, @@ -431,7 +430,6 @@ pub fn add( .target = target, .optimize = optimize, .@"tables.zig" = self.uucode_tables_zig, - .x_root_path = b.path("src/build/uucode_x.zig"), })) |dep| { step.root_module.addImport("uucode", dep.module("uucode")); } diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index 5bba2341b..bb625c3b8 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -9,7 +9,7 @@ exe: *std.Build.Step.Compile, /// The output path for the unicode tables output: std.Build.LazyPath, -pub fn init(b: *std.Build) !UnicodeTables { +pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables { const exe = b.addExecutable(.{ .name = "unigen", .root_module = b.createModule(.{ @@ -30,6 +30,13 @@ pub fn init(b: *std.Build) !UnicodeTables { ); } + if (b.lazyDependency("uucode", .{ + .target = b.graph.host, + .@"tables.zig" = uucode_tables_zig, + })) |dep| { + exe.root_module.addImport("uucode", dep.module("uucode")); + } + const run = b.addRunArtifact(exe); return .{ .exe = exe, diff --git a/src/build/uucode_build_config.zig b/src/build/uucode_config.zig similarity index 78% rename from src/build/uucode_build_config.zig rename to src/build/uucode_config.zig index 4c87dfdc4..bbc833158 100644 --- a/src/build/uucode_build_config.zig +++ b/src/build/uucode_config.zig @@ -4,9 +4,9 @@ const d = config.default; pub const tables = [_]config.Table{ .{ - .extensions = &.{x.width}, + .extensions = &.{x.wcwidth}, .fields = &.{ - x.width.field("width"), + x.wcwidth.field("wcwidth"), d.field("general_category"), d.field("has_emoji_presentation"), }, diff --git a/src/build/uucode_x.zig b/src/build/uucode_x.zig deleted file mode 100644 index ce305e3cc..000000000 --- a/src/build/uucode_x.zig +++ /dev/null @@ -1,8 +0,0 @@ -const get = @import("get.zig"); -const tableFor = get.tableFor; -const data = get.data; - -pub fn width(cp: u21) u2 { - const table = comptime tableFor("width"); - return data(table, cp).width; -} diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index ec318abe5..31a688150 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -150,7 +150,7 @@ pub fn getIndex( // we'll do this multiple times if we recurse, but this is a cached function // call higher up (GroupCache) so this should be rare. const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (uucode.hasEmojiPresentation(@intCast(cp))) + .default = if (uucode.get("has_emoji_presentation", @intCast(cp))) .emoji else .text, diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index d72e5965c..708f3b743 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -237,7 +237,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const cp = cell.codepoint(); // If not a Co (Private Use) and not a Dingbats, use grid width. - if (uucode.generalCategory(cp) != .Co and + if (uucode.get("general_category", cp) != .Co and !ziglyph.blocks.isDingbats(cp)) { return cell.gridWidth(); @@ -262,7 +262,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { if (isPowerline(prev_cp)) break :prev; // If it's Private Use (Co) use 1 as the width. - if (uucode.generalCategory(prev_cp) == .Co) { + if (uucode.get("general_category", prev_cp) == .Co) { return 1; } } diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 99c57aa0a..f611b9311 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -2,6 +2,7 @@ const props = @This(); const std = @import("std"); const assert = std.debug.assert; const ziglyph = @import("ziglyph"); +const uucode = @import("uucode"); const lut = @import("lut.zig"); /// The lookup tables for Ghostty. @@ -121,10 +122,10 @@ pub const GraphemeBoundaryClass = enum(u4) { }; pub fn get(cp: u21) Properties { - const zg_width = ziglyph.display_width.codePointWidth(cp, .half); + const wcwidth = if (cp < 0x110000) uucode.get("wcwidth", cp) else 0; return .{ - .width = @intCast(@min(2, @max(0, zg_width))), + .width = @intCast(@min(2, @max(0, wcwidth))), .grapheme_boundary_class = .init(cp), }; } From 7e429d73d6af65a397c6264b18ab60609ae8eefe Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 6 Aug 2025 00:06:27 -0400 Subject: [PATCH 007/319] block --- src/build/SharedDeps.zig | 3 ++- src/build/uucode_config.zig | 1 + src/renderer/cell.zig | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index c77a8ba0b..66563d173 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -25,7 +25,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { const uucode = b.dependency("uucode", .{ .build_config_path = b.path("src/build/uucode_config.zig"), }); - @import("uucode_x").connectBuild(b.dependency("uucode_x", .{}), uucode); + const uucode_x = b.dependency("uucode_x", .{}); + @import("uucode_x").connectBuild(uucode_x, uucode); break :blk uucode.namedLazyPath("tables.zig"); }; diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index bbc833158..1c102fbd0 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -9,6 +9,7 @@ pub const tables = [_]config.Table{ x.wcwidth.field("wcwidth"), d.field("general_category"), d.field("has_emoji_presentation"), + d.field("block"), }, }, }; diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 708f3b743..7de61e9b3 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -1,7 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const ziglyph = @import("ziglyph"); const uucode = @import("uucode"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); @@ -238,7 +237,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // If not a Co (Private Use) and not a Dingbats, use grid width. if (uucode.get("general_category", cp) != .Co and - !ziglyph.blocks.isDingbats(cp)) + uucode.get("block", cp) != .dingbats) { return cell.gridWidth(); } From f5a036a6a04e60ddecc3404476a5cfe84bd5d3d9 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 12 Aug 2025 09:43:12 -0400 Subject: [PATCH 008/319] update after refactor (string field config, etc) --- build.zig.zon | 8 ++++---- src/build/uucode_config.zig | 7 ++++--- src/font/CodepointResolver.zig | 2 +- src/input/Binding.zig | 3 ++- src/unicode/props.zig | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 8095fbe1f..de0faac18 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,12 +42,12 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/c5300b75cf2a1f11fe00f492875a8db9f8d346f0.tar.gz", - .hash = "uucode-0.0.0-ZZjBPsGrOwBSzsufKmTo-bwSYyjogdt7HW0sBPkE24uc", + .url = "https://github.com/jacobsandlund/uucode/archive/96dd05cb9df893293fffe35321eb0aeb36379b48.tar.gz", + .hash = "uucode-0.0.0-ZZjBPuZ1PAB3TAPIR1yzwyGZbAZ4AqYEtfQ81lsdBL67", }, .uucode_x = .{ - .url = "https://github.com/jacobsandlund/uucode.x/archive/db7678dd8af009971021f8479fb2ac07db00e698.tar.gz", - .hash = "uucode_x-0.0.0-5_D0jxkhAACi1yTMBn7Rat1telHy82oD65B7ywtj4W4j", + .url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", + .hash = "uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 1c102fbd0..943928c3f 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -1,12 +1,13 @@ const config = @import("config.zig"); -const x = @import("uucode.x.config"); +const config_x = @import("config.x.zig"); const d = config.default; +const wcwidth = config_x.wcwidth; pub const tables = [_]config.Table{ .{ - .extensions = &.{x.wcwidth}, + .extensions = &.{wcwidth}, .fields = &.{ - x.wcwidth.field("wcwidth"), + wcwidth.field("wcwidth"), d.field("general_category"), d.field("has_emoji_presentation"), d.field("block"), diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 31a688150..4c671b769 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -150,7 +150,7 @@ pub fn getIndex( // we'll do this multiple times if we recurse, but this is a cached function // call higher up (GroupCache) so this should be rare. const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (uucode.get("has_emoji_presentation", @intCast(cp))) + .default = if (uucode.get(.has_emoji_presentation, @intCast(cp))) .emoji else .text, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b20319810..3d2d71fe7 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -7,6 +7,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const build_config = @import("../build_config.zig"); const ziglyph = @import("ziglyph"); +const uucode = @import("uucode"); const key = @import("key.zig"); const KeyEvent = key.KeyEvent; @@ -1574,7 +1575,7 @@ pub const Trigger = struct { /// in more codepoints so we need to use a 3 element array. fn foldedCodepoint(cp: u21) [3]u21 { // ASCII fast path - if (ziglyph.letter.isAsciiLetter(cp)) { + if (uucode.ascii.isAlphabetic(cp)) { return .{ ziglyph.letter.toLower(cp), 0, 0 }; } diff --git a/src/unicode/props.zig b/src/unicode/props.zig index f611b9311..b7bf6e3c1 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -122,7 +122,7 @@ pub const GraphemeBoundaryClass = enum(u4) { }; pub fn get(cp: u21) Properties { - const wcwidth = if (cp < 0x110000) uucode.get("wcwidth", cp) else 0; + const wcwidth = if (cp < 0x110000) uucode.get(.wcwidth, cp) else 0; return .{ .width = @intCast(@min(2, @max(0, wcwidth))), From 341137ce2eca23e3ba082b5f6ee9350f342aea4f Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 13 Aug 2025 09:56:05 -0400 Subject: [PATCH 009/319] case folding --- build.zig.zon | 4 ++-- src/build/uucode_config.zig | 5 ++++- src/input/Binding.zig | 11 +++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index de0faac18..4110effd4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/96dd05cb9df893293fffe35321eb0aeb36379b48.tar.gz", - .hash = "uucode-0.0.0-ZZjBPuZ1PAB3TAPIR1yzwyGZbAZ4AqYEtfQ81lsdBL67", + .url = "https://github.com/jacobsandlund/uucode/archive/c7421d97dbdaa8aa176a87eb6dd7dcc3baab12d1.tar.gz", + .hash = "uucode-0.0.0-ZZjBPkh9PAAR34asa24LSw5i5TNKE1B3hohDkZL5iT8J", }, .uucode_x = .{ .url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 943928c3f..bc6ff7eb7 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -9,8 +9,11 @@ pub const tables = [_]config.Table{ .fields = &.{ wcwidth.field("wcwidth"), d.field("general_category"), - d.field("has_emoji_presentation"), d.field("block"), + d.field("has_emoji_presentation"), + d.field("case_folding_full"), + // Alternative: + // d.field("case_folding_simple"), }, }, }; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 3d2d71fe7..6c7870f06 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -6,7 +6,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const build_config = @import("../build_config.zig"); -const ziglyph = @import("ziglyph"); const uucode = @import("uucode"); const key = @import("key.zig"); const KeyEvent = key.KeyEvent; @@ -1566,6 +1565,8 @@ pub const Trigger = struct { .unicode => |cp| std.hash.autoHash( hasher, foldedCodepoint(cp), + // Alternative, just use simple case folding: + // uucode.get(.case_folding_simple, cp), ), } std.hash.autoHash(hasher, self.mods.binding()); @@ -1576,14 +1577,16 @@ pub const Trigger = struct { fn foldedCodepoint(cp: u21) [3]u21 { // ASCII fast path if (uucode.ascii.isAlphabetic(cp)) { - return .{ ziglyph.letter.toLower(cp), 0, 0 }; + return .{ uucode.ascii.toLower(cp), 0, 0 }; } - // Unicode slow path. Case folding can resultin more codepoints. + // Unicode slow path. Case folding can result in more codepoints. // If more codepoints are produced then we return the codepoint // as-is which isn't correct but until we have a failing test // then I don't want to handle this. - return ziglyph.letter.toCaseFold(cp); + var folded: [3]u21 = .{ 0, 0, 0 }; + _ = uucode.get(.case_folding_full, cp).copy(&folded, cp); + return folded; } /// Convert the trigger to a C API compatible trigger. From 1abc9b5e41ed58a2a74008f0f87031a16b2a10ca Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 17 Aug 2025 19:05:40 -0400 Subject: [PATCH 010/319] `array` --- build.zig.zon | 4 ++-- src/input/Binding.zig | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 9698f81c7..f2f8362c3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/c7421d97dbdaa8aa176a87eb6dd7dcc3baab12d1.tar.gz", - .hash = "uucode-0.0.0-ZZjBPkh9PAAR34asa24LSw5i5TNKE1B3hohDkZL5iT8J", + .url = "https://github.com/jacobsandlund/uucode/archive/a50e106b57f406ada41d380ec59b6b33cdb77667.tar.gz", + .hash = "uucode-0.0.0-ZZjBPoF_PADS8lyIfgw-C-j5lM-CznP5808p9OMSxytN", }, .uucode_x = .{ .url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 5b1cefca0..78760a9a7 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1574,7 +1574,8 @@ pub const Trigger = struct { .unicode => |cp| std.hash.autoHash( hasher, foldedCodepoint(cp), - // Alternative, just use simple case folding: + // Alternative, just use simple case folding, and delete + // `foldedCodepoint` below: // uucode.get(.case_folding_simple, cp), ), } @@ -1593,9 +1594,7 @@ pub const Trigger = struct { // If more codepoints are produced then we return the codepoint // as-is which isn't correct but until we have a failing test // then I don't want to handle this. - var folded: [3]u21 = .{ 0, 0, 0 }; - _ = uucode.get(.case_folding_full, cp).copy(&folded, cp); - return folded; + return uucode.get(.case_folding_full, cp).array(cp); } /// Convert the trigger to a C API compatible trigger. From e84d8535f5504d7a47bcba8792f019f6f421336f Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 17 Aug 2025 21:24:27 -0400 Subject: [PATCH 011/319] removing all ziglyph imports (aside from unicode/grapheme.zig) --- build.zig.zon | 4 +- src/build/SharedDeps.zig | 6 --- src/build/UnicodeTables.zig | 9 ----- src/build/uucode_config.zig | 1 + src/simd/codepoint_width.zig | 16 ++++---- src/terminal/Terminal.zig | 4 +- src/unicode/grapheme.zig | 6 +++ src/unicode/props.zig | 77 ++++++++++++++++++------------------ 8 files changed, 57 insertions(+), 66 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index f2f8362c3..3d4e59ef3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/a50e106b57f406ada41d380ec59b6b33cdb77667.tar.gz", - .hash = "uucode-0.0.0-ZZjBPoF_PADS8lyIfgw-C-j5lM-CznP5808p9OMSxytN", + .url = "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", + .hash = "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i", }, .uucode_x = .{ .url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 66563d173..0c2cc96d0 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -421,12 +421,6 @@ pub fn add( })) |dep| { step.root_module.addImport("z2d", dep.module("z2d")); } - if (b.lazyDependency("ziglyph", .{ - .target = target, - .optimize = optimize, - })) |dep| { - step.root_module.addImport("ziglyph", dep.module("ziglyph")); - } if (b.lazyDependency("uucode", .{ .target = target, .optimize = optimize, diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index bb625c3b8..78bcef2c9 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -21,15 +21,6 @@ pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables }), }); - if (b.lazyDependency("ziglyph", .{ - .target = b.graph.host, - })) |ziglyph_dep| { - exe.root_module.addImport( - "ziglyph", - ziglyph_dep.module("ziglyph"), - ); - } - if (b.lazyDependency("uucode", .{ .target = b.graph.host, .@"tables.zig" = uucode_tables_zig, diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index bc6ff7eb7..de1b71717 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -14,6 +14,7 @@ pub const tables = [_]config.Table{ d.field("case_folding_full"), // Alternative: // d.field("case_folding_simple"), + d.field("grapheme_break"), }, }, }; diff --git a/src/simd/codepoint_width.zig b/src/simd/codepoint_width.zig index aab4bdd95..e2383aff1 100644 --- a/src/simd/codepoint_width.zig +++ b/src/simd/codepoint_width.zig @@ -4,7 +4,7 @@ const std = @import("std"); extern "c" fn ghostty_simd_codepoint_width(u32) i8; pub fn codepointWidth(cp: u32) i8 { - //return @import("ziglyph").display_width.codePointWidth(@intCast(cp), .half); + //return @import("uucode").get(.wcwidth, @intCast(cp)); return ghostty_simd_codepoint_width(cp); } @@ -19,26 +19,26 @@ test "codepointWidth basic" { try testing.expectEqual(@as(i8, 2), codepointWidth(0xF900)); // 豈 try testing.expectEqual(@as(i8, 2), codepointWidth(0x20000)); // 𠀀 try testing.expectEqual(@as(i8, 2), codepointWidth(0x30000)); // 𠀀 - // try testing.expectEqual(@as(i8, 1), @import("ziglyph").display_width.codePointWidth(0x100, .half)); + // try testing.expectEqual(@as(i8, 1), @import("uucode").get(.wcwidth, 0x100)); } // This is not very fast in debug modes, so its commented by default. // IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES. -// test "codepointWidth matches ziglyph" { +// test "codepointWidth matches uucode" { // const testing = std.testing; -// const ziglyph = @import("ziglyph"); +// const uucode = @import("uucode"); // // const min = 0xFF + 1; // start outside ascii -// for (min..std.math.maxInt(u21)) |cp| { +// for (min..uucode.code_point_range_end) |cp| { // const simd = codepointWidth(@intCast(cp)); -// const zg = ziglyph.display_width.codePointWidth(@intCast(cp), .half); -// if (simd != zg) mismatch: { +// const uu = @min(2, @max(0, uucode.get(.wcwidth, @intCast(cp)))); +// if (simd != uu) mismatch: { // if (cp == 0x2E3B) { // try testing.expectEqual(@as(i8, 2), simd); // break :mismatch; // } // -// std.log.warn("mismatch cp=U+{x} simd={} zg={}", .{ cp, simd, zg }); +// std.log.warn("mismatch cp=U+{x} simd={} uucode={}", .{ cp, simd, uu }); // try testing.expect(false); // } // } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index dd7207f6d..d08c31b34 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -415,8 +415,8 @@ pub fn print(self: *Terminal, c: u21) !void { // control characters because they're always filtered prior. const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width); - // Note: it is possible to have a width of "3" and a width of "-1" - // from ziglyph. We should look into those cases and handle them + // Note: it is possible to have a width of "3" and a width of "-1" from + // uucode.x's wcwidth. We should look into those cases and handle them // appropriately. assert(width <= 2); // log.debug("c={x} width={}", .{ c, width }); diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index 7847ef6f5..0950bedba 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -152,6 +152,12 @@ fn graphemeBreakClass( /// If you build this file as a binary, we will verify the grapheme break /// implementation. This iterates over billions of codepoints so it is /// SLOW. It's not meant to be run in CI, but it's useful for debugging. +/// TODO: this is hard to build with newer zig build, so +/// https://github.com/ghostty-org/ghostty/pull/7806 took the approach of +/// adding a `-Demit-unicode-test` option for `zig build`, but that +/// hasn't been done here yet. +/// TODO: this also still uses `ziglyph`, but could be switched to use +/// `uucode`'s grapheme break once that is implemented. pub fn main() !void { const ziglyph = @import("ziglyph"); diff --git a/src/unicode/props.zig b/src/unicode/props.zig index b7bf6e3c1..879ada7a8 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -1,7 +1,6 @@ const props = @This(); const std = @import("std"); const assert = std.debug.assert; -const ziglyph = @import("ziglyph"); const uucode = @import("uucode"); const lut = @import("lut.zig"); @@ -78,33 +77,33 @@ pub const GraphemeBoundaryClass = enum(u4) { extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base} emoji_modifier, // \p{Emoji_Modifier} - /// Gets the grapheme boundary class for a codepoint. This is VERY - /// SLOW. The use case for this is only in generating lookup tables. + /// Gets the grapheme boundary class for a codepoint. + /// The use case for this is only in generating lookup tables. pub fn init(cp: u21) GraphemeBoundaryClass { - // We special-case modifier bases because we should not break - // if a modifier isn't next to a base. - if (ziglyph.emoji.isEmojiModifierBase(cp)) { - assert(ziglyph.emoji.isExtendedPictographic(cp)); - return .extended_pictographic_base; + if (cp < uucode.code_point_range_end) { + return switch (uucode.get(.grapheme_break, cp)) { + .emoji_modifier_base => .extended_pictographic_base, + .emoji_modifier => .emoji_modifier, + .extended_pictographic => .extended_pictographic, + .l => .L, + .v => .V, + .t => .T, + .lv => .LV, + .lvt => .LVT, + .prepend => .prepend, + .extend => .extend, + .zwj => .zwj, + .spacing_mark => .spacing_mark, + .regional_indicator => .regional_indicator, + + // This is obviously not INVALID invalid, there is SOME grapheme + // boundary class for every codepoint. But we don't care about + // anything that doesn't fit into the above categories. + .other, .cr, .lf, .control => .invalid, + }; + } else { + return .invalid; } - - if (ziglyph.emoji.isEmojiModifier(cp)) return .emoji_modifier; - if (ziglyph.emoji.isExtendedPictographic(cp)) return .extended_pictographic; - if (ziglyph.grapheme_break.isL(cp)) return .L; - if (ziglyph.grapheme_break.isV(cp)) return .V; - if (ziglyph.grapheme_break.isT(cp)) return .T; - if (ziglyph.grapheme_break.isLv(cp)) return .LV; - if (ziglyph.grapheme_break.isLvt(cp)) return .LVT; - if (ziglyph.grapheme_break.isPrepend(cp)) return .prepend; - if (ziglyph.grapheme_break.isExtend(cp)) return .extend; - if (ziglyph.grapheme_break.isZwj(cp)) return .zwj; - if (ziglyph.grapheme_break.isSpacingmark(cp)) return .spacing_mark; - if (ziglyph.grapheme_break.isRegionalIndicator(cp)) return .regional_indicator; - - // This is obviously not INVALID invalid, there is SOME grapheme - // boundary class for every codepoint. But we don't care about - // anything that doesn't fit into the above categories. - return .invalid; } /// Returns true if this is an extended pictographic type. This @@ -122,7 +121,7 @@ pub const GraphemeBoundaryClass = enum(u4) { }; pub fn get(cp: u21) Properties { - const wcwidth = if (cp < 0x110000) uucode.get(.wcwidth, cp) else 0; + const wcwidth = if (cp < uucode.code_point_range_end) uucode.get(.wcwidth, cp) else 0; return .{ .width = @intCast(@min(2, @max(0, wcwidth))), @@ -167,16 +166,16 @@ pub fn main() !void { // This is not very fast in debug modes, so its commented by default. // IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES. -// test "tables match ziglyph" { -// const testing = std.testing; +//test "tables match uucode" { +// const testing = std.testing; // -// const min = 0xFF + 1; // start outside ascii -// for (min..std.math.maxInt(u21)) |cp| { -// const t = table.get(@intCast(cp)); -// const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half))); -// if (t.width != zg) { -// std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); -// try testing.expect(false); -// } -// } -// } +// const min = 0xFF + 1; // start outside ascii +// for (min..uucode.code_point_range_end) |cp| { +// const t = table.get(@intCast(cp)); +// const uu = @min(2, @max(0, uucode.get(.wcwidth, @intCast(cp)))); +// if (t.width != uu) { +// std.log.warn("mismatch cp=U+{x} t={} uucode={}", .{ cp, t, uu }); +// try testing.expect(false); +// } +// } +//} From 0b7ab006e9b046e89016832e65a3b77866756235 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 17 Aug 2025 21:27:59 -0400 Subject: [PATCH 012/319] nix and flatpak updates --- build.zig.zon | 5 ----- build.zig.zon.json | 15 ++++++++++----- build.zig.zon.nix | 24 ++++++++++++++++-------- build.zig.zon.txt | 3 ++- flatpak/zig-packages.json | 18 ++++++++++++------ 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 3d4e59ef3..34532bba1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -36,11 +36,6 @@ .hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", .lazy = true, }, - .ziglyph = .{ - .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - .hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", - .lazy = true, - }, .uucode = .{ .url = "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", .hash = "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i", diff --git a/build.zig.zon.json b/build.zig.zon.json index 2ed4f63ce..1e31b72a2 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -109,6 +109,16 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, + "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i": { + "name": "uucode", + "url": "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", + "hash": "sha256-1q5n3eqopVi1qrsg3XOth3ZkVo2ah2WgcyHkZrV7260=" + }, + "uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ": { + "name": "uucode_x", + "url": "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", + "hash": "sha256-nr3tujSgGr5qE++ctjXGyS+9PrVdItovVHHgbo9ntWM=" + }, "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { "name": "vaxis", "url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", @@ -164,11 +174,6 @@ "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d", "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=" }, - "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": { - "name": "ziglyph", - "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - "hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=" - }, "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": { "name": "zlib", "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index b1b19be37..0cb79329b 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -257,6 +257,22 @@ in hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="; }; } + { + name = "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i"; + path = fetchZigArtifact { + name = "uucode"; + url = "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz"; + hash = "sha256-1q5n3eqopVi1qrsg3XOth3ZkVo2ah2WgcyHkZrV7260="; + }; + } + { + name = "uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ"; + path = fetchZigArtifact { + name = "uucode_x"; + url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz"; + hash = "sha256-nr3tujSgGr5qE++ctjXGyS+9PrVdItovVHHgbo9ntWM="; + }; + } { name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; path = fetchZigArtifact { @@ -345,14 +361,6 @@ in hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; }; } - { - name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; - path = fetchZigArtifact { - name = "ziglyph"; - url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; - hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; - }; - } { name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; path = fetchZigArtifact { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 849c679fc..e2c578ea5 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -25,8 +25,9 @@ https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d. https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz -https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz +https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz +https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index a67ccef59..cdf566fda 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -131,6 +131,18 @@ "dest": "vendor/p/N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH", "sha256": "ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf" }, + { + "type": "archive", + "url": "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", + "dest": "vendor/p/uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i", + "sha256": "d6ae67ddeaa8a558b5aabb20dd73ad877664568d9a8765a07321e466b57bdbad" + }, + { + "type": "archive", + "url": "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", + "dest": "vendor/p/uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ", + "sha256": "9ebdedba34a01abe6a13ef9cb635c6c92fbd3eb55d22da2f5471e06e8f67b563" + }, { "type": "git", "url": "https://github.com/rockorager/libvaxis", @@ -197,12 +209,6 @@ "commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d", "dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj" }, - { - "type": "archive", - "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - "dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", - "sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9" - }, { "type": "archive", "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", From 8f0785e90aaf4ab71b184307e986a3c62facb74a Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 17 Aug 2025 21:33:37 -0400 Subject: [PATCH 013/319] is_emoji_presentation --- build.zig.zon | 4 ++-- src/build/uucode_config.zig | 2 +- src/font/CodepointResolver.zig | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 34532bba1..60b96ee18 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,8 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", - .hash = "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i", + .url = "https://github.com/jacobsandlund/uucode/archive/aaa2aef70dd37e7c3975bb973fcc36bf93faab9f.tar.gz", + .hash = "uucode-0.0.0-ZZjBPi6BPAC5vZ7yoeYp_5uMNSVx_JsgzY-r54DEgt3a", }, .uucode_x = .{ .url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index de1b71717..42c755d8c 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -10,7 +10,7 @@ pub const tables = [_]config.Table{ wcwidth.field("wcwidth"), d.field("general_category"), d.field("block"), - d.field("has_emoji_presentation"), + d.field("is_emoji_presentation"), d.field("case_folding_full"), // Alternative: // d.field("case_folding_simple"), diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 4c671b769..a4f13c290 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -150,7 +150,7 @@ pub fn getIndex( // we'll do this multiple times if we recurse, but this is a cached function // call higher up (GroupCache) so this should be rare. const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (uucode.get(.has_emoji_presentation, @intCast(cp))) + .default = if (uucode.get(.is_emoji_presentation, @intCast(cp))) .emoji else .text, From 0444c614da05ff6d5609bb973e62d90e9d1113d2 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 21 Aug 2025 22:29:34 -0400 Subject: [PATCH 014/319] update for new grapheme_break --- build.zig.zon | 8 ++++---- src/build/uucode_config.zig | 2 ++ src/unicode/props.zig | 18 ++++++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 2a44c0220..db0b63596 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,12 +37,12 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/aaa2aef70dd37e7c3975bb973fcc36bf93faab9f.tar.gz", - .hash = "uucode-0.0.0-ZZjBPi6BPAC5vZ7yoeYp_5uMNSVx_JsgzY-r54DEgt3a", + .url = "https://github.com/jacobsandlund/uucode/archive/907218a2c8097688554e54bb0999e6dbd59b226e.tar.gz", + .hash = "uucode-0.0.0-ZZjBPopfPwDqH70dn65Zklni_Yo8KWdLcVMEvmPoj1vW", }, .uucode_x = .{ - .url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", - .hash = "uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ", + .url = "https://github.com/jacobsandlund/uucode.x/archive/9f5cfb1b48ab923677e837e22aa33c2a4380fc47.tar.gz", + .hash = "uucode_x-0.0.0-5_D0j2YhAAC8KvciYTFrV3hKANPbXke5havA5OIEf7XT", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 42c755d8c..69d0d2fd3 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -15,6 +15,8 @@ pub const tables = [_]config.Table{ // Alternative: // d.field("case_folding_simple"), d.field("grapheme_break"), + d.field("is_emoji_modifier"), + d.field("is_emoji_modifier_base"), }, }, }; diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 879ada7a8..c06329876 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -81,9 +81,10 @@ pub const GraphemeBoundaryClass = enum(u4) { /// The use case for this is only in generating lookup tables. pub fn init(cp: u21) GraphemeBoundaryClass { if (cp < uucode.code_point_range_end) { + if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; + if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; + return switch (uucode.get(.grapheme_break, cp)) { - .emoji_modifier_base => .extended_pictographic_base, - .emoji_modifier => .emoji_modifier, .extended_pictographic => .extended_pictographic, .l => .L, .v => .V, @@ -91,15 +92,24 @@ pub const GraphemeBoundaryClass = enum(u4) { .lv => .LV, .lvt => .LVT, .prepend => .prepend, - .extend => .extend, .zwj => .zwj, .spacing_mark => .spacing_mark, .regional_indicator => .regional_indicator, + .zwnj, + .indic_conjunct_break_extend, + .indic_conjunct_break_linker, + => .extend, + // This is obviously not INVALID invalid, there is SOME grapheme // boundary class for every codepoint. But we don't care about // anything that doesn't fit into the above categories. - .other, .cr, .lf, .control => .invalid, + .other, + .indic_conjunct_break_consonant, + .cr, + .lf, + .control, + => .invalid, }; } else { return .invalid; From 3c61aaca2aed11e909b1edb075638253037350e3 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 23 Aug 2025 09:16:01 -0400 Subject: [PATCH 015/319] attempting to use uucode from uucode.x --- build.zig.zon | 8 ++++---- src/build/SharedDeps.zig | 8 ++++++++ src/font/shaper/web_canvas.zig | 6 +++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index db0b63596..b4dc58efb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,12 +37,12 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/907218a2c8097688554e54bb0999e6dbd59b226e.tar.gz", - .hash = "uucode-0.0.0-ZZjBPopfPwDqH70dn65Zklni_Yo8KWdLcVMEvmPoj1vW", + .url = "https://github.com/jacobsandlund/uucode/archive/7e2bde7c3cd90a5b754cd62b7d0059f70f5eea3f.tar.gz", + .hash = "uucode-0.0.0-ZZjBPn1iPwCsTwd680sxrXcqNZxA8r_EmnpMiP53h5a5", }, .uucode_x = .{ - .url = "https://github.com/jacobsandlund/uucode.x/archive/9f5cfb1b48ab923677e837e22aa33c2a4380fc47.tar.gz", - .hash = "uucode_x-0.0.0-5_D0j2YhAAC8KvciYTFrV3hKANPbXke5havA5OIEf7XT", + .url = "https://github.com/jacobsandlund/uucode.x/archive/5964041fa51234a5d86dedda88f2af6bb9adf361.tar.gz", + .hash = "uucode_x-0.0.0-5_D0j200AAB5nu0QR5oQiceKujDN8C1WqMNlZiaxqNu0", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0c2cc96d0..0170910c5 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -428,6 +428,14 @@ pub fn add( })) |dep| { step.root_module.addImport("uucode", dep.module("uucode")); } + if (b.lazyDependency("uucode_x", .{ + .target = target, + .optimize = optimize, + })) |dep| { + const mod = dep.module("uucode.x"); + step.root_module.addImport("uucode.x", mod); + mod.addImport("root_module", step.root_module); + } if (b.lazyDependency("zf", .{ .target = target, .optimize = optimize, diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index e0f0e1a00..6cf53073a 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -3,7 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); -const unicode = @import("../../unicode/main.zig"); +const uucode_x = @import("uucode.x"); const log = std.log.scoped(.font_shaper); @@ -111,7 +111,7 @@ pub const Shaper = struct { // font ligatures. However, we do support grapheme clustering. // This means we can render things like skin tone emoji but // we can't render things like single glyph "=>". - var break_state: unicode.GraphemeBreakState = .{}; + var break_state: uucode_x.GraphemeBreakState = .default; var cp1: u21 = @intCast(codepoints[0]); var start: usize = 0; @@ -126,7 +126,7 @@ pub const Shaper = struct { const cp2: u21 = @intCast(codepoints[i]); defer cp1 = cp2; - break :blk unicode.graphemeBreak( + break :blk uucode_x.graphemeBreakXEmoji( cp1, cp2, &break_state, From c7fa1d8381f74c262517389af7c712341055f5e3 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 23 Aug 2025 14:33:11 -0400 Subject: [PATCH 016/319] using uucode for the graphemeBreak in shaper/web_canvas.zig --- build.zig.zon | 8 ++------ src/build/SharedDeps.zig | 11 +---------- src/font/shaper/web_canvas.zig | 6 +++--- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b164175c8..02f74ec0a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,12 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/7e2bde7c3cd90a5b754cd62b7d0059f70f5eea3f.tar.gz", - .hash = "uucode-0.0.0-ZZjBPn1iPwCsTwd680sxrXcqNZxA8r_EmnpMiP53h5a5", - }, - .uucode_x = .{ - .url = "https://github.com/jacobsandlund/uucode.x/archive/5964041fa51234a5d86dedda88f2af6bb9adf361.tar.gz", - .hash = "uucode_x-0.0.0-5_D0j200AAB5nu0QR5oQiceKujDN8C1WqMNlZiaxqNu0", + .url = "https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz", + .hash = "uucode-0.0.0-ZZjBPiqdPwB-rG3ieaq3c6tMpnksWYs4_rGj2IvFGjjB", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0170910c5..f338f8b98 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -25,8 +25,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { const uucode = b.dependency("uucode", .{ .build_config_path = b.path("src/build/uucode_config.zig"), }); - const uucode_x = b.dependency("uucode_x", .{}); - @import("uucode_x").connectBuild(uucode_x, uucode); break :blk uucode.namedLazyPath("tables.zig"); }; @@ -425,17 +423,10 @@ pub fn add( .target = target, .optimize = optimize, .@"tables.zig" = self.uucode_tables_zig, + .build_config_path = b.path("src/build/uucode_config.zig"), })) |dep| { step.root_module.addImport("uucode", dep.module("uucode")); } - if (b.lazyDependency("uucode_x", .{ - .target = target, - .optimize = optimize, - })) |dep| { - const mod = dep.module("uucode.x"); - step.root_module.addImport("uucode.x", mod); - mod.addImport("root_module", step.root_module); - } if (b.lazyDependency("zf", .{ .target = target, .optimize = optimize, diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index 6cf53073a..c41262238 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -3,7 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); -const uucode_x = @import("uucode.x"); +const uucode = @import("uucode"); const log = std.log.scoped(.font_shaper); @@ -111,7 +111,7 @@ pub const Shaper = struct { // font ligatures. However, we do support grapheme clustering. // This means we can render things like skin tone emoji but // we can't render things like single glyph "=>". - var break_state: uucode_x.GraphemeBreakState = .default; + var break_state: uucode.GraphemeBreakState = .default; var cp1: u21 = @intCast(codepoints[0]); var start: usize = 0; @@ -126,7 +126,7 @@ pub const Shaper = struct { const cp2: u21 = @intCast(codepoints[i]); defer cp1 = cp2; - break :blk uucode_x.graphemeBreakXEmoji( + break :blk uucode.graphemeBreak( cp1, cp2, &break_state, From 6d02da0317361851adfd8f61b0b67f61b2c34472 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 30 Aug 2025 18:20:06 -0400 Subject: [PATCH 017/319] nix and flatpak --- build.zig.zon.json | 11 +++-------- build.zig.zon.nix | 14 +++----------- build.zig.zon.txt | 3 +-- flatpak/zig-packages.json | 12 +++--------- 4 files changed, 10 insertions(+), 30 deletions(-) diff --git a/build.zig.zon.json b/build.zig.zon.json index e1773f575..396ca85d0 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -109,15 +109,10 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i": { + "uucode-0.0.0-ZZjBPiqdPwB-rG3ieaq3c6tMpnksWYs4_rGj2IvFGjjB": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", - "hash": "sha256-1q5n3eqopVi1qrsg3XOth3ZkVo2ah2WgcyHkZrV7260=" - }, - "uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ": { - "name": "uucode_x", - "url": "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", - "hash": "sha256-nr3tujSgGr5qE++ctjXGyS+9PrVdItovVHHgbo9ntWM=" + "url": "https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz", + "hash": "sha256-MkWxYZHONRxGyUvGI5cAKi/ckGiXIBxkktCz5r+zWrk=" }, "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { "name": "vaxis", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 2d5fd8355..0b1aa7654 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -258,19 +258,11 @@ in }; } { - name = "uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i"; + name = "uucode-0.0.0-ZZjBPiqdPwB-rG3ieaq3c6tMpnksWYs4_rGj2IvFGjjB"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz"; - hash = "sha256-1q5n3eqopVi1qrsg3XOth3ZkVo2ah2WgcyHkZrV7260="; - }; - } - { - name = "uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ"; - path = fetchZigArtifact { - name = "uucode_x"; - url = "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz"; - hash = "sha256-nr3tujSgGr5qE++ctjXGyS+9PrVdItovVHHgbo9ntWM="; + url = "https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz"; + hash = "sha256-MkWxYZHONRxGyUvGI5cAKi/ckGiXIBxkktCz5r+zWrk="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 88a0ad813..f31cfcc50 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -26,8 +26,7 @@ https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23c https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz -https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz -https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz +https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 80dcf4bc1..3eae3c8d2 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -133,15 +133,9 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz", - "dest": "vendor/p/uucode-0.0.0-ZZjBPjWBPACBbQFG11xoSRCP8NztUnPCieiCtBx0t57i", - "sha256": "d6ae67ddeaa8a558b5aabb20dd73ad877664568d9a8765a07321e466b57bdbad" - }, - { - "type": "archive", - "url": "https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz", - "dest": "vendor/p/uucode_x-0.0.0-5_D0j04hAADjn00a4Jfsjqz-gO6oF8FTLWUXmmvO1_MQ", - "sha256": "9ebdedba34a01abe6a13ef9cb635c6c92fbd3eb55d22da2f5471e06e8f67b563" + "url": "https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz", + "dest": "vendor/p/uucode-0.0.0-ZZjBPiqdPwB-rG3ieaq3c6tMpnksWYs4_rGj2IvFGjjB", + "sha256": "3245b16191ce351c46c94bc62397002a2fdc906897201c6492d0b3e6bfb35ab9" }, { "type": "git", From 2af08bdbe30d4982cf9ed85de42ca5cd80bcf75c Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 6 Sep 2025 10:42:02 -0400 Subject: [PATCH 018/319] trying a bunch of things to get performance to match --- build.zig.zon | 4 +- src/benchmark/CodepointWidth.zig | 40 ++++++++ src/benchmark/GraphemeBreak.zig | 159 +++++++++++++++++++++++++++++++ src/build/UnicodeTables.zig | 4 +- src/build/uucode_config.zig | 96 ++++++++++++++++++- src/simd/codepoint_width.zig | 3 +- src/terminal/Terminal.zig | 2 +- src/unicode/grapheme.zig | 5 +- src/unicode/main.zig | 1 + src/unicode/props.zig | 129 ++++++++++++------------- 10 files changed, 367 insertions(+), 76 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 79b06cf8b..4c3e36b89 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,8 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz", - .hash = "uucode-0.0.0-ZZjBPiqdPwB-rG3ieaq3c6tMpnksWYs4_rGj2IvFGjjB", + .url = "https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz", + .hash = "uucode-0.0.0-ZZjBPl_dPwC-BPhSJLID4Hs9O0zw-vZKGXdaOBFch8c8", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index e9207aed5..b6c719184 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -10,6 +10,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); +const uucode = @import("uucode"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); const simd = @import("../simd/main.zig"); const table = @import("../unicode/main.zig").table; @@ -47,6 +48,9 @@ pub const Mode = enum { /// Test our lookup table implementation. table, + + /// Using uucode, with custom `width` extension based on `wcwidth`. + uucode, }; /// Create a new terminal stream handler for the given arguments. @@ -71,6 +75,7 @@ pub fn benchmark(self: *CodepointWidth) Benchmark { .wcwidth => stepWcwidth, .table => stepTable, .simd => stepSimd, + .uucode => stepUucode, }, .setupFn = setup, .teardownFn = teardown, @@ -192,6 +197,41 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { } } +fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + const cp_, const consumed = d.next(c); + assert(consumed); + if (cp_) |cp| { + // This is the same trick we do in terminal.zig so we + // keep it here. + const width = if (cp <= 0xFF) + 1 + else + //uucode.getX(.width, @intCast(cp)); + //uucode.getWidth(@intCast(cp)); + uucode.getSpecial(@intCast(cp)).width; + + // Write the width to the buffer to avoid it being compiled + // away + buf[0] = @intCast(width); + } + } + } +} + test CodepointWidth { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index 57effebe4..105371ea5 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -8,6 +8,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); +const uucode = @import("uucode"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); const unicode = @import("../unicode/main.zig"); @@ -38,6 +39,9 @@ pub const Mode = enum { /// Ghostty's table-based approach. table, + + /// Uucode + uucode, }; /// Create a new terminal stream handler for the given arguments. @@ -60,6 +64,7 @@ pub fn benchmark(self: *GraphemeBreak) Benchmark { .stepFn = switch (self.opts.mode) { .noop => stepNoop, .table => stepTable, + .uucode => stepUucode, }, .setupFn = setup, .teardownFn = teardown, @@ -134,6 +139,160 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { } } +const GraphemeBoundaryClass = uucode.TypeOfX(.grapheme_boundary_class); + +pub fn computeGraphemeBoundaryClass( + gb1: GraphemeBoundaryClass, + gb2: GraphemeBoundaryClass, + state: *uucode.grapheme.BreakState, +) bool { + // Set state back to default when `gb1` or `gb2` is not expected in sequence. + switch (state.*) { + .regional_indicator => { + if (gb1 != .regional_indicator or gb2 != .regional_indicator) { + state.* = .default; + } + }, + .extended_pictographic => { + switch (gb1) { + .extend, + .zwj, + .extended_pictographic, + => {}, + + else => state.* = .default, + } + + switch (gb2) { + .extend, + .zwj, + .extended_pictographic, + => {}, + + else => state.* = .default, + } + }, + .default, .indic_conjunct_break_consonant, .indic_conjunct_break_linker => {}, + } + + // GB6: L x (L | V | LV | VT) + if (gb1 == .L) { + if (gb2 == .L or + gb2 == .V or + gb2 == .LV or + gb2 == .LVT) return false; + } + + // GB7: (LV | V) x (V | T) + if (gb1 == .LV or gb1 == .V) { + if (gb2 == .V or gb2 == .T) return false; + } + + // GB8: (LVT | T) x T + if (gb1 == .LVT or gb1 == .T) { + if (gb2 == .T) return false; + } + + // Handle GB9 (Extend | ZWJ) later, since it can also match the start of + // GB9c (Indic) and GB11 (Emoji ZWJ) + + // GB9a: SpacingMark + if (gb2 == .spacing_mark) return false; + + // GB9b: Prepend + if (gb1 == .prepend) return false; + + // GB11: Emoji ZWJ sequence + if (gb1 == .extended_pictographic) { + // start of sequence: + + // In normal operation, we'll be in this state, but + // precomputeGraphemeBreak iterates all states. + // std.debug.assert(state.* == .default); + + if (gb2 == .extend or gb2 == .zwj) { + state.* = .extended_pictographic; + return false; + } + // else, not an Emoji ZWJ sequence + } else if (state.* == .extended_pictographic) { + // continue or end sequence: + + if (gb1 == .extend and (gb2 == .extend or gb2 == .zwj)) { + // continue extend* ZWJ sequence + return false; + } else if (gb1 == .zwj and gb2 == .extended_pictographic) { + // ZWJ -> end of sequence + state.* = .default; + return false; + } else { + // Not a valid Emoji ZWJ sequence + state.* = .default; + } + } + + // GB12 and GB13: Regional Indicator + if (gb1 == .regional_indicator and gb2 == .regional_indicator) { + if (state.* == .default) { + state.* = .regional_indicator; + return false; + } else { + state.* = .default; + return true; + } + } + + // GB9: x (Extend | ZWJ) + if (gb2 == .extend or gb2 == .zwj) return false; + + // GB999: Otherwise, break everywhere + return true; +} + +pub fn isBreak( + cp1: u21, + cp2: u21, + state: *uucode.grapheme.BreakState, +) bool { + const table = comptime uucode.grapheme.precomputeGraphemeBreak( + GraphemeBoundaryClass, + computeGraphemeBoundaryClass, + ); + const gb1 = uucode.getX(.grapheme_boundary_class, cp1); + const gb2 = uucode.getX(.grapheme_boundary_class, cp2); + const result = table.get(gb1, gb2, state.*); + state.* = result.state; + return result.result; +} + +fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { + const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var state: uucode.grapheme.BreakState = .default; + var cp1: u21 = 0; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + const cp_, const consumed = d.next(c); + assert(consumed); + if (cp_) |cp2| { + const v = isBreak(cp1, @intCast(cp2), &state); + buf[0] = @intCast(@intFromBool(v)); + cp1 = cp2; + } + } + } +} + test GraphemeBreak { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index 78bcef2c9..219b8589a 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -24,14 +24,16 @@ pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables if (b.lazyDependency("uucode", .{ .target = b.graph.host, .@"tables.zig" = uucode_tables_zig, + .build_config_path = b.path("src/build/uucode_config.zig"), })) |dep| { exe.root_module.addImport("uucode", dep.module("uucode")); } const run = b.addRunArtifact(exe); + const output = run.addOutputFileArg("tables.zig"); return .{ .exe = exe, - .output = run.captureStdOut(), + .output = output, }; } diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 69d0d2fd3..e2e3c9163 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -3,6 +3,93 @@ const config_x = @import("config.x.zig"); const d = config.default; const wcwidth = config_x.wcwidth; +pub const log_level = .debug; + +fn computeWidth(cp: u21, data: anytype, backing: anytype, tracking: anytype) void { + _ = cp; + _ = backing; + _ = tracking; + if (data.wcwidth < 0) { + data.width = 0; + } else if (data.wcwidth > 2) { + data.width = 2; + } else { + data.width = @intCast(data.wcwidth); + } +} + +const width = config.Extension{ .inputs = &.{"wcwidth"}, .compute = &computeWidth, .fields = &.{ + .{ .name = "width", .type = u2 }, +} }; + +pub const GraphemeBoundaryClass = enum(u4) { + invalid, + L, + V, + T, + LV, + LVT, + prepend, + extend, + zwj, + spacing_mark, + regional_indicator, + extended_pictographic, + extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base} + emoji_modifier, // \p{Emoji_Modifier} +}; + +fn computeGraphemeBoundaryClass(cp: u21, data: anytype, backing: anytype, tracking: anytype) void { + _ = cp; + _ = backing; + _ = tracking; + if (data.is_emoji_modifier) { + data.grapheme_boundary_class = .emoji_modifier; + } else if (data.is_emoji_modifier_base) { + data.grapheme_boundary_class = .extended_pictographic_base; + } else { + data.grapheme_boundary_class = switch (data.grapheme_break) { + .extended_pictographic => .extended_pictographic, + .l => .L, + .v => .V, + .t => .T, + .lv => .LV, + .lvt => .LVT, + .prepend => .prepend, + .zwj => .zwj, + .spacing_mark => .spacing_mark, + .regional_indicator => .regional_indicator, + + .zwnj, + .indic_conjunct_break_extend, + .indic_conjunct_break_linker, + => .extend, + + // This is obviously not INVALID invalid, there is SOME grapheme + // boundary class for every codepoint. But we don't care about + // anything that doesn't fit into the above categories. + .other, + .indic_conjunct_break_consonant, + .cr, + .lf, + .control, + => .invalid, + }; + } +} + +const grapheme_boundary_class = config.Extension{ + .inputs = &.{ + "grapheme_break", + "is_emoji_modifier", + "is_emoji_modifier_base", + }, + .compute = &computeGraphemeBoundaryClass, + .fields = &.{ + .{ .name = "grapheme_boundary_class", .type = GraphemeBoundaryClass }, + }, +}; + pub const tables = [_]config.Table{ .{ .extensions = &.{wcwidth}, @@ -14,9 +101,16 @@ pub const tables = [_]config.Table{ d.field("case_folding_full"), // Alternative: // d.field("case_folding_simple"), - d.field("grapheme_break"), d.field("is_emoji_modifier"), d.field("is_emoji_modifier_base"), + d.field("grapheme_break"), + }, + }, + .{ + .extensions = &.{ wcwidth, width, grapheme_boundary_class }, + .fields = &.{ + width.field("width"), + grapheme_boundary_class.field("grapheme_boundary_class"), }, }, }; diff --git a/src/simd/codepoint_width.zig b/src/simd/codepoint_width.zig index e2383aff1..008c7ad9f 100644 --- a/src/simd/codepoint_width.zig +++ b/src/simd/codepoint_width.zig @@ -29,7 +29,8 @@ test "codepointWidth basic" { // const uucode = @import("uucode"); // // const min = 0xFF + 1; // start outside ascii -// for (min..uucode.code_point_range_end) |cp| { +// const max = std.math.maxInt(u21) + 1; +// for (min..max) |cp| { // const simd = codepointWidth(@intCast(cp)); // const uu = @min(2, @max(0, uucode.get(.wcwidth, @intCast(cp)))); // if (simd != uu) mismatch: { diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c46488f98..6b3b9252b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -345,7 +345,7 @@ pub fn print(self: *Terminal, c: u21) !void { if (c == 0xFE0F or c == 0xFE0E) { // This only applies to emoji const prev_props = unicode.getProperties(prev.cell.content.codepoint); - const emoji = prev_props.grapheme_boundary_class.isExtendedPictographic(); + const emoji = unicode.isExtendedPictographic(prev_props.grapheme_boundary_class); if (!emoji) return; switch (c) { diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index 0950bedba..b0cb4ead9 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -2,6 +2,7 @@ const std = @import("std"); const props = @import("props.zig"); const GraphemeBoundaryClass = props.GraphemeBoundaryClass; const table = props.table; +const isExtendedPictographic = props.isExtendedPictographic; /// Determines if there is a grapheme break between two codepoints. This /// must be called sequentially maintaining the state between calls. @@ -80,7 +81,7 @@ fn graphemeBreakClass( state: *BreakState, ) bool { // GB11: Emoji Extend* ZWJ x Emoji - if (!state.extended_pictographic and gbc1.isExtendedPictographic()) { + if (!state.extended_pictographic and isExtendedPictographic(gbc1)) { state.extended_pictographic = true; } @@ -131,7 +132,7 @@ fn graphemeBreakClass( // GB11: Emoji Extend* ZWJ x Emoji if (state.extended_pictographic and gbc1 == .zwj and - gbc2.isExtendedPictographic()) + isExtendedPictographic(gbc2)) { state.extended_pictographic = false; return false; diff --git a/src/unicode/main.zig b/src/unicode/main.zig index f5b911948..2b0b8ef9c 100644 --- a/src/unicode/main.zig +++ b/src/unicode/main.zig @@ -7,6 +7,7 @@ pub const Properties = props.Properties; pub const getProperties = props.get; pub const graphemeBreak = grapheme.graphemeBreak; pub const GraphemeBreakState = grapheme.BreakState; +pub const isExtendedPictographic = props.isExtendedPictographic; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/unicode/props.zig b/src/unicode/props.zig index c06329876..579e59977 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -6,10 +6,11 @@ const lut = @import("lut.zig"); /// The lookup tables for Ghostty. pub const table = table: { + const Props = uucode.PackedTypeOf("1"); // This is only available after running main() below as part of the Ghostty // build.zig, but due to Zig's lazy analysis we can still reference it here. - const generated = @import("unicode_tables").Tables(Properties); - const Tables = lut.Tables(Properties); + const generated = @import("unicode_tables").Tables(Props); + const Tables = lut.Tables(Props); break :table Tables{ .stage1 = &generated.stage1, .stage2 = &generated.stage2, @@ -61,81 +62,62 @@ pub const Properties = struct { /// Possible grapheme boundary classes. This isn't an exhaustive list: /// we omit control, CR, LF, etc. because in Ghostty's usage that are /// impossible because they're handled by the terminal. -pub const GraphemeBoundaryClass = enum(u4) { - invalid, - L, - V, - T, - LV, - LVT, - prepend, - extend, - zwj, - spacing_mark, - regional_indicator, - extended_pictographic, - extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base} - emoji_modifier, // \p{Emoji_Modifier} +pub const GraphemeBoundaryClass = uucode.TypeOfX(.grapheme_boundary_class); - /// Gets the grapheme boundary class for a codepoint. - /// The use case for this is only in generating lookup tables. - pub fn init(cp: u21) GraphemeBoundaryClass { - if (cp < uucode.code_point_range_end) { - if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; - if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; +/// Gets the grapheme boundary class for a codepoint. +/// The use case for this is only in generating lookup tables. +pub fn computeGraphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { + if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; + if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; - return switch (uucode.get(.grapheme_break, cp)) { - .extended_pictographic => .extended_pictographic, - .l => .L, - .v => .V, - .t => .T, - .lv => .LV, - .lvt => .LVT, - .prepend => .prepend, - .zwj => .zwj, - .spacing_mark => .spacing_mark, - .regional_indicator => .regional_indicator, + return switch (uucode.get(.grapheme_break, cp)) { + .extended_pictographic => .extended_pictographic, + .l => .L, + .v => .V, + .t => .T, + .lv => .LV, + .lvt => .LVT, + .prepend => .prepend, + .zwj => .zwj, + .spacing_mark => .spacing_mark, + .regional_indicator => .regional_indicator, - .zwnj, - .indic_conjunct_break_extend, - .indic_conjunct_break_linker, - => .extend, + .zwnj, + .indic_conjunct_break_extend, + .indic_conjunct_break_linker, + => .extend, - // This is obviously not INVALID invalid, there is SOME grapheme - // boundary class for every codepoint. But we don't care about - // anything that doesn't fit into the above categories. - .other, - .indic_conjunct_break_consonant, - .cr, - .lf, - .control, - => .invalid, - }; - } else { - return .invalid; - } - } + // This is obviously not INVALID invalid, there is SOME grapheme + // boundary class for every codepoint. But we don't care about + // anything that doesn't fit into the above categories. + .other, + .indic_conjunct_break_consonant, + .cr, + .lf, + .control, + => .invalid, + }; +} - /// Returns true if this is an extended pictographic type. This - /// should be used instead of comparing the enum value directly - /// because we classify multiple. - pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool { - return switch (self) { - .extended_pictographic, - .extended_pictographic_base, - => true, +/// Returns true if this is an extended pictographic type. This +/// should be used instead of comparing the enum value directly +/// because we classify multiple. +pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool { + return switch (self) { + .extended_pictographic, + .extended_pictographic_base, + => true, - else => false, - }; - } -}; + else => false, + }; +} pub fn get(cp: u21) Properties { - const wcwidth = if (cp < uucode.code_point_range_end) uucode.get(.wcwidth, cp) else 0; + const wcwidth = uucode.get(.wcwidth, cp); return .{ .width = @intCast(@min(2, @max(0, wcwidth))), - .grapheme_boundary_class = .init(cp), + .grapheme_boundary_class = computeGraphemeBoundaryClass(cp), }; } @@ -145,6 +127,13 @@ pub fn main() !void { defer arena_state.deinit(); const alloc = arena_state.allocator(); + var args_iter = try std.process.argsWithAllocator(alloc); + defer args_iter.deinit(); + _ = args_iter.skip(); // Skip program name + + const output_path = args_iter.next() orelse std.debug.panic("No output file arg!", .{}); + std.debug.print("Unicode tables output_path = {s}\n", .{output_path}); + const gen: lut.Generator( Properties, struct { @@ -164,7 +153,10 @@ pub fn main() !void { defer alloc.free(t.stage1); defer alloc.free(t.stage2); defer alloc.free(t.stage3); - try t.writeZig(std.io.getStdOut().writer()); + var out_file = try std.fs.cwd().createFile(output_path, .{}); + defer out_file.close(); + const writer = out_file.writer(); + try t.writeZig(writer); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ @@ -180,7 +172,8 @@ pub fn main() !void { // const testing = std.testing; // // const min = 0xFF + 1; // start outside ascii -// for (min..uucode.code_point_range_end) |cp| { +// const max = std.math.maxInt(u21) + 1; +// for (min..max) |cp| { // const t = table.get(@intCast(cp)); // const uu = @min(2, @max(0, uucode.get(.wcwidth, @intCast(cp)))); // if (t.width != uu) { From c3994347c079151fa22ce28e7e60bdfd9dcd1f44 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 6 Sep 2025 14:55:21 -0400 Subject: [PATCH 019/319] doNotOptimizeAway --- build.zig.zon | 4 ++-- src/benchmark/CodepointWidth.zig | 28 ++++++---------------------- src/benchmark/GraphemeBreak.zig | 25 +++++++++++++++---------- src/benchmark/IsSymbol.zig | 10 +++++----- src/build/UnicodeTables.zig | 5 ++--- 5 files changed, 30 insertions(+), 42 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index ba128f853..4b2ef813a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,8 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz", - .hash = "uucode-0.0.0-ZZjBPl_dPwC-BPhSJLID4Hs9O0zw-vZKGXdaOBFch8c8", + .url = "https://github.com/jacobsandlund/uucode/archive/8a4e07adbcb70bd45fbb70520dbbca6df44ec083.tar.gz", + .hash = "uucode-0.0.0-ZZjBPuTdPwBOU3VAvAT6XMbmj1QL1IA7OtMraVMB5j_0", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index b6c719184..d175b69e9 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -126,11 +126,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - const width = wcwidth(cp); - - // Write the width to the buffer to avoid it being compiled - // away - buf[0] = @intCast(width); + std.mem.doNotOptimizeAway(wcwidth(cp)); } } } @@ -156,14 +152,10 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { if (cp_) |cp| { // This is the same trick we do in terminal.zig so we // keep it here. - const width = if (cp <= 0xFF) + std.mem.doNotOptimizeAway(if (cp <= 0xFF) 1 else - table.get(@intCast(cp)).width; - - // Write the width to the buffer to avoid it being compiled - // away - buf[0] = @intCast(width); + table.get(@intCast(cp)).width); } } } @@ -187,11 +179,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - const width = simd.codepointWidth(cp); - - // Write the width to the buffer to avoid it being compiled - // away - buf[0] = @intCast(width); + std.mem.doNotOptimizeAway(simd.codepointWidth(cp)); } } } @@ -217,16 +205,12 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { if (cp_) |cp| { // This is the same trick we do in terminal.zig so we // keep it here. - const width = if (cp <= 0xFF) + std.mem.doNotOptimizeAway(if (cp <= 0xFF) 1 else //uucode.getX(.width, @intCast(cp)); //uucode.getWidth(@intCast(cp)); - uucode.getSpecial(@intCast(cp)).width; - - // Write the width to the buffer to avoid it being compiled - // away - buf[0] = @intCast(width); + uucode.getSpecial(@intCast(cp)).width); } } } diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index 105371ea5..9bbfc469c 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -21,7 +21,7 @@ data_f: ?std.fs.File = null, pub const Options = struct { /// The type of codepoint width calculation to use. - mode: Mode = .table, + mode: Mode = .noop, /// The data to read as a filepath. If this is "-" then /// we will read stdin. If this is unset, then we will @@ -40,7 +40,7 @@ pub const Mode = enum { /// Ghostty's table-based approach. table, - /// Uucode + /// uucode implementation uucode, }; @@ -131,8 +131,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp2| { - const v = unicode.graphemeBreak(cp1, @intCast(cp2), &state); - buf[0] = @intCast(@intFromBool(v)); + std.mem.doNotOptimizeAway(unicode.graphemeBreak(cp1, @intCast(cp2), &state)); cp1 = cp2; } } @@ -141,10 +140,16 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const GraphemeBoundaryClass = uucode.TypeOfX(.grapheme_boundary_class); +const BreakState = enum(u3) { + default, + regional_indicator, + extended_pictographic, +}; + pub fn computeGraphemeBoundaryClass( gb1: GraphemeBoundaryClass, gb2: GraphemeBoundaryClass, - state: *uucode.grapheme.BreakState, + state: *BreakState, ) bool { // Set state back to default when `gb1` or `gb2` is not expected in sequence. switch (state.*) { @@ -172,7 +177,7 @@ pub fn computeGraphemeBoundaryClass( else => state.* = .default, } }, - .default, .indic_conjunct_break_consonant, .indic_conjunct_break_linker => {}, + .default => {}, } // GB6: L x (L | V | LV | VT) @@ -252,10 +257,11 @@ pub fn computeGraphemeBoundaryClass( pub fn isBreak( cp1: u21, cp2: u21, - state: *uucode.grapheme.BreakState, + state: *BreakState, ) bool { const table = comptime uucode.grapheme.precomputeGraphemeBreak( GraphemeBoundaryClass, + BreakState, computeGraphemeBoundaryClass, ); const gb1 = uucode.getX(.grapheme_boundary_class, cp1); @@ -271,7 +277,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var state: uucode.grapheme.BreakState = .default; + var state: BreakState = .default; var cp1: u21 = 0; var buf: [4096]u8 = undefined; while (true) { @@ -285,8 +291,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp2| { - const v = isBreak(cp1, @intCast(cp2), &state); - buf[0] = @intCast(@intFromBool(v)); + std.mem.doNotOptimizeAway(isBreak(cp1, @intCast(cp2), &state)); cp1 = cp2; } } diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index 940207619..368a0570e 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -21,7 +21,7 @@ data_f: ?std.fs.File = null, pub const Options = struct { /// Which test to run. - mode: Mode = .ziglyph, + mode: Mode = .uucode, /// The data to read as a filepath. If this is "-" then /// we will read stdin. If this is unset, then we will @@ -32,8 +32,8 @@ pub const Options = struct { }; pub const Mode = enum { - /// "Naive" ziglyph implementation. - ziglyph, + /// uucode implementation + uucode, /// Ghostty's table-based approach. table, @@ -57,7 +57,7 @@ pub fn destroy(self: *IsSymbol, alloc: Allocator) void { pub fn benchmark(self: *IsSymbol) Benchmark { return .init(self, .{ .stepFn = switch (self.opts.mode) { - .ziglyph => stepZiglyph, + .uucode => stepUucode, .table => stepTable, }, .setupFn = setup, @@ -85,7 +85,7 @@ fn teardown(ptr: *anyopaque) void { } } -fn stepZiglyph(ptr: *anyopaque) Benchmark.Error!void { +fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const self: *IsSymbol = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index d71c5ca95..dc3fa2cb3 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -46,14 +46,13 @@ pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables const props_run = b.addRunArtifact(props_exe); const symbols_run = b.addRunArtifact(symbols_exe); - const props_output = props_run.addOutputFileArg("tables.zig"); - const symbols_output = symbols_run.addOutputFileArg("tables.zig"); + const props_output = props_run.addOutputFileArg("props_table.zig"); return .{ .props_exe = props_exe, .symbols_exe = symbols_exe, .props_output = props_output, - .symbols_output = symbols_output, + .symbols_output = symbols_run.captureStdOut(), }; } From b0db51c45e29beb703a711922e44cfd4a621efd2 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 6 Sep 2025 15:01:29 -0400 Subject: [PATCH 020/319] fast getX(.is_symbol) --- src/benchmark/IsSymbol.zig | 3 ++- src/build/uucode_config.zig | 29 +++++++++++++++++++++++++++++ src/unicode/symbols.zig | 11 ++--------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index 368a0570e..7ec9137d2 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -11,6 +11,7 @@ const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); const symbols = @import("../unicode/symbols.zig"); +const uucode = @import("uucode"); const log = std.log.scoped(.@"is-symbol-bench"); @@ -103,7 +104,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - std.mem.doNotOptimizeAway(symbols.isSymbol(cp)); + std.mem.doNotOptimizeAway(uucode.getX(.is_symbol, cp)); } } } diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index e2e3c9163..c349216d7 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -90,6 +90,29 @@ const grapheme_boundary_class = config.Extension{ }, }; +fn computeIsSymbol(cp: u21, data: anytype, backing: anytype, tracking: anytype) void { + _ = cp; + _ = backing; + _ = tracking; + const block = data.block; + data.is_symbol = data.general_category == .other_private_use or + block == .dingbats or + block == .emoticons or + block == .miscellaneous_symbols or + block == .enclosed_alphanumerics or + block == .enclosed_alphanumeric_supplement or + block == .miscellaneous_symbols_and_pictographs or + block == .transport_and_map_symbols; +} + +const is_symbol = config.Extension{ + .inputs = &.{ "block", "general_category" }, + .compute = &computeIsSymbol, + .fields = &.{ + .{ .name = "is_symbol", .type = bool }, + }, +}; + pub const tables = [_]config.Table{ .{ .extensions = &.{wcwidth}, @@ -113,4 +136,10 @@ pub const tables = [_]config.Table{ grapheme_boundary_class.field("grapheme_boundary_class"), }, }, + .{ + .extensions = &.{is_symbol}, + .fields = &.{ + is_symbol.field("is_symbol"), + }, + }, }; diff --git a/src/unicode/symbols.zig b/src/unicode/symbols.zig index 20749bf91..b03f82cf8 100644 --- a/src/unicode/symbols.zig +++ b/src/unicode/symbols.zig @@ -31,15 +31,8 @@ pub const table = table: { /// In the future it may be prudent to expand this to encompass more /// symbol-like characters, and/or exclude some PUA sections. pub fn isSymbol(cp: u21) bool { - const block = uucode.get(.block, cp); - return uucode.get(.general_category, cp) == .other_private_use or - block == .dingbats or - block == .emoticons or - block == .miscellaneous_symbols or - block == .enclosed_alphanumerics or - block == .enclosed_alphanumeric_supplement or - block == .miscellaneous_symbols_and_pictographs or - block == .transport_and_map_symbols; + // TODO: probably can remove this method and just call uucode directly + return uucode.getX(.is_symbol, cp); } /// Runnable binary to generate the lookup tables and output to stdout. From 9ed2385b489995f6704bac6dff40c859bb040bbf Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sat, 6 Sep 2025 15:52:35 -0400 Subject: [PATCH 021/319] Merge `main` --- macos/Sources/App/macOS/AppDelegate.swift | 12 ++++++++++-- src/config/Config.zig | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index f8cf95de2..558658b89 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -259,8 +259,16 @@ class AppDelegate: NSObject, // Setup signal handlers setupSignals() - // If we launched via zig run then we need to force foreground. - if Ghostty.launchSource == .zig_run { + switch Ghostty.launchSource { + case .app: + // Don't have to do anything. + break + + case .zig_run, .cli: + // Part of launch services (clicking an app, using `open`, etc.) activates + // the application and brings it to the front. When using the CLI we don't + // get this behavior, so we have to do it manually. + // This never gets called until we click the dock icon. This forces it // activate immediately. applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification)) diff --git a/src/config/Config.zig b/src/config/Config.zig index 221a7cf93..3ef39d87c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4535,6 +4535,13 @@ fn probableCliEnvironment() bool { // its not a real supported target and GTK via WSL2 assuming // single instance is probably fine. .windows => return false, + + // On macOS, we don't want to detect `open` calls as CLI envs. + // Our desktop detection on macOS is very accurate due to how + // processes are launched on macOS, so if we detect we're launched + // from the app bundle then we're not in a CLI environment. + .macos => if (internal_os.launchedFromDesktop()) return false, + else => {}, } From cffa52e6582a05ce5dd057533152c460b874099f Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 9 Sep 2025 11:38:10 -0400 Subject: [PATCH 022/319] changes after benchmarking --- build.zig.zon | 4 ++-- src/benchmark/CodepointWidth.zig | 12 +++++------- src/benchmark/GraphemeBreak.zig | 2 +- src/benchmark/IsSymbol.zig | 13 ++++++++++--- src/build/UnicodeTables.zig | 3 ++- src/unicode/props.zig | 12 ++++++++---- src/unicode/symbols.zig | 26 +++++++++++++++++++++----- 7 files changed, 49 insertions(+), 23 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index fe5c2882d..4504dd87d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,8 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/8a4e07adbcb70bd45fbb70520dbbca6df44ec083.tar.gz", - .hash = "uucode-0.0.0-ZZjBPuTdPwBOU3VAvAT6XMbmj1QL1IA7OtMraVMB5j_0", + .url = "https://github.com/jacobsandlund/uucode/archive/507da5bf0a03c940f2688f717fd2357c5b2e9386.tar.gz", + .hash = "uucode-0.0.0-ZZjBPhbMPwBdJL3hgJifuJf2CiOWfBp08pxULHNohqZE", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index d175b69e9..ab3e31318 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -114,7 +114,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); @@ -138,7 +138,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); @@ -167,7 +167,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); @@ -191,7 +191,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); @@ -208,9 +208,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { std.mem.doNotOptimizeAway(if (cp <= 0xFF) 1 else - //uucode.getX(.width, @intCast(cp)); - //uucode.getWidth(@intCast(cp)); - uucode.getSpecial(@intCast(cp)).width); + uucode.getX(.width, @intCast(cp))); } } } diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index 9bbfc469c..8096a3897 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -146,7 +146,7 @@ const BreakState = enum(u3) { extended_pictographic, }; -pub fn computeGraphemeBoundaryClass( +fn computeGraphemeBoundaryClass( gb1: GraphemeBoundaryClass, gb2: GraphemeBoundaryClass, state: *BreakState, diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index 7ec9137d2..0997da41d 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -92,7 +92,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); @@ -116,7 +116,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); @@ -128,7 +128,14 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - std.mem.doNotOptimizeAway(symbols.table.get(cp)); + if (uucode.getX(.is_symbol, cp) != symbols.table.get(cp)) { + std.debug.panic("uucode and table disagree on codepoint {d}: uucode={}, table={}", .{ + cp, + uucode.getX(.is_symbol, cp), + symbols.table.get(cp), + }); + } + //std.mem.doNotOptimizeAway(symbols.table.get(cp)); } } } diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index dc3fa2cb3..a947ce137 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -47,12 +47,13 @@ pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables const props_run = b.addRunArtifact(props_exe); const symbols_run = b.addRunArtifact(symbols_exe); const props_output = props_run.addOutputFileArg("props_table.zig"); + const symbols_output = symbols_run.addOutputFileArg("symbols_table.zig"); return .{ .props_exe = props_exe, .symbols_exe = symbols_exe, .props_output = props_output, - .symbols_output = symbols_run.captureStdOut(), + .symbols_output = symbols_output, }; } diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 962fb16c4..e104bcb53 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -66,7 +66,8 @@ pub const GraphemeBoundaryClass = uucode.TypeOfX(.grapheme_boundary_class); /// Gets the grapheme boundary class for a codepoint. /// The use case for this is only in generating lookup tables. -pub fn computeGraphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { +fn computeGraphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { + if (cp > uucode.config.max_code_point) return .invalid; if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; @@ -113,7 +114,10 @@ pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool { } pub fn get(cp: u21) Properties { - const wcwidth = uucode.get(.wcwidth, cp); + const wcwidth = if (cp > uucode.config.max_code_point) + 0 + else + uucode.get(.wcwidth, cp); return .{ .width = @intCast(@min(2, @max(0, wcwidth))), @@ -131,8 +135,8 @@ pub fn main() !void { defer args_iter.deinit(); _ = args_iter.skip(); // Skip program name - const output_path = args_iter.next() orelse std.debug.panic("No output file arg!", .{}); - std.debug.print("Unicode tables output_path = {s}\n", .{output_path}); + const output_path = args_iter.next() orelse std.debug.panic("No output file arg for props exe!", .{}); + std.debug.print("Unicode props_table output_path = {s}\n", .{output_path}); const gen: lut.Generator( Properties, diff --git a/src/unicode/symbols.zig b/src/unicode/symbols.zig index b03f82cf8..8150d279f 100644 --- a/src/unicode/symbols.zig +++ b/src/unicode/symbols.zig @@ -41,12 +41,22 @@ pub fn main() !void { defer arena_state.deinit(); const alloc = arena_state.allocator(); + var args_iter = try std.process.argsWithAllocator(alloc); + defer args_iter.deinit(); + _ = args_iter.skip(); // Skip program name + + const output_path = args_iter.next() orelse std.debug.panic("No output file arg for symbols exe!", .{}); + std.debug.print("Unicode symbols_table output_path = {s}\n", .{output_path}); + const gen: lut.Generator( bool, struct { pub fn get(ctx: @This(), cp: u21) !bool { _ = ctx; - return isSymbol(cp); + return if (cp > uucode.config.max_code_point) + false + else + isSymbol(@intCast(cp)); } pub fn eql(ctx: @This(), a: bool, b: bool) bool { @@ -60,7 +70,10 @@ pub fn main() !void { defer alloc.free(t.stage1); defer alloc.free(t.stage2); defer alloc.free(t.stage3); - try t.writeZig(std.io.getStdOut().writer()); + var out_file = try std.fs.cwd().createFile(output_path, .{}); + defer out_file.close(); + const writer = out_file.writer(); + try t.writeZig(writer); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ @@ -79,10 +92,13 @@ test "unicode symbols: tables match uucode" { for (0..std.math.maxInt(u21)) |cp| { const t = table.get(@intCast(cp)); - const zg = isSymbol(@intCast(cp)); + const uu = if (cp > uucode.config.max_code_point) + false + else + isSymbol(@intCast(cp)); - if (t != zg) { - std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); + if (t != uu) { + std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t, uu }); try testing.expect(false); } } From 4d37853f6c3fe79069e8408d42a936e9bb13411f Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 11 Sep 2025 10:30:01 -0400 Subject: [PATCH 023/319] benchmark sources --- bench.sh | 7 +++++++ benchmark-sources.txt | 14 ++++++++++++++ build.zig.zon | 4 ++-- src/build/uucode_config.zig | 2 ++ src/unicode/props.zig | 5 ++--- 5 files changed, 27 insertions(+), 5 deletions(-) create mode 100755 bench.sh create mode 100644 benchmark-sources.txt diff --git a/bench.sh b/bench.sh new file mode 100755 index 000000000..332469b97 --- /dev/null +++ b/bench.sh @@ -0,0 +1,7 @@ +bench=$1 +warmup=$2 +runs=$3 +data=$4 + +echo hyperfine --warmup $warmup --runs=$runs "zig-out/bin/ghostty-bench +$bench --data=$data --mode=uucode" "zig-out/bin/ghostty-bench-old +$bench --data=$data --mode=uucode" +hyperfine --warmup $warmup --runs=$runs "zig-out/bin/ghostty-bench +$bench --data=$data --mode=uucode" "zig-out/bin/ghostty-bench-old +$bench --data=$data --mode=uucode" diff --git a/benchmark-sources.txt b/benchmark-sources.txt new file mode 100644 index 000000000..06aaf17d4 --- /dev/null +++ b/benchmark-sources.txt @@ -0,0 +1,14 @@ +# https://chatgpt.com/share/68c2da5f-65c8-800f-b3e4-55cdff7150cb + +zig-out/bin/ghostty-gen +utf8 | head -c 200000000 > data.txt + +curl -O https://www.gutenberg.org/cache/epub/30/pg30.txt +curl -O https://www.gutenberg.org/cache/epub/24264/pg24264.txt # 紅樓夢 by Xueqin Cao + +# From https://linguatools.org/tools/corpora/wikipedia-monolingual-corpora/ + +curl -L "https://www.dropbox.com/scl/fi/86gjpfzopssavk2nzo69u/arwiki-20180920-corpus.xml.bz2?dl=1&e=1&file_subpath=%2Fdata&rlkey=dmjlaw1xegg8vsje4xrn040v8" | bzcat | head -c 1000000000 > arwiki-20180920-corpus.xml # arabic + +curl -L "https://www.dropbox.com/scl/fi/la1nvupgk2honb3n6m9zc/enwiki-20181001-corpus.xml.bz2?rlkey=8vg4vokbaijh1lg5lw3ytc864&e=1&dl=1" | bzcat | head -c 1000000000 > enwiki-20181001-corpus.xml # english + +curl -L "https://www.dropbox.com/scl/fi/vru4zxv5qff1klod9xiht/jawiki-20181001-corpus.xml.bz2?rlkey=utuuooiwyupws3x5517u8n8jl&e=1&dl=1" | bzcat | head -c 1000000000 > jawiki-20181001-corpus.xml # japanese diff --git a/build.zig.zon b/build.zig.zon index 4504dd87d..97aaed8be 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,8 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/507da5bf0a03c940f2688f717fd2357c5b2e9386.tar.gz", - .hash = "uucode-0.0.0-ZZjBPhbMPwBdJL3hgJifuJf2CiOWfBp08pxULHNohqZE", + .url = "https://github.com/jacobsandlund/uucode/archive/a1833012b50197bdf7d43543e38f8be5c5a75016.tar.gz", + .hash = "uucode-0.0.0-ZZjBPu_RPwA01mgYvIKApZJX_JMUTKD2kyBsyYCdzfaz", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index c349216d7..f2078eb7a 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -130,6 +130,7 @@ pub const tables = [_]config.Table{ }, }, .{ + .stages = .two, .extensions = &.{ wcwidth, width, grapheme_boundary_class }, .fields = &.{ width.field("width"), @@ -137,6 +138,7 @@ pub const tables = [_]config.Table{ }, }, .{ + .stages = .two, .extensions = &.{is_symbol}, .fields = &.{ is_symbol.field("is_symbol"), diff --git a/src/unicode/props.zig b/src/unicode/props.zig index e104bcb53..c209fe2d3 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -6,11 +6,10 @@ const lut = @import("lut.zig"); /// The lookup tables for Ghostty. pub const table = table: { - const Props = uucode.PackedTypeOf("1"); // This is only available after running main() below as part of the Ghostty // build.zig, but due to Zig's lazy analysis we can still reference it here. - const generated = @import("unicode_tables").Tables(Props); - const Tables = lut.Tables(Props); + const generated = @import("unicode_tables").Tables(Properties); + const Tables = lut.Tables(Properties); break :table Tables{ .stage1 = &generated.stage1, .stage2 = &generated.stage2, From 3a7e7f905be2feac90f61b5111dca390e726a643 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Wed, 17 Sep 2025 22:03:00 -0700 Subject: [PATCH 024/319] Give the autoformatter what it wants --- 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 82cf107c8..e63b55726 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -170,7 +170,7 @@ pub const Face = struct { if (string.len > 1024) break :skip; var tmp: [512]u16 = undefined; const max = string.len / 2; - for (@as([]const u16, @alignCast(@ptrCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); + for (@as([]const u16, @ptrCast(@alignCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string; return buf[0..len]; } From cc165990ecfbc9f962e178e3c82dc1352729f5dc Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Wed, 17 Sep 2025 22:03:53 -0700 Subject: [PATCH 025/319] Use outline bbox for ascii_height measurement --- src/font/face/freetype.zig | 57 ++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index e63b55726..1cd789a66 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -985,14 +985,21 @@ pub const Face = struct { f26dot6ToF64(glyph.*.advance.x), max, ); - top = @max( - f26dot6ToF64(glyph.*.metrics.horiBearingY), - top, - ); - bottom = @min( - f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), - bottom, - ); + // We use the outline's bbox instead of the built-in + // metrics for better accuracy (see renderGlyph()). + const ymin, const ymax = metrics: { + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + break :metrics .{ bbox.yMin, bbox.yMax }; + } + break :metrics .{ + glyph.*.metrics.horiBearingY - glyph.*.metrics.height, + glyph.*.metrics.horiBearingY, + }; + }; + top = @max(f26dot6ToF64(ymax), top); + bottom = @min(f26dot6ToF64(ymin), bottom); } else |_| {} } } @@ -1035,7 +1042,15 @@ pub const Face = struct { .render = false, .no_svg = true, })) { - break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + const glyph = face.handle.*.glyph; + // We use the outline's bbox instead of the built-in + // metrics for better accuracy (see renderGlyph()). + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + break :cap f26dot6ToF64(bbox.yMax - bbox.yMin); + } + break :cap f26dot6ToF64(glyph.*.metrics.height); } else |_| {} } break :cap null; @@ -1048,7 +1063,15 @@ pub const Face = struct { .render = false, .no_svg = true, })) { - break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + const glyph = face.handle.*.glyph; + // We use the outline's bbox instead of the built-in + // metrics for better accuracy (see renderGlyph()). + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + break :ex f26dot6ToF64(bbox.yMax - bbox.yMin); + } + break :ex f26dot6ToF64(glyph.*.metrics.height); } else |_| {} } break :ex null; @@ -1078,14 +1101,24 @@ pub const Face = struct { // This can sometimes happen if there's a CJK font that has been // patched with the nerd fonts patcher and it butchers the advance // values so the advance ends up half the width of the actual glyph. - if (ft_glyph.*.metrics.width > ft_glyph.*.advance.x) { + const ft_glyph_width = ft_glyph_width: { + // We use the outline's bbox instead of the built-in + // metrics for better accuracy (see renderGlyph()). + if (ft_glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&ft_glyph.*.outline, &bbox); + break :ft_glyph_width bbox.xMax - bbox.xMin; + } + break :ft_glyph_width ft_glyph.*.metrics.width; + }; + if (ft_glyph_width > ft_glyph.*.advance.x) { var buf: [1024]u8 = undefined; const font_name = self.name(&buf) catch ""; log.warn( "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.", .{ font_name, - f26dot6ToF64(ft_glyph.*.metrics.width), + f26dot6ToF64(ft_glyph_width), f26dot6ToF64(ft_glyph.*.advance.x), }, ); From e1b2f6f02182192d6c040dbebc883615d4d5bbbd Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Wed, 17 Sep 2025 22:04:59 -0700 Subject: [PATCH 026/319] Use same hinting flags for measurement and rendering --- pkg/freetype/main.zig | 1 + src/font/face/freetype.zig | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/freetype/main.zig b/pkg/freetype/main.zig index b39650423..6ec818181 100644 --- a/pkg/freetype/main.zig +++ b/pkg/freetype/main.zig @@ -9,6 +9,7 @@ pub const Library = @import("Library.zig"); pub const Error = errors.Error; pub const Face = face.Face; +pub const LoadFlags = face.LoadFlags; pub const Tag = tag.Tag; pub const mulFix = computations.mulFix; diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 1cd789a66..c448d2735 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -956,6 +956,17 @@ pub const Face = struct { break :st .{ pos, thick }; }; + // Set the load flags to use when measuring glyphs. For consistency, we + // use same hinting settings as when rendering for consistency. + const measurement_load_flags: freetype.LoadFlags = .{ + .render = false, + .no_hinting = !self.load_flags.hinting, + .force_autohint = self.load_flags.@"force-autohint", + .no_autohint = !self.load_flags.autohint, + .target_mono = self.load_flags.monochrome, + .no_svg = true, + }; + // Cell width is calculated by calculating the widest width of the // visible ASCII characters. Usually 'M' is widest but we just take // whatever is widest. @@ -976,10 +987,7 @@ pub const Face = struct { var c: u8 = ' '; while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { + if (face.loadGlyph(glyph_index, measurement_load_flags)) { const glyph = face.handle.*.glyph; max = @max( f26dot6ToF64(glyph.*.advance.x), @@ -1038,10 +1046,7 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { + if (face.loadGlyph(glyph_index, measurement_load_flags)) { const glyph = face.handle.*.glyph; // We use the outline's bbox instead of the built-in // metrics for better accuracy (see renderGlyph()). @@ -1059,10 +1064,7 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { + if (face.loadGlyph(glyph_index, measurement_load_flags)) { const glyph = face.handle.*.glyph; // We use the outline's bbox instead of the built-in // metrics for better accuracy (see renderGlyph()). @@ -1086,10 +1088,7 @@ pub const Face = struct { const glyph = face.getCharIndex('水') orelse break :ic_width null; - face.loadGlyph(glyph, .{ - .render = false, - .no_svg = true, - }) catch break :ic_width null; + face.loadGlyph(glyph, measurement_load_flags) catch break :ic_width null; const ft_glyph = face.handle.*.glyph; From 03a707b2c0d31f2a228d5578abf6c9c46291c443 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Wed, 17 Sep 2025 19:05:13 -0700 Subject: [PATCH 027/319] Add tests for font metrics and their estimators --- src/font/Collection.zig | 122 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index ad9590d70..c06358cbf 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1369,3 +1369,125 @@ test "adjusted sizes" { ); } } + +test "face metrics" { + const testing = std.testing; + const alloc = testing.allocator; + const narrowFont = font.embedded.cozette; + const wideFont = font.embedded.geist_mono; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = init(); + defer c.deinit(alloc); + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; + + const narrowIndex = try c.add(alloc, try .init( + lib, + narrowFont, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + const wideIndex = try c.add(alloc, try .init( + lib, + wideFont, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + + const narrowMetrics = (try c.getFace(narrowIndex)).getMetrics(); + const wideMetrics = (try c.getFace(wideIndex)).getMetrics(); + + // Verify provided/measured metrics. Measured + // values are backend-dependent due to hinting. + if (options.backend != .web_canvas) { + try std.testing.expectEqual(font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 8.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 7.3828125, + .web_canvas => unreachable, + }, + .ascent = 12.3046875, + .descent = -3.6953125, + .line_gap = 0.0, + .underline_position = -1.2265625, + .underline_thickness = 1.2265625, + .strikethrough_position = 6.15625, + .strikethrough_thickness = 1.234375, + .cap_height = 9.84375, + .ex_height = 7.3828125, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 18.0625, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 16.0, + .web_canvas => unreachable, + }, + }, narrowMetrics); + try std.testing.expectEqual(font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 10.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 9.6, + .web_canvas => unreachable, + }, + .ascent = 14.72, + .descent = -3.52, + .line_gap = 1.6, + .underline_position = -1.6, + .underline_thickness = 0.8, + .strikethrough_position = 4.24, + .strikethrough_thickness = 0.8, + .cap_height = 11.36, + .ex_height = 8.48, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 16.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 15.472000000000001, + .web_canvas => unreachable, + }, + }, wideMetrics); + } + + // Verify estimated metrics. icWidth() should equal the smaller of + // 2 * cell_width and ascii_height. For a narrow (wide) font, the + // smaller quantity is the former (latter). + try std.testing.expectEqual( + 2 * narrowMetrics.cell_width, + narrowMetrics.icWidth(), + ); + try std.testing.expectEqual( + wideMetrics.ascii_height, + wideMetrics.icWidth(), + ); +} From 32759036113a6c00c7e31d19d71050f43163df16 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 09:26:09 -0400 Subject: [PATCH 028/319] update uucode and cleanups --- build.zig.zon | 4 +- src/benchmark/GraphemeBreak.zig | 137 +------------------------------- src/build/uucode_config.zig | 114 ++++++++------------------ src/input/Binding.zig | 6 +- src/unicode/props.zig | 23 +++++- 5 files changed, 59 insertions(+), 225 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index f8566eb4e..2c8a8fd68 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,8 +37,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/a1833012b50197bdf7d43543e38f8be5c5a75016.tar.gz", - .hash = "uucode-0.0.0-ZZjBPu_RPwA01mgYvIKApZJX_JMUTKD2kyBsyYCdzfaz", + .url = "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz", + .hash = "uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index 2bed60df6..b3b169909 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -138,146 +138,13 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { } } -const GraphemeBoundaryClass = uucode.TypeOfX(.grapheme_boundary_class); - -const BreakState = enum(u3) { - default, - regional_indicator, - extended_pictographic, -}; - -fn computeGraphemeBoundaryClass( - gb1: GraphemeBoundaryClass, - gb2: GraphemeBoundaryClass, - state: *BreakState, -) bool { - // Set state back to default when `gb1` or `gb2` is not expected in sequence. - switch (state.*) { - .regional_indicator => { - if (gb1 != .regional_indicator or gb2 != .regional_indicator) { - state.* = .default; - } - }, - .extended_pictographic => { - switch (gb1) { - .extend, - .zwj, - .extended_pictographic, - => {}, - - else => state.* = .default, - } - - switch (gb2) { - .extend, - .zwj, - .extended_pictographic, - => {}, - - else => state.* = .default, - } - }, - .default => {}, - } - - // GB6: L x (L | V | LV | VT) - if (gb1 == .L) { - if (gb2 == .L or - gb2 == .V or - gb2 == .LV or - gb2 == .LVT) return false; - } - - // GB7: (LV | V) x (V | T) - if (gb1 == .LV or gb1 == .V) { - if (gb2 == .V or gb2 == .T) return false; - } - - // GB8: (LVT | T) x T - if (gb1 == .LVT or gb1 == .T) { - if (gb2 == .T) return false; - } - - // Handle GB9 (Extend | ZWJ) later, since it can also match the start of - // GB9c (Indic) and GB11 (Emoji ZWJ) - - // GB9a: SpacingMark - if (gb2 == .spacing_mark) return false; - - // GB9b: Prepend - if (gb1 == .prepend) return false; - - // GB11: Emoji ZWJ sequence - if (gb1 == .extended_pictographic) { - // start of sequence: - - // In normal operation, we'll be in this state, but - // precomputeGraphemeBreak iterates all states. - // std.debug.assert(state.* == .default); - - if (gb2 == .extend or gb2 == .zwj) { - state.* = .extended_pictographic; - return false; - } - // else, not an Emoji ZWJ sequence - } else if (state.* == .extended_pictographic) { - // continue or end sequence: - - if (gb1 == .extend and (gb2 == .extend or gb2 == .zwj)) { - // continue extend* ZWJ sequence - return false; - } else if (gb1 == .zwj and gb2 == .extended_pictographic) { - // ZWJ -> end of sequence - state.* = .default; - return false; - } else { - // Not a valid Emoji ZWJ sequence - state.* = .default; - } - } - - // GB12 and GB13: Regional Indicator - if (gb1 == .regional_indicator and gb2 == .regional_indicator) { - if (state.* == .default) { - state.* = .regional_indicator; - return false; - } else { - state.* = .default; - return true; - } - } - - // GB9: x (Extend | ZWJ) - if (gb2 == .extend or gb2 == .zwj) return false; - - // GB999: Otherwise, break everywhere - return true; -} - -pub fn isBreak( - cp1: u21, - cp2: u21, - state: *BreakState, -) bool { - const table = comptime uucode.grapheme.precomputeGraphemeBreak( - GraphemeBoundaryClass, - BreakState, - computeGraphemeBoundaryClass, - ); - const gb1 = uucode.getX(.grapheme_boundary_class, cp1); - const gb2 = uucode.getX(.grapheme_boundary_class, cp2); - const result = table.get(gb1, gb2, state.*); - state.* = result.state; - return result.result; -} - fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); var d: UTF8Decoder = .{}; - var state: BreakState = .default; + var state: uucode.grapheme.BreakState = .default; var cp1: u21 = 0; var buf: [4096]u8 = undefined; while (true) { @@ -291,7 +158,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp2| { - std.mem.doNotOptimizeAway(isBreak(cp1, @intCast(cp2), &state)); + std.mem.doNotOptimizeAway(uucode.grapheme.isBreak(cp1, @intCast(cp2), &state)); cp1 = cp2; } } diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index f2078eb7a..fcc50057e 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -1,96 +1,42 @@ +const std = @import("std"); const config = @import("config.zig"); const config_x = @import("config.x.zig"); const d = config.default; const wcwidth = config_x.wcwidth; +const Allocator = std.mem.Allocator; pub const log_level = .debug; -fn computeWidth(cp: u21, data: anytype, backing: anytype, tracking: anytype) void { +fn computeWidth( + alloc: std.mem.Allocator, + cp: u21, + data: anytype, + backing: anytype, + tracking: anytype, +) Allocator.Error!void { + _ = alloc; _ = cp; _ = backing; _ = tracking; - if (data.wcwidth < 0) { - data.width = 0; - } else if (data.wcwidth > 2) { - data.width = 2; - } else { - data.width = @intCast(data.wcwidth); - } + data.width = @intCast(@min(2, @max(0, data.wcwidth))); } -const width = config.Extension{ .inputs = &.{"wcwidth"}, .compute = &computeWidth, .fields = &.{ - .{ .name = "width", .type = u2 }, -} }; - -pub const GraphemeBoundaryClass = enum(u4) { - invalid, - L, - V, - T, - LV, - LVT, - prepend, - extend, - zwj, - spacing_mark, - regional_indicator, - extended_pictographic, - extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base} - emoji_modifier, // \p{Emoji_Modifier} -}; - -fn computeGraphemeBoundaryClass(cp: u21, data: anytype, backing: anytype, tracking: anytype) void { - _ = cp; - _ = backing; - _ = tracking; - if (data.is_emoji_modifier) { - data.grapheme_boundary_class = .emoji_modifier; - } else if (data.is_emoji_modifier_base) { - data.grapheme_boundary_class = .extended_pictographic_base; - } else { - data.grapheme_boundary_class = switch (data.grapheme_break) { - .extended_pictographic => .extended_pictographic, - .l => .L, - .v => .V, - .t => .T, - .lv => .LV, - .lvt => .LVT, - .prepend => .prepend, - .zwj => .zwj, - .spacing_mark => .spacing_mark, - .regional_indicator => .regional_indicator, - - .zwnj, - .indic_conjunct_break_extend, - .indic_conjunct_break_linker, - => .extend, - - // This is obviously not INVALID invalid, there is SOME grapheme - // boundary class for every codepoint. But we don't care about - // anything that doesn't fit into the above categories. - .other, - .indic_conjunct_break_consonant, - .cr, - .lf, - .control, - => .invalid, - }; - } -} - -const grapheme_boundary_class = config.Extension{ - .inputs = &.{ - "grapheme_break", - "is_emoji_modifier", - "is_emoji_modifier_base", - }, - .compute = &computeGraphemeBoundaryClass, +const width = config.Extension{ + .inputs = &.{"wcwidth"}, + .compute = &computeWidth, .fields = &.{ - .{ .name = "grapheme_boundary_class", .type = GraphemeBoundaryClass }, + .{ .name = "width", .type = u2 }, }, }; -fn computeIsSymbol(cp: u21, data: anytype, backing: anytype, tracking: anytype) void { +fn computeIsSymbol( + alloc: Allocator, + cp: u21, + data: anytype, + backing: anytype, + tracking: anytype, +) Allocator.Error!void { + _ = alloc; _ = cp; _ = backing; _ = tracking; @@ -117,24 +63,26 @@ pub const tables = [_]config.Table{ .{ .extensions = &.{wcwidth}, .fields = &.{ - wcwidth.field("wcwidth"), - d.field("general_category"), - d.field("block"), d.field("is_emoji_presentation"), d.field("case_folding_full"), // Alternative: // d.field("case_folding_simple"), d.field("is_emoji_modifier"), d.field("is_emoji_modifier_base"), - d.field("grapheme_break"), }, }, .{ .stages = .two, - .extensions = &.{ wcwidth, width, grapheme_boundary_class }, + .extensions = &.{ wcwidth, width }, .fields = &.{ width.field("width"), - grapheme_boundary_class.field("grapheme_boundary_class"), + }, + }, + .{ + .stages = .two, + .extensions = &.{}, + .fields = &.{ + d.field("grapheme_break"), }, }, .{ diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 8dc7e3ddf..039a6ac89 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1629,7 +1629,11 @@ pub const Trigger = struct { // If more codepoints are produced then we return the codepoint // as-is which isn't correct but until we have a failing test // then I don't want to handle this. - return uucode.get(.case_folding_full, cp).array(cp); + var buffer: [1]u21 = undefined; + const slice = uucode.get(.case_folding_full, cp).with(&buffer, cp); + var array: [3]u21 = [_]u21{0} ** 3; + @memcpy(array[0..slice.len], slice); + return array; } /// Convert the trigger to a C API compatible trigger. diff --git a/src/unicode/props.zig b/src/unicode/props.zig index c209fe2d3..0c11f3dc9 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -61,7 +61,22 @@ pub const Properties = struct { /// Possible grapheme boundary classes. This isn't an exhaustive list: /// we omit control, CR, LF, etc. because in Ghostty's usage that are /// impossible because they're handled by the terminal. -pub const GraphemeBoundaryClass = uucode.TypeOfX(.grapheme_boundary_class); +pub const GraphemeBoundaryClass = enum(u4) { + invalid, + L, + V, + T, + LV, + LVT, + prepend, + extend, + zwj, + spacing_mark, + regional_indicator, + extended_pictographic, + extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base} + emoji_modifier, // \p{Emoji_Modifier} +}; /// Gets the grapheme boundary class for a codepoint. /// The use case for this is only in generating lookup tables. @@ -113,13 +128,13 @@ pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool { } pub fn get(cp: u21) Properties { - const wcwidth = if (cp > uucode.config.max_code_point) + const width = if (cp > uucode.config.max_code_point) 0 else - uucode.get(.wcwidth, cp); + uucode.getX(.width, cp); return .{ - .width = @intCast(@min(2, @max(0, wcwidth))), + .width = width, .grapheme_boundary_class = computeGraphemeBoundaryClass(cp), }; } From 285a33fbc0cdbefd250fe6448e4c9b41e14ba7b9 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 09:29:22 -0400 Subject: [PATCH 029/319] nix update and remove extra benchmark files --- bench.sh | 7 ------- benchmark-sources.txt | 14 -------------- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 6 files changed, 10 insertions(+), 31 deletions(-) delete mode 100755 bench.sh delete mode 100644 benchmark-sources.txt diff --git a/bench.sh b/bench.sh deleted file mode 100755 index 332469b97..000000000 --- a/bench.sh +++ /dev/null @@ -1,7 +0,0 @@ -bench=$1 -warmup=$2 -runs=$3 -data=$4 - -echo hyperfine --warmup $warmup --runs=$runs "zig-out/bin/ghostty-bench +$bench --data=$data --mode=uucode" "zig-out/bin/ghostty-bench-old +$bench --data=$data --mode=uucode" -hyperfine --warmup $warmup --runs=$runs "zig-out/bin/ghostty-bench +$bench --data=$data --mode=uucode" "zig-out/bin/ghostty-bench-old +$bench --data=$data --mode=uucode" diff --git a/benchmark-sources.txt b/benchmark-sources.txt deleted file mode 100644 index 06aaf17d4..000000000 --- a/benchmark-sources.txt +++ /dev/null @@ -1,14 +0,0 @@ -# https://chatgpt.com/share/68c2da5f-65c8-800f-b3e4-55cdff7150cb - -zig-out/bin/ghostty-gen +utf8 | head -c 200000000 > data.txt - -curl -O https://www.gutenberg.org/cache/epub/30/pg30.txt -curl -O https://www.gutenberg.org/cache/epub/24264/pg24264.txt # 紅樓夢 by Xueqin Cao - -# From https://linguatools.org/tools/corpora/wikipedia-monolingual-corpora/ - -curl -L "https://www.dropbox.com/scl/fi/86gjpfzopssavk2nzo69u/arwiki-20180920-corpus.xml.bz2?dl=1&e=1&file_subpath=%2Fdata&rlkey=dmjlaw1xegg8vsje4xrn040v8" | bzcat | head -c 1000000000 > arwiki-20180920-corpus.xml # arabic - -curl -L "https://www.dropbox.com/scl/fi/la1nvupgk2honb3n6m9zc/enwiki-20181001-corpus.xml.bz2?rlkey=8vg4vokbaijh1lg5lw3ytc864&e=1&dl=1" | bzcat | head -c 1000000000 > enwiki-20181001-corpus.xml # english - -curl -L "https://www.dropbox.com/scl/fi/vru4zxv5qff1klod9xiht/jawiki-20181001-corpus.xml.bz2?rlkey=utuuooiwyupws3x5517u8n8jl&e=1&dl=1" | bzcat | head -c 1000000000 > jawiki-20181001-corpus.xml # japanese diff --git a/build.zig.zon.json b/build.zig.zon.json index e1ce67047..7af90834f 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -109,10 +109,10 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "uucode-0.0.0-ZZjBPl_dPwC-BPhSJLID4Hs9O0zw-vZKGXdaOBFch8c8": { + "uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz", - "hash": "sha256-wTtlHjbl17xNeg67vNELNJs9lXX3wndV5+6dqZOEvbQ=" + "url": "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz", + "hash": "sha256-NFBH94kHmaxsFLBEePgdLjOt3JfbPn8cTQ1ZHiH6xBg=" }, "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { "name": "vaxis", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 932a46b99..aff14c289 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -259,11 +259,11 @@ in }; } { - name = "uucode-0.0.0-ZZjBPl_dPwC-BPhSJLID4Hs9O0zw-vZKGXdaOBFch8c8"; + name = "uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz"; - hash = "sha256-wTtlHjbl17xNeg67vNELNJs9lXX3wndV5+6dqZOEvbQ="; + url = "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz"; + hash = "sha256-NFBH94kHmaxsFLBEePgdLjOt3JfbPn8cTQ1ZHiH6xBg="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index ab8c81ff8..1ee2923e3 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23c https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz -https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz +https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 677d93868..ec2e72b9e 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -133,9 +133,9 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz", - "dest": "vendor/p/uucode-0.0.0-ZZjBPl_dPwC-BPhSJLID4Hs9O0zw-vZKGXdaOBFch8c8", - "sha256": "c13b651e36e5d7bc4d7a0ebbbcd10b349b3d9575f7c27755e7ee9da99384bdb4" + "url": "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz", + "dest": "vendor/p/uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J", + "sha256": "345047f7890799ac6c14b04478f81d2e33addc97db3e7f1c4d0d591e21fac418" }, { "type": "git", From 69594119c320920d7795214ef4cc4afa3699d3fa Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 11:46:05 -0400 Subject: [PATCH 030/319] fix up diff from benchmarks, and add tests against ziglyph --- build.zig | 8 ++ build.zig.zon | 9 +- build.zig.zon.json | 11 +- build.zig.zon.nix | 14 ++- build.zig.zon.txt | 3 +- flatpak/zig-packages.json | 12 +- src/benchmark/GraphemeBreak.zig | 2 +- src/benchmark/IsSymbol.zig | 9 +- src/build/SharedDeps.zig | 10 +- src/build/UnicodeTables.zig | 10 +- src/build/uucode_config.zig | 2 +- src/input/Binding.zig | 4 +- src/terminal/Terminal.zig | 2 +- src/unicode/grapheme.zig | 27 ++--- src/unicode/main.zig | 1 - src/unicode/props.zig | 203 ++++++++++++++++++++------------ src/unicode/symbols.zig | 61 +++++----- 17 files changed, 227 insertions(+), 161 deletions(-) diff --git a/build.zig b/build.zig index 38cfd0e56..61bcd575b 100644 --- a/build.zig +++ b/build.zig @@ -234,6 +234,14 @@ pub fn build(b: *std.Build) !void { if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe); + // Only need ziglyph for tests + if (b.lazyDependency("ziglyph", .{ + .target = test_exe.root_module.resolved_target.?, + .optimize = test_exe.root_module.optimize.?, + })) |dep| { + test_exe.root_module.addImport("ziglyph", dep.module("ziglyph")); + } + // Normal test running const test_run = b.addRunArtifact(test_exe); test_step.dependOn(&test_run.step); diff --git a/build.zig.zon b/build.zig.zon index 2c8a8fd68..953ec2f79 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -36,9 +36,14 @@ .hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", .lazy = true, }, + .ziglyph = .{ + .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", + .hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", + .lazy = true, + }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz", - .hash = "uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J", + .url = "https://github.com/jacobsandlund/uucode/archive/3512203ca991c02b2500392d1d51226c48131c99.tar.gz", + .hash = "uucode-0.0.0-ZZjBPgErQADBJsnLdcZKdRk94lB28CbKC4OrUDPOnSeV", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/build.zig.zon.json b/build.zig.zon.json index 7af90834f..1b2ccebe1 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -109,10 +109,10 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J": { + "uucode-0.0.0-ZZjBPgErQADBJsnLdcZKdRk94lB28CbKC4OrUDPOnSeV": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz", - "hash": "sha256-NFBH94kHmaxsFLBEePgdLjOt3JfbPn8cTQ1ZHiH6xBg=" + "url": "https://github.com/jacobsandlund/uucode/archive/3512203ca991c02b2500392d1d51226c48131c99.tar.gz", + "hash": "sha256-nbbeHgvkoMmr5DJN0qRF776hu3waTL85d8dGpvYsZBw=" }, "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { "name": "vaxis", @@ -169,6 +169,11 @@ "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d", "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=" }, + "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": { + "name": "ziglyph", + "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", + "hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=" + }, "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": { "name": "zlib", "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index aff14c289..2cedd8fba 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -259,11 +259,11 @@ in }; } { - name = "uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J"; + name = "uucode-0.0.0-ZZjBPgErQADBJsnLdcZKdRk94lB28CbKC4OrUDPOnSeV"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz"; - hash = "sha256-NFBH94kHmaxsFLBEePgdLjOt3JfbPn8cTQ1ZHiH6xBg="; + url = "https://github.com/jacobsandlund/uucode/archive/3512203ca991c02b2500392d1d51226c48131c99.tar.gz"; + hash = "sha256-nbbeHgvkoMmr5DJN0qRF776hu3waTL85d8dGpvYsZBw="; }; } { @@ -354,6 +354,14 @@ in hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; }; } + { + name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; + path = fetchZigArtifact { + name = "ziglyph"; + url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; + hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; + }; + } { name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; path = fetchZigArtifact { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 1ee2923e3..9a7dd59ba 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -26,8 +26,9 @@ https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d. https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz +https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz -https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz +https://github.com/jacobsandlund/uucode/archive/3512203ca991c02b2500392d1d51226c48131c99.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index ec2e72b9e..f43d2e9f7 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -133,9 +133,9 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/ef173d765bd756eeecf7ce89f93c4f70c9038ab6.tar.gz", - "dest": "vendor/p/uucode-0.0.0-ZZjBPtMqQABaVqHdy8MX_XwChpQyZBAGchp-1cPuiQ6J", - "sha256": "345047f7890799ac6c14b04478f81d2e33addc97db3e7f1c4d0d591e21fac418" + "url": "https://github.com/jacobsandlund/uucode/archive/3512203ca991c02b2500392d1d51226c48131c99.tar.gz", + "dest": "vendor/p/uucode-0.0.0-ZZjBPgErQADBJsnLdcZKdRk94lB28CbKC4OrUDPOnSeV", + "sha256": "9db6de1e0be4a0c9abe4324dd2a445efbea1bb7c1a4cbf3977c746a6f62c641c" }, { "type": "git", @@ -203,6 +203,12 @@ "commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d", "dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj" }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", + "dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", + "sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index b3b169909..28de82593 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -21,7 +21,7 @@ data_f: ?std.fs.File = null, pub const Options = struct { /// The type of codepoint width calculation to use. - mode: Mode = .noop, + mode: Mode = .table, /// The data to read as a filepath. If this is "-" then /// we will read stdin. If this is unset, then we will diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index 0997da41d..09b61fceb 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -128,14 +128,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - if (uucode.getX(.is_symbol, cp) != symbols.table.get(cp)) { - std.debug.panic("uucode and table disagree on codepoint {d}: uucode={}, table={}", .{ - cp, - uucode.getX(.is_symbol, cp), - symbols.table.get(cp), - }); - } - //std.mem.doNotOptimizeAway(symbols.table.get(cp)); + std.mem.doNotOptimizeAway(symbols.table.get(cp)); } } } diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index fd3f91d89..68f0fb64f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -15,13 +15,13 @@ help_strings: HelpStrings, metallib: ?*MetallibStep, unicode_tables: UnicodeTables, framedata: GhosttyFrameData, -uucode_tables_zig: std.Build.LazyPath, +uucode_tables: std.Build.LazyPath, /// Used to keep track of a list of file sources. pub const LazyPathList = std.ArrayList(std.Build.LazyPath); pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { - const uucode_tables_zig = blk: { + const uucode_tables = blk: { const uucode = b.dependency("uucode", .{ .build_config_path = b.path("src/build/uucode_config.zig"), }); @@ -32,9 +32,9 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { var result: SharedDeps = .{ .config = cfg, .help_strings = try .init(b, cfg), - .unicode_tables = try .init(b, uucode_tables_zig), + .unicode_tables = try .init(b, uucode_tables), .framedata = try .init(b), - .uucode_tables_zig = uucode_tables_zig, + .uucode_tables = uucode_tables, // Setup by retarget .options = undefined, @@ -423,7 +423,7 @@ pub fn add( if (b.lazyDependency("uucode", .{ .target = target, .optimize = optimize, - .@"tables.zig" = self.uucode_tables_zig, + .tables_path = self.uucode_tables, .build_config_path = b.path("src/build/uucode_config.zig"), })) |dep| { step.root_module.addImport("uucode", dep.module("uucode")); diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index a947ce137..4b5f6db99 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -11,7 +11,7 @@ symbols_exe: *std.Build.Step.Compile, props_output: std.Build.LazyPath, symbols_output: std.Build.LazyPath, -pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables { +pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables { const props_exe = b.addExecutable(.{ .name = "props-unigen", .root_module = b.createModule(.{ @@ -36,7 +36,7 @@ pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables if (b.lazyDependency("uucode", .{ .target = b.graph.host, - .@"tables.zig" = uucode_tables_zig, + .tables_path = uucode_tables, .build_config_path = b.path("src/build/uucode_config.zig"), })) |dep| { inline for (&.{ props_exe, symbols_exe }) |exe| { @@ -46,14 +46,12 @@ pub fn init(b: *std.Build, uucode_tables_zig: std.Build.LazyPath) !UnicodeTables const props_run = b.addRunArtifact(props_exe); const symbols_run = b.addRunArtifact(symbols_exe); - const props_output = props_run.addOutputFileArg("props_table.zig"); - const symbols_output = symbols_run.addOutputFileArg("symbols_table.zig"); return .{ .props_exe = props_exe, .symbols_exe = symbols_exe, - .props_output = props_output, - .symbols_output = symbols_output, + .props_output = props_run.captureStdOut(), + .symbols_output = symbols_run.captureStdOut(), }; } diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index fcc50057e..6e2e263bd 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -65,7 +65,7 @@ pub const tables = [_]config.Table{ .fields = &.{ d.field("is_emoji_presentation"), d.field("case_folding_full"), - // Alternative: + // TODO: Alternatively, use: // d.field("case_folding_simple"), d.field("is_emoji_modifier"), d.field("is_emoji_modifier_base"), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 039a6ac89..467dd5949 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1609,8 +1609,8 @@ pub const Trigger = struct { .unicode => |cp| std.hash.autoHash( hasher, foldedCodepoint(cp), - // Alternative, just use simple case folding, and delete - // `foldedCodepoint` below: + // TODO: Alternatively, just use simple case folding, and + // delete `foldedCodepoint` below: // uucode.get(.case_folding_simple, cp), ), } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 229b6e100..2d191077a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -345,7 +345,7 @@ pub fn print(self: *Terminal, c: u21) !void { if (c == 0xFE0F or c == 0xFE0E) { // This only applies to emoji const prev_props = unicode.getProperties(prev.cell.content.codepoint); - const emoji = unicode.isExtendedPictographic(prev_props.grapheme_boundary_class); + const emoji = prev_props.grapheme_boundary_class.isExtendedPictographic(); if (!emoji) return; switch (c) { diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index b0cb4ead9..f3edb58b2 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -2,7 +2,6 @@ const std = @import("std"); const props = @import("props.zig"); const GraphemeBoundaryClass = props.GraphemeBoundaryClass; const table = props.table; -const isExtendedPictographic = props.isExtendedPictographic; /// Determines if there is a grapheme break between two codepoints. This /// must be called sequentially maintaining the state between calls. @@ -81,7 +80,7 @@ fn graphemeBreakClass( state: *BreakState, ) bool { // GB11: Emoji Extend* ZWJ x Emoji - if (!state.extended_pictographic and isExtendedPictographic(gbc1)) { + if (!state.extended_pictographic and gbc1.isExtendedPictographic()) { state.extended_pictographic = true; } @@ -132,7 +131,7 @@ fn graphemeBreakClass( // GB11: Emoji Extend* ZWJ x Emoji if (state.extended_pictographic and gbc1 == .zwj and - isExtendedPictographic(gbc2)) + gbc2.isExtendedPictographic()) { state.extended_pictographic = false; return false; @@ -156,38 +155,36 @@ fn graphemeBreakClass( /// TODO: this is hard to build with newer zig build, so /// https://github.com/ghostty-org/ghostty/pull/7806 took the approach of /// adding a `-Demit-unicode-test` option for `zig build`, but that -/// hasn't been done here yet. -/// TODO: this also still uses `ziglyph`, but could be switched to use -/// `uucode`'s grapheme break once that is implemented. +/// hasn't been done here. pub fn main() !void { - const ziglyph = @import("ziglyph"); + const uucode = @import("uucode"); // Set the min and max to control the test range. const min = 0; const max = std.math.maxInt(u21) + 1; var state: BreakState = .{}; - var zg_state: u3 = 0; + var uu_state: uucode.grapheme.BreakState = .default; for (min..max) |cp1| { if (cp1 % 1000 == 0) std.log.warn("progress cp1={}", .{cp1}); if (cp1 == '\r' or cp1 == '\n' or - ziglyph.grapheme_break.isControl(@intCast(cp1))) continue; + uucode.get(.grapheme_break, @intCast(cp1)) == .control) continue; for (min..max) |cp2| { if (cp2 == '\r' or cp2 == '\n' or - ziglyph.grapheme_break.isControl(@intCast(cp2))) continue; + uucode.get(.grapheme_break, @intCast(cp1)) == .control) continue; const gb = graphemeBreak(@intCast(cp1), @intCast(cp2), &state); - const zg_gb = ziglyph.graphemeBreak(@intCast(cp1), @intCast(cp2), &zg_state); - if (gb != zg_gb) { - std.log.warn("cp1={x} cp2={x} gb={} state={} zg_gb={} zg_state={}", .{ + const uu_gb = uucode.grapheme.isBreak(@intCast(cp1), @intCast(cp2), &uu_state); + if (gb != uu_gb) { + std.log.warn("cp1={x} cp2={x} gb={} state={} uu_gb={} uu_state={}", .{ cp1, cp2, gb, state, - zg_gb, - zg_state, + uu_gb, + uu_state, }); } } diff --git a/src/unicode/main.zig b/src/unicode/main.zig index e053976bc..17c86deca 100644 --- a/src/unicode/main.zig +++ b/src/unicode/main.zig @@ -7,7 +7,6 @@ pub const Properties = props.Properties; pub const getProperties = props.get; pub const graphemeBreak = grapheme.graphemeBreak; pub const GraphemeBreakState = grapheme.BreakState; -pub const isExtendedPictographic = props.isExtendedPictographic; test { _ = @import("symbols.zig"); diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 0c11f3dc9..53493b2ff 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -76,66 +76,66 @@ pub const GraphemeBoundaryClass = enum(u4) { extended_pictographic, extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base} emoji_modifier, // \p{Emoji_Modifier} + + /// Gets the grapheme boundary class for a codepoint. + /// The use case for this is only in generating lookup tables. + pub fn init(cp: u21) GraphemeBoundaryClass { + if (cp > uucode.config.max_code_point) return .invalid; + if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; + if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; + + return switch (uucode.get(.grapheme_break, cp)) { + .extended_pictographic => .extended_pictographic, + .l => .L, + .v => .V, + .t => .T, + .lv => .LV, + .lvt => .LVT, + .prepend => .prepend, + .zwj => .zwj, + .spacing_mark => .spacing_mark, + .regional_indicator => .regional_indicator, + + .zwnj, + .indic_conjunct_break_extend, + .indic_conjunct_break_linker, + => .extend, + + // This is obviously not INVALID invalid, there is SOME grapheme + // boundary class for every codepoint. But we don't care about + // anything that doesn't fit into the above categories. + .other, + .indic_conjunct_break_consonant, + .cr, + .lf, + .control, + => .invalid, + }; + } + + /// Returns true if this is an extended pictographic type. This + /// should be used instead of comparing the enum value directly + /// because we classify multiple. + pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool { + return switch (self) { + .extended_pictographic, + .extended_pictographic_base, + => true, + + else => false, + }; + } }; -/// Gets the grapheme boundary class for a codepoint. -/// The use case for this is only in generating lookup tables. -fn computeGraphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { - if (cp > uucode.config.max_code_point) return .invalid; - if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; - if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; - - return switch (uucode.get(.grapheme_break, cp)) { - .extended_pictographic => .extended_pictographic, - .l => .L, - .v => .V, - .t => .T, - .lv => .LV, - .lvt => .LVT, - .prepend => .prepend, - .zwj => .zwj, - .spacing_mark => .spacing_mark, - .regional_indicator => .regional_indicator, - - .zwnj, - .indic_conjunct_break_extend, - .indic_conjunct_break_linker, - => .extend, - - // This is obviously not INVALID invalid, there is SOME grapheme - // boundary class for every codepoint. But we don't care about - // anything that doesn't fit into the above categories. - .other, - .indic_conjunct_break_consonant, - .cr, - .lf, - .control, - => .invalid, - }; -} - -/// Returns true if this is an extended pictographic type. This -/// should be used instead of comparing the enum value directly -/// because we classify multiple. -pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool { - return switch (self) { - .extended_pictographic, - .extended_pictographic_base, - => true, - - else => false, - }; -} - pub fn get(cp: u21) Properties { const width = if (cp > uucode.config.max_code_point) - 0 + 1 else uucode.getX(.width, cp); return .{ .width = width, - .grapheme_boundary_class = computeGraphemeBoundaryClass(cp), + .grapheme_boundary_class = .init(cp), }; } @@ -145,13 +145,6 @@ pub fn main() !void { defer arena_state.deinit(); const alloc = arena_state.allocator(); - var args_iter = try std.process.argsWithAllocator(alloc); - defer args_iter.deinit(); - _ = args_iter.skip(); // Skip program name - - const output_path = args_iter.next() orelse std.debug.panic("No output file arg for props exe!", .{}); - std.debug.print("Unicode props_table output_path = {s}\n", .{output_path}); - const gen: lut.Generator( Properties, struct { @@ -171,10 +164,7 @@ pub fn main() !void { defer alloc.free(t.stage1); defer alloc.free(t.stage2); defer alloc.free(t.stage3); - var out_file = try std.fs.cwd().createFile(output_path, .{}); - defer out_file.close(); - const writer = out_file.writer(); - try t.writeZig(writer); + try t.writeZig(std.io.getStdOut().writer()); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ @@ -186,17 +176,78 @@ pub fn main() !void { // This is not very fast in debug modes, so its commented by default. // IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES. -// test "unicode props: tables match uucode" { -// const testing = std.testing; -// -// const min = 0xFF + 1; // start outside ascii -// const max = std.math.maxInt(u21) + 1; -// for (min..max) |cp| { -// const t = table.get(@intCast(cp)); -// const uu = @min(2, @max(0, uucode.get(.wcwidth, @intCast(cp)))); -// if (t.width != uu) { -// std.log.warn("mismatch cp=U+{x} t={} uucode={}", .{ cp, t, uu }); -// try testing.expect(false); -// } -// } -//} +test "unicode props: tables match uucode" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + + const min = 0xFF + 1; // start outside ascii + const max = std.math.maxInt(u21) + 1; + for (min..max) |cp| { + const t = table.get(@intCast(cp)); + const uu = if (cp > uucode.config.max_code_point) + 1 + else + uucode.getX(.width, @intCast(cp)); + if (t.width != uu) { + std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t.width, uu }); + try testing.expect(false); + } + } +} + +test "unicode props: tables match ziglyph" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const ziglyph = @import("ziglyph"); + const testing = std.testing; + + const min = 0xFF + 1; // start outside ascii + const max = std.math.maxInt(u21) + 1; + for (min..max) |cp| { + const t = table.get(@intCast(cp)); + const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half))); + if (t.width != zg) { + + // Known exceptions + if (cp == 0x0897) continue; // non-spacing mark (t = 0) + if (cp == 0x2065) continue; // unassigned (t = 1) + if (cp >= 0x2630 and cp <= 0x2637) continue; // east asian width is wide (t = 2) + if (cp >= 0x268A and cp <= 0x268F) continue; // east asian width is wide (t = 2) + if (cp >= 0x2FFC and cp <= 0x2FFF) continue; // east asian width is wide (t = 2) + if (cp == 0x31E4 or cp == 0x31E5) continue; // east asian width is wide (t = 2) + if (cp == 0x31EF) continue; // east asian width is wide (t = 2) + if (cp >= 0x4DC0 and cp <= 0x4DFF) continue; // east asian width is wide (t = 2) + if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) + if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) + if (cp >= 0x10D69 and cp <= 0x10D6D) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0x10EFC and cp <= 0x10EFF) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0x113BB and cp <= 0x113C0) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113CE) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113D0) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113D2) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113E1) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113E2) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1171E) continue; // mark spacing combining (t = 1) + if (cp == 0x11F5A) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1611E) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1611F) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0x16120 and cp <= 0x1612F) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0xE0000 and cp <= 0xE0FFF) continue; // ziglyph ignores these with 0, but many are unassigned (t = 1) + if (cp == 0x18CFF) continue; // east asian width is wide (t = 2) + if (cp >= 0x1D300 and cp <= 0x1D376) continue; // east asian width is wide (t = 2) + if (cp == 0x1E5EE) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1E5EF) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1FA89) continue; // east asian width is wide (t = 2) + if (cp == 0x1FA8F) continue; // east asian width is wide (t = 2) + if (cp == 0x1FABE) continue; // east asian width is wide (t = 2) + if (cp == 0x1FAC6) continue; // east asian width is wide (t = 2) + if (cp == 0x1FADC) continue; // east asian width is wide (t = 2) + if (cp == 0x1FADF) continue; // east asian width is wide (t = 2) + if (cp == 0x1FAE9) continue; // east asian width is wide (t = 2) + + std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t.width, zg }); + try testing.expect(false); + } + } +} diff --git a/src/unicode/symbols.zig b/src/unicode/symbols.zig index 8150d279f..e5c09a7b0 100644 --- a/src/unicode/symbols.zig +++ b/src/unicode/symbols.zig @@ -17,37 +17,12 @@ pub const table = table: { }; }; -/// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area and anything -/// in several unicode blocks: -/// - Dingbats -/// - Emoticons -/// - Miscellaneous Symbols -/// - Enclosed Alphanumerics -/// - Enclosed Alphanumeric Supplement -/// - Miscellaneous Symbols and Pictographs -/// - Transport and Map Symbols -/// -/// In the future it may be prudent to expand this to encompass more -/// symbol-like characters, and/or exclude some PUA sections. -pub fn isSymbol(cp: u21) bool { - // TODO: probably can remove this method and just call uucode directly - return uucode.getX(.is_symbol, cp); -} - /// Runnable binary to generate the lookup tables and output to stdout. pub fn main() !void { var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena_state.deinit(); const alloc = arena_state.allocator(); - var args_iter = try std.process.argsWithAllocator(alloc); - defer args_iter.deinit(); - _ = args_iter.skip(); // Skip program name - - const output_path = args_iter.next() orelse std.debug.panic("No output file arg for symbols exe!", .{}); - std.debug.print("Unicode symbols_table output_path = {s}\n", .{output_path}); - const gen: lut.Generator( bool, struct { @@ -56,7 +31,7 @@ pub fn main() !void { return if (cp > uucode.config.max_code_point) false else - isSymbol(@intCast(cp)); + uucode.getX(.is_symbol, @intCast(cp)); } pub fn eql(ctx: @This(), a: bool, b: bool) bool { @@ -70,10 +45,7 @@ pub fn main() !void { defer alloc.free(t.stage1); defer alloc.free(t.stage2); defer alloc.free(t.stage3); - var out_file = try std.fs.cwd().createFile(output_path, .{}); - defer out_file.close(); - const writer = out_file.writer(); - try t.writeZig(writer); + try t.writeZig(std.io.getStdOut().writer()); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ @@ -83,8 +55,6 @@ pub fn main() !void { // }); } -// This is not very fast in debug modes, so its commented by default. -// IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CHANGES. test "unicode symbols: tables match uucode" { if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; @@ -95,7 +65,7 @@ test "unicode symbols: tables match uucode" { const uu = if (cp > uucode.config.max_code_point) false else - isSymbol(@intCast(cp)); + uucode.getX(.is_symbol, @intCast(cp)); if (t != uu) { std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t, uu }); @@ -103,3 +73,28 @@ test "unicode symbols: tables match uucode" { } } } + +test "unicode symbols: tables match ziglyph" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const ziglyph = @import("ziglyph"); + const testing = std.testing; + + for (0..std.math.maxInt(u21)) |cp_usize| { + const cp: u21 = @intCast(cp_usize); + const t = table.get(cp); + const zg = ziglyph.general_category.isPrivateUse(cp) or + ziglyph.blocks.isDingbats(cp) or + ziglyph.blocks.isEmoticons(cp) or + ziglyph.blocks.isMiscellaneousSymbols(cp) or + ziglyph.blocks.isEnclosedAlphanumerics(cp) or + ziglyph.blocks.isEnclosedAlphanumericSupplement(cp) or + ziglyph.blocks.isMiscellaneousSymbolsAndPictographs(cp) or + ziglyph.blocks.isTransportAndMapSymbols(cp); + + if (t != zg) { + std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); + try testing.expect(false); + } + } +} From bb607e0999f35cf24b31d5c861fd16414130c94f Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 18 Sep 2025 09:08:08 -0700 Subject: [PATCH 031/319] Refactor load flags into a function --- src/font/face/freetype.zig | 57 +++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index c448d2735..e3d4b34cc 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -351,26 +351,16 @@ pub const Face = struct { return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA; } - /// Render a glyph using the glyph index. The rendered glyph is stored in the - /// given texture atlas. - pub fn renderGlyph( - self: Face, - alloc: Allocator, - atlas: *font.Atlas, - glyph_index: u32, - opts: font.face.RenderOptions, - ) !Glyph { - self.ft_mutex.lock(); - defer self.ft_mutex.unlock(); - + /// Set the load flags to use when loading a glyph for measurement or + /// rendering. + fn glyphLoadFlags(self: Face, constrained: bool) freetype.LoadFlags { // Hinting should only be enabled if the configured load flags specify // it and the provided constraint doesn't actually do anything, since // if it does, then it'll mess up the hinting anyway when it moves or // resizes the glyph. - const do_hinting = self.load_flags.hinting and !opts.constraint.doesAnything(); + const do_hinting = self.load_flags.hinting and !constrained; - // Load the glyph. - try self.face.loadGlyph(glyph_index, .{ + return .{ // If our glyph has color, we want to render the color .color = self.face.hasColor(), @@ -392,7 +382,23 @@ pub const Face = struct { // SVG glyphs under FreeType, since that requires bundling another // dependency to handle rendering the SVG. .no_svg = true, - }); + }; + } + + /// Render a glyph using the glyph index. The rendered glyph is stored in the + /// given texture atlas. + pub fn renderGlyph( + self: Face, + alloc: Allocator, + atlas: *font.Atlas, + glyph_index: u32, + opts: font.face.RenderOptions, + ) !Glyph { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + // Load the glyph. + try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything())); const glyph = self.face.handle.*.glyph; // We get a rect that represents the position @@ -956,17 +962,6 @@ pub const Face = struct { break :st .{ pos, thick }; }; - // Set the load flags to use when measuring glyphs. For consistency, we - // use same hinting settings as when rendering for consistency. - const measurement_load_flags: freetype.LoadFlags = .{ - .render = false, - .no_hinting = !self.load_flags.hinting, - .force_autohint = self.load_flags.@"force-autohint", - .no_autohint = !self.load_flags.autohint, - .target_mono = self.load_flags.monochrome, - .no_svg = true, - }; - // Cell width is calculated by calculating the widest width of the // visible ASCII characters. Usually 'M' is widest but we just take // whatever is widest. @@ -987,7 +982,7 @@ pub const Face = struct { var c: u8 = ' '; while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { - if (face.loadGlyph(glyph_index, measurement_load_flags)) { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { const glyph = face.handle.*.glyph; max = @max( f26dot6ToF64(glyph.*.advance.x), @@ -1046,7 +1041,7 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { - if (face.loadGlyph(glyph_index, measurement_load_flags)) { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { const glyph = face.handle.*.glyph; // We use the outline's bbox instead of the built-in // metrics for better accuracy (see renderGlyph()). @@ -1064,7 +1059,7 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { - if (face.loadGlyph(glyph_index, measurement_load_flags)) { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { const glyph = face.handle.*.glyph; // We use the outline's bbox instead of the built-in // metrics for better accuracy (see renderGlyph()). @@ -1088,7 +1083,7 @@ pub const Face = struct { const glyph = face.getCharIndex('水') orelse break :ic_width null; - face.loadGlyph(glyph, measurement_load_flags) catch break :ic_width null; + face.loadGlyph(glyph, self.glyphLoadFlags(false)) catch break :ic_width null; const ft_glyph = face.handle.*.glyph; From 18e9989f63da95a730a0bf2bab9b5ae39dc9c71b Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 14:20:41 -0400 Subject: [PATCH 032/319] forgot to align buf --- src/benchmark/GraphemeBreak.zig | 2 +- src/benchmark/TerminalParser.zig | 2 +- src/benchmark/TerminalStream.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index 28de82593..e576c71ef 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -146,7 +146,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { var d: UTF8Decoder = .{}; var state: uucode.grapheme.BreakState = .default; var cp1: u21 = 0; - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); diff --git a/src/benchmark/TerminalParser.zig b/src/benchmark/TerminalParser.zig index 002af4831..3065c1ed6 100644 --- a/src/benchmark/TerminalParser.zig +++ b/src/benchmark/TerminalParser.zig @@ -79,7 +79,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { var p: terminalpkg.Parser = .init(); - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 28a95226c..71ab1fdfc 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -115,7 +115,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var r = std.io.bufferedReader(f.reader()); - var buf: [4096]u8 = undefined; + var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { const n = r.read(&buf) catch |err| { log.warn("error reading data file err={}", .{err}); From 83f387d735b790c533b5f14bfef4c60de1701a85 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 14:21:39 -0400 Subject: [PATCH 033/319] default log level --- src/build/uucode_config.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 6e2e263bd..7179e2010 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -5,7 +5,6 @@ const d = config.default; const wcwidth = config_x.wcwidth; const Allocator = std.mem.Allocator; -pub const log_level = .debug; fn computeWidth( alloc: std.mem.Allocator, From 6bd5da7354c1bae91aad7f0d321a68bd526d80f9 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 14:24:24 -0400 Subject: [PATCH 034/319] update commented out test --- src/simd/codepoint_width.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/simd/codepoint_width.zig b/src/simd/codepoint_width.zig index 008c7ad9f..8d7ad456b 100644 --- a/src/simd/codepoint_width.zig +++ b/src/simd/codepoint_width.zig @@ -32,7 +32,10 @@ test "codepointWidth basic" { // const max = std.math.maxInt(u21) + 1; // for (min..max) |cp| { // const simd = codepointWidth(@intCast(cp)); -// const uu = @min(2, @max(0, uucode.get(.wcwidth, @intCast(cp)))); +// const uu = if (cp > uucode.config.max_code_point) +// 1 +// else +// uucode.getX(.width, @intCast(cp)); // if (simd != uu) mismatch: { // if (cp == 0x2E3B) { // try testing.expectEqual(@as(i8, 2), simd); From b83315cb810e1cab3dc708c83c1e8a644e84acda Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 18 Sep 2025 14:26:04 -0400 Subject: [PATCH 035/319] set max for unicode grapheme executable --- src/unicode/grapheme.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index f3edb58b2..a028e5690 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -161,7 +161,7 @@ pub fn main() !void { // Set the min and max to control the test range. const min = 0; - const max = std.math.maxInt(u21) + 1; + const max = uucode.config.max_code_point + 1; var state: BreakState = .{}; var uu_state: uucode.grapheme.BreakState = .default; From 4af4e18725b7cdfd3632bcc7eabd5a82c465ea55 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 18 Sep 2025 12:34:32 -0700 Subject: [PATCH 036/319] Use approximate equality for float comparisons --- src/font/Collection.zig | 173 +++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 71 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index c06358cbf..5a66749d6 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1371,6 +1371,9 @@ test "adjusted sizes" { } test "face metrics" { + // The web canvas backend doesn't calculate face metrics, only cell metrics + if (options.backend != .web_canvas) return error.SkipZigTest; + const testing = std.testing; const alloc = testing.allocator; const narrowFont = font.embedded.cozette; @@ -1403,80 +1406,108 @@ test "face metrics" { .size_adjustment = .none, }); - const narrowMetrics = (try c.getFace(narrowIndex)).getMetrics(); - const wideMetrics = (try c.getFace(wideIndex)).getMetrics(); + const narrowMetrics: font.Metrics.FaceMetrics = (try c.getFace(narrowIndex)).getMetrics(); + const wideMetrics: font.Metrics.FaceMetrics = (try c.getFace(wideIndex)).getMetrics(); // Verify provided/measured metrics. Measured // values are backend-dependent due to hinting. - if (options.backend != .web_canvas) { - try std.testing.expectEqual(font.Metrics.FaceMetrics{ - .px_per_em = 16.0, - .cell_width = switch (options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => 8.0, - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - => 7.3828125, - .web_canvas => unreachable, - }, - .ascent = 12.3046875, - .descent = -3.6953125, - .line_gap = 0.0, - .underline_position = -1.2265625, - .underline_thickness = 1.2265625, - .strikethrough_position = 6.15625, - .strikethrough_thickness = 1.234375, - .cap_height = 9.84375, - .ex_height = 7.3828125, - .ascii_height = switch (options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => 18.0625, - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - => 16.0, - .web_canvas => unreachable, - }, - }, narrowMetrics); - try std.testing.expectEqual(font.Metrics.FaceMetrics{ - .px_per_em = 16.0, - .cell_width = switch (options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => 10.0, - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - => 9.6, - .web_canvas => unreachable, - }, - .ascent = 14.72, - .descent = -3.52, - .line_gap = 1.6, - .underline_position = -1.6, - .underline_thickness = 0.8, - .strikethrough_position = 4.24, - .strikethrough_thickness = 0.8, - .cap_height = 11.36, - .ex_height = 8.48, - .ascii_height = switch (options.backend) { - .freetype, - .fontconfig_freetype, - .coretext_freetype, - => 16.0, - .coretext, - .coretext_harfbuzz, - .coretext_noshape, - => 15.472000000000001, - .web_canvas => unreachable, - }, - }, wideMetrics); + const narrowMetricsExpected = font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 8.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 7.3828125, + .web_canvas => unreachable, + }, + .ascent = 12.3046875, + .descent = -3.6953125, + .line_gap = 0.0, + .underline_position = -1.2265625, + .underline_thickness = 1.2265625, + .strikethrough_position = 6.15625, + .strikethrough_thickness = 1.234375, + .cap_height = 9.84375, + .ex_height = 7.3828125, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 18.0625, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 16.0, + .web_canvas => unreachable, + }, + }; + const wideMetricsExpected = font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 10.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 9.6, + .web_canvas => unreachable, + }, + .ascent = 14.72, + .descent = -3.52, + .line_gap = 1.6, + .underline_position = -1.6, + .underline_thickness = 0.8, + .strikethrough_position = 4.24, + .strikethrough_thickness = 0.8, + .cap_height = 11.36, + .ex_height = 8.48, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 16.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 15.472000000000001, + .web_canvas => unreachable, + }, + }; + + inline for ( + .{ narrowMetricsExpected, wideMetricsExpected }, + .{ narrowMetrics, wideMetrics }, + ) |metricsExpected, metricsActual| { + inline for (@typeInfo(font.Metrics.FaceMetrics).@"struct".fields) |field| { + const expected = @field(metricsExpected, field.name); + const actual = @field(metricsActual, field.name); + // Unwrap optional fields + const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) { + .optional => |Tinfo| { + if (expected) |expectedValue| { + const actualValue = actual orelse std.math.nan(Tinfo.child); + break :unwrap .{ expectedValue, actualValue }; + } + // Null values can be compared directly + try std.testing.expectEqual(expected, actual); + continue; + }, + else => break :unwrap .{ expected, actual }, + }; + // All non-null values are floats + const eps = std.math.floatEps(@TypeOf(actualValue - expectedValue)); + try std.testing.expectApproxEqRel( + expectedValue, + actualValue, + std.math.sqrt(eps), + ); + } } // Verify estimated metrics. icWidth() should equal the smaller of From 8fe9c579ef945228ccd4f604d23fd6670890cbfb Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 18 Sep 2025 12:39:19 -0700 Subject: [PATCH 037/319] Drop the nan sentinel; just fall through instead --- src/font/Collection.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 5a66749d6..0ab353a02 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1489,11 +1489,10 @@ test "face metrics" { const actual = @field(metricsActual, field.name); // Unwrap optional fields const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) { - .optional => |Tinfo| { - if (expected) |expectedValue| { - const actualValue = actual orelse std.math.nan(Tinfo.child); + .optional => { + if (expected) |expectedValue| if (actual) |actualValue| { break :unwrap .{ expectedValue, actualValue }; - } + }; // Null values can be compared directly try std.testing.expectEqual(expected, actual); continue; From 333a32208e2988661f76cd04d6680ffcd4e0f575 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 18 Sep 2025 14:01:00 -0700 Subject: [PATCH 038/319] Factor out glyph rect function --- src/font/face/freetype.zig | 120 ++++++++++++------------------------- 1 file changed, 39 insertions(+), 81 deletions(-) diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index e3d4b34cc..0dc4c4c03 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -385,6 +385,34 @@ pub const Face = struct { }; } + /// Get a rect that represents the position and size of the loaded glyph. + fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.RenderOptions.Constraint.GlyphSize { + // If we're dealing with an outline glyph then we get the + // outline's bounding box instead of using the built-in + // metrics, since that's more precise and allows better + // cell-fitting. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + // Get the glyph's bounding box before we transform it at all. + // We use this rather than the metrics, since it's more precise. + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + + return .{ + .x = f26dot6ToF64(bbox.xMin), + .y = f26dot6ToF64(bbox.yMin), + .width = f26dot6ToF64(bbox.xMax - bbox.xMin), + .height = f26dot6ToF64(bbox.yMax - bbox.yMin), + }; + } + + return .{ + .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), + .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), + .width = f26dot6ToF64(glyph.*.metrics.width), + .height = f26dot6ToF64(glyph.*.metrics.height), + }; + } + /// Render a glyph using the glyph index. The rendered glyph is stored in the /// given texture atlas. pub fn renderGlyph( @@ -403,37 +431,7 @@ pub const Face = struct { // We get a rect that represents the position // and size of the glyph before any changes. - const rect: struct { - x: f64, - y: f64, - width: f64, - height: f64, - } = metrics: { - // If we're dealing with an outline glyph then we get the - // outline's bounding box instead of using the built-in - // metrics, since that's more precise and allows better - // cell-fitting. - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - // Get the glyph's bounding box before we transform it at all. - // We use this rather than the metrics, since it's more precise. - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); - - break :metrics .{ - .x = f26dot6ToF64(bbox.xMin), - .y = f26dot6ToF64(bbox.yMin), - .width = f26dot6ToF64(bbox.xMax - bbox.xMin), - .height = f26dot6ToF64(bbox.yMax - bbox.yMin), - }; - } - - break :metrics .{ - .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), - .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), - .width = f26dot6ToF64(glyph.*.metrics.width), - .height = f26dot6ToF64(glyph.*.metrics.height), - }; - }; + const rect = getGlyphSize(glyph); // If our glyph is smaller than a quarter pixel in either axis // then it has no outlines or they're too small to render. @@ -988,21 +986,9 @@ pub const Face = struct { f26dot6ToF64(glyph.*.advance.x), max, ); - // We use the outline's bbox instead of the built-in - // metrics for better accuracy (see renderGlyph()). - const ymin, const ymax = metrics: { - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); - break :metrics .{ bbox.yMin, bbox.yMax }; - } - break :metrics .{ - glyph.*.metrics.horiBearingY - glyph.*.metrics.height, - glyph.*.metrics.horiBearingY, - }; - }; - top = @max(f26dot6ToF64(ymax), top); - bottom = @min(f26dot6ToF64(ymin), bottom); + const rect = getGlyphSize(glyph); + top = @max(rect.y + rect.height, top); + bottom = @min(rect.y, bottom); } else |_| {} } } @@ -1042,15 +1028,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { - const glyph = face.handle.*.glyph; - // We use the outline's bbox instead of the built-in - // metrics for better accuracy (see renderGlyph()). - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); - break :cap f26dot6ToF64(bbox.yMax - bbox.yMin); - } - break :cap f26dot6ToF64(glyph.*.metrics.height); + break :cap getGlyphSize(face.handle.*.glyph).height; } else |_| {} } break :cap null; @@ -1060,15 +1038,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { - const glyph = face.handle.*.glyph; - // We use the outline's bbox instead of the built-in - // metrics for better accuracy (see renderGlyph()). - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); - break :ex f26dot6ToF64(bbox.yMax - bbox.yMin); - } - break :ex f26dot6ToF64(glyph.*.metrics.height); + break :ex getGlyphSize(face.handle.*.glyph).height; } else |_| {} } break :ex null; @@ -1095,31 +1065,19 @@ pub const Face = struct { // This can sometimes happen if there's a CJK font that has been // patched with the nerd fonts patcher and it butchers the advance // values so the advance ends up half the width of the actual glyph. - const ft_glyph_width = ft_glyph_width: { - // We use the outline's bbox instead of the built-in - // metrics for better accuracy (see renderGlyph()). - if (ft_glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&ft_glyph.*.outline, &bbox); - break :ft_glyph_width bbox.xMax - bbox.xMin; - } - break :ft_glyph_width ft_glyph.*.metrics.width; - }; - if (ft_glyph_width > ft_glyph.*.advance.x) { + const ft_glyph_width = getGlyphSize(ft_glyph).width; + const advance = f26dot6ToF64(ft_glyph.*.advance.x); + if (ft_glyph_width > advance) { var buf: [1024]u8 = undefined; const font_name = self.name(&buf) catch ""; log.warn( "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.", - .{ - font_name, - f26dot6ToF64(ft_glyph_width), - f26dot6ToF64(ft_glyph.*.advance.x), - }, + .{ font_name, ft_glyph_width, advance }, ); break :ic_width null; } - break :ic_width f26dot6ToF64(ft_glyph.*.advance.x); + break :ic_width advance; }; return .{ From cf3b514efc1535636753107025adc488fa8d66ac Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Fri, 19 Sep 2025 01:24:13 -0400 Subject: [PATCH 039/319] pr feedback: `get`, remove todos for case_folding_simple --- build.zig.zon | 4 ++-- src/benchmark/CodepointWidth.zig | 2 +- src/benchmark/IsSymbol.zig | 2 +- src/build/uucode_config.zig | 6 ------ src/input/Binding.zig | 3 --- src/simd/codepoint_width.zig | 2 +- src/unicode/props.zig | 4 ++-- src/unicode/symbols.zig | 4 ++-- 8 files changed, 9 insertions(+), 18 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 953ec2f79..000d04fae 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/3512203ca991c02b2500392d1d51226c48131c99.tar.gz", - .hash = "uucode-0.0.0-ZZjBPgErQADBJsnLdcZKdRk94lB28CbKC4OrUDPOnSeV", + .url = "https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz", + .hash = "uucode-0.0.0-ZZjBPk0GQACuYIoFqT_Vzkvn8Ur_M3dE7o4DNUE65Z7v", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index ab3e31318..9bbc2def7 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -208,7 +208,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { std.mem.doNotOptimizeAway(if (cp <= 0xFF) 1 else - uucode.getX(.width, @intCast(cp))); + uucode.get(.width, @intCast(cp))); } } } diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index 09b61fceb..9e4e9e260 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -104,7 +104,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - std.mem.doNotOptimizeAway(uucode.getX(.is_symbol, cp)); + std.mem.doNotOptimizeAway(uucode.get(.is_symbol, cp)); } } } diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 7179e2010..de0549f3d 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -64,28 +64,22 @@ pub const tables = [_]config.Table{ .fields = &.{ d.field("is_emoji_presentation"), d.field("case_folding_full"), - // TODO: Alternatively, use: - // d.field("case_folding_simple"), d.field("is_emoji_modifier"), d.field("is_emoji_modifier_base"), }, }, .{ - .stages = .two, .extensions = &.{ wcwidth, width }, .fields = &.{ width.field("width"), }, }, .{ - .stages = .two, - .extensions = &.{}, .fields = &.{ d.field("grapheme_break"), }, }, .{ - .stages = .two, .extensions = &.{is_symbol}, .fields = &.{ is_symbol.field("is_symbol"), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 467dd5949..225ef0ae4 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1609,9 +1609,6 @@ pub const Trigger = struct { .unicode => |cp| std.hash.autoHash( hasher, foldedCodepoint(cp), - // TODO: Alternatively, just use simple case folding, and - // delete `foldedCodepoint` below: - // uucode.get(.case_folding_simple, cp), ), } std.hash.autoHash(hasher, self.mods.binding()); diff --git a/src/simd/codepoint_width.zig b/src/simd/codepoint_width.zig index 8d7ad456b..f5806948a 100644 --- a/src/simd/codepoint_width.zig +++ b/src/simd/codepoint_width.zig @@ -35,7 +35,7 @@ test "codepointWidth basic" { // const uu = if (cp > uucode.config.max_code_point) // 1 // else -// uucode.getX(.width, @intCast(cp)); +// uucode.get(.width, @intCast(cp)); // if (simd != uu) mismatch: { // if (cp == 0x2E3B) { // try testing.expectEqual(@as(i8, 2), simd); diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 53493b2ff..a32271f38 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -131,7 +131,7 @@ pub fn get(cp: u21) Properties { const width = if (cp > uucode.config.max_code_point) 1 else - uucode.getX(.width, cp); + uucode.get(.width, cp); return .{ .width = width, @@ -188,7 +188,7 @@ test "unicode props: tables match uucode" { const uu = if (cp > uucode.config.max_code_point) 1 else - uucode.getX(.width, @intCast(cp)); + uucode.get(.width, @intCast(cp)); if (t.width != uu) { std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t.width, uu }); try testing.expect(false); diff --git a/src/unicode/symbols.zig b/src/unicode/symbols.zig index e5c09a7b0..8ac0edcd3 100644 --- a/src/unicode/symbols.zig +++ b/src/unicode/symbols.zig @@ -31,7 +31,7 @@ pub fn main() !void { return if (cp > uucode.config.max_code_point) false else - uucode.getX(.is_symbol, @intCast(cp)); + uucode.get(.is_symbol, @intCast(cp)); } pub fn eql(ctx: @This(), a: bool, b: bool) bool { @@ -65,7 +65,7 @@ test "unicode symbols: tables match uucode" { const uu = if (cp > uucode.config.max_code_point) false else - uucode.getX(.is_symbol, @intCast(cp)); + uucode.get(.is_symbol, @intCast(cp)); if (t != uu) { std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t, uu }); From 7b0722bf16043fe7ee099e2fd8ca11c78c976bc5 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Fri, 19 Sep 2025 01:26:17 -0400 Subject: [PATCH 040/319] Remove comment above test. it's not too slow --- src/unicode/props.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/unicode/props.zig b/src/unicode/props.zig index a32271f38..7f3a3ece5 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -174,8 +174,6 @@ pub fn main() !void { // }); } -// This is not very fast in debug modes, so its commented by default. -// IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES. test "unicode props: tables match uucode" { if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; From 52ef17d4e0012e79b0f1db1d4119fbb17ce8d9bd Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 21 Sep 2025 11:04:32 -0700 Subject: [PATCH 041/319] Hoist `GlyphSize` out of nested scopes --- src/font/face.zig | 16 ++++++++-------- src/font/face/freetype.zig | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index 9da3c30f6..5eb84c898 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -93,6 +93,14 @@ pub const Variation = struct { }; }; +/// The size and position of a glyph. +pub const GlyphSize = struct { + width: f64, + height: f64, + x: f64, + y: f64, +}; + /// Additional options for rendering glyphs. pub const RenderOptions = struct { /// The metrics that are defining the grid layout. These are usually @@ -214,14 +222,6 @@ pub const RenderOptions = struct { icon, }; - /// The size and position of a glyph. - pub const GlyphSize = struct { - width: f64, - height: f64, - x: f64, - y: f64, - }; - /// Returns true if the constraint does anything. If it doesn't, /// because it neither sizes nor positions the glyph, then this /// returns false. diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 0dc4c4c03..0ccc84c44 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -386,7 +386,7 @@ pub const Face = struct { } /// Get a rect that represents the position and size of the loaded glyph. - fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.RenderOptions.Constraint.GlyphSize { + fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.GlyphSize { // If we're dealing with an outline glyph then we get the // outline's bounding box instead of using the built-in // metrics, since that's more precise and allows better From 13d44129bf39ff383a69b73fcf89ffeacc03a40e Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 21 Sep 2025 16:30:06 -0700 Subject: [PATCH 042/319] Add constraint width tests --- src/terminal/Screen.zig | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 67769923f..ab5aac3d4 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -14,6 +14,7 @@ const Selection = @import("Selection.zig"); const PageList = @import("PageList.zig"); const StringMap = @import("StringMap.zig"); const pagepkg = @import("page.zig"); +const cellpkg = @import("../renderer/cell.zig"); const point = @import("point.zig"); const size = @import("size.zig"); const style = @import("style.zig"); @@ -9094,3 +9095,97 @@ test "Screen UTF8 cell map with blank prefix" { .y = 1, }, cell_map.items[3]); } + +test "Screen cell constraint widths" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 4, 1, 0); + defer s.deinit(); + + // for each case, the numbers in the comment denote expected + // constraint widths for the symbol-containing cells + + // symbol->nothing: 2 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // symbol->character: 1 + { + try s.testWriteString("z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // symbol->space: 2 + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p0)); + s.reset(); + } + // symbol->no-break space: 1 + { + try s.testWriteString("\u{00a0}z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // symbol->end of row: 1 + { + try s.testWriteString(" "); + const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p3)); + s.reset(); + } + + // character->symbol: 2 + { + try s.testWriteString("z"); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p1)); + s.reset(); + } + + // symbol->symbol: 1,1 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + try testing.expectEqual(1, cellpkg.constraintWidth(p1)); + s.reset(); + } + + // symbol->space->symbol: 2,2 + { + try s.testWriteString(" "); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p0)); + try testing.expectEqual(2, cellpkg.constraintWidth(p2)); + s.reset(); + } + + // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, cellpkg.constraintWidth(p0)); + s.reset(); + } + + // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, cellpkg.constraintWidth(p1)); + s.reset(); + } +} From 2f19d6bb7355c9957ae373ec9cdf999f7fda0c2a Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 18 Sep 2025 00:03:27 -0700 Subject: [PATCH 043/319] Treat Powerline glyphs like normal characters ...not whitespace. Powerline glyphs can be considered an extension of the Block Elements unicode block, which is neither whitespace nor symbols (icons). This ensures that characters immediately followed by a powerline glyph are constrained to a single cell (unlike the current behavior where a PL glyph is considered whitespace), while symbols (icons) immediately preceded by a powerline glyph are not (unlike if a PL glyph were considered a symbol). This resolves https://discord.com/channels/1005603569187160125/1417236683266592798 --- src/renderer/cell.zig | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 3cf306f91..206bb9d81 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -236,8 +236,8 @@ pub fn isCovering(cp: u21) bool { } /// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area and anything -/// in several unicode blocks: +/// for now we define as anything in a private use area, except +/// the Powerline range, and anything in several unicode blocks: /// - Dingbats /// - Emoticons /// - Miscellaneous Symbols @@ -249,11 +249,13 @@ pub fn isCovering(cp: u21) bool { /// In the future it may be prudent to expand this to encompass more /// symbol-like characters, and/or exclude some PUA sections. pub fn isSymbol(cp: u21) bool { - return symbols.get(cp); + return symbols.get(cp) and !isPowerline(cp); } /// Returns the appropriate `constraint_width` for /// the provided cell when rendering its glyph(s). +/// +/// Tested as part of the Screen tests. pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const cell = cell_pin.rowAndCell().cell; const cp = cell.codepoint(); @@ -274,9 +276,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // If we have a previous cell and it was a symbol then we need // to also constrain. This is so that multiple PUA glyphs align. - // As an exception, we ignore powerline glyphs since they are - // used for box drawing and we consider them whitespace. - if (cell_pin.x > 0) prev: { + if (cell_pin.x > 0) { const prev_cp = prev_cp: { var copy = cell_pin; copy.x -= 1; @@ -284,9 +284,6 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { break :prev_cp prev_cell.codepoint(); }; - // We consider powerline glyphs whitespace. - if (isPowerline(prev_cp)) break :prev; - if (isSymbol(prev_cp)) { return 1; } @@ -300,10 +297,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const next_cell = copy.rowAndCell().cell; break :next_cp next_cell.codepoint(); }; - if (next_cp == 0 or - isSpace(next_cp) or - isPowerline(next_cp)) - { + if (next_cp == 0 or isSpace(next_cp)) { return 2; } From d1db596039c445844c3a8966c5155c143ba1a0c4 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 21 Sep 2025 13:36:38 -0700 Subject: [PATCH 044/319] Add box drawing characters to the min contrast exclusion --- src/renderer/cell.zig | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 206bb9d81..c55733516 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -306,9 +306,10 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { } /// Whether min contrast should be disabled for a given glyph. +/// True for glyphs used for terminal graphics, such as box +/// drawing characters, block elements, and Powerline glyphs. pub fn noMinContrast(cp: u21) bool { - // TODO: We should disable for all box drawing type characters. - return isPowerline(cp); + return isBoxDrawing(cp) or isBlockElement(cp) or isLegacyComputing(cp) or isPowerline(cp); } // Some general spaces, others intentionally kept @@ -322,6 +323,32 @@ fn isSpace(char: u21) bool { }; } +// Returns true if the codepoint is a box drawing character. +fn isBoxDrawing(char: u21) bool { + return switch (char) { + 0x2500...0x257F => true, + else => false, + }; +} + +// Returns true if the codepoint is a block element. +fn isBlockElement(char: u21) bool { + return switch (char) { + 0x2580...0x259F => true, + else => false, + }; +} + +// Returns true if the codepoint is in a Symbols for Legacy +// Computing block, including supplements. +fn isLegacyComputing(char: u21) bool { + return switch (char) { + 0x1FB00...0x1FBFF => true, + 0x1CC00...0x1CEBF => true, // Supplement introduced in Unicode 16.0 + else => false, + }; +} + // Returns true if the codepoint is a part of the Powerline range. fn isPowerline(char: u21) bool { return switch (char) { From f2fcbd6e5e8224051f6436eb8fd6e0b9bca44416 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 21 Sep 2025 19:00:35 -0700 Subject: [PATCH 045/319] Add missing codepoints to isPowerline predicate e0d6 and e0d7 were left out. Also collapsed everything to a single range; unlikely that the unused gaps (e0c9, e0cb, e0d3, e0d5) would be used for something else in any font that ships Powerline glyphs. --- src/renderer/cell.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index c55733516..d54e98811 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -352,7 +352,7 @@ fn isLegacyComputing(char: u21) bool { // Returns true if the codepoint is a part of the Powerline range. fn isPowerline(char: u21) bool { return switch (char) { - 0xE0B0...0xE0C8, 0xE0CA, 0xE0CC...0xE0D2, 0xE0D4 => true, + 0xE0B0...0xE0D7 => true, else => false, }; } From 86009545260c2d3609d52a40e7ed44e716ba11a1 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:52:10 +0200 Subject: [PATCH 046/319] Workaround for #8669 --- macos/Sources/Features/App Intents/NewTerminalIntent.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index f7242ee56..6e679673f 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -43,11 +43,13 @@ struct NewTerminalIntent: AppIntent { ) var parent: TerminalEntity? + // Performing in the background can avoid opening multiple windows at the same time + // using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time @available(macOS 26.0, *) - static var supportedModes: IntentModes = .foreground(.immediate) + static var supportedModes: IntentModes = .background @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") - static var openAppWhenRun = true + static var openAppWhenRun = false @MainActor func perform() async throws -> some IntentResult & ReturnsValue { From 8beeebc21de998baa858d353f8261d6d2044f323 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:27:27 +0200 Subject: [PATCH 047/319] Force Ghostty to be active if not --- macos/Sources/Features/App Intents/NewTerminalIntent.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 6e679673f..46a752198 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -98,6 +98,11 @@ struct NewTerminalIntent: AppIntent { parent = nil } + defer { + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } + } switch location { case .window: let newController = TerminalController.newWindow( From 7386dae0797fb09e33ef844acb5286d9b480a7b8 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 23 Sep 2025 09:42:28 -0400 Subject: [PATCH 048/319] use unicode.graphemeBreak in src/font/shaper/web_canvas.zig --- src/font/shaper/web_canvas.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index c41262238..e0f0e1a00 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -3,7 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); -const uucode = @import("uucode"); +const unicode = @import("../../unicode/main.zig"); const log = std.log.scoped(.font_shaper); @@ -111,7 +111,7 @@ pub const Shaper = struct { // font ligatures. However, we do support grapheme clustering. // This means we can render things like skin tone emoji but // we can't render things like single glyph "=>". - var break_state: uucode.GraphemeBreakState = .default; + var break_state: unicode.GraphemeBreakState = .{}; var cp1: u21 = @intCast(codepoints[0]); var start: usize = 0; @@ -126,7 +126,7 @@ pub const Shaper = struct { const cp2: u21 = @intCast(codepoints[i]); defer cp1 = cp2; - break :blk uucode.graphemeBreak( + break :blk unicode.graphemeBreak( cp1, cp2, &break_state, From c2a9c5ee5b3be5cad1952bb3c569b2abf1d0cf69 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 23 Sep 2025 09:48:09 -0400 Subject: [PATCH 049/319] fix comment --- src/simd/codepoint_width.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simd/codepoint_width.zig b/src/simd/codepoint_width.zig index c39cfb4f3..c1767bea4 100644 --- a/src/simd/codepoint_width.zig +++ b/src/simd/codepoint_width.zig @@ -22,7 +22,7 @@ test "codepointWidth basic" { try testing.expectEqual(@as(i8, 2), codepointWidth(0xF900)); // 豈 try testing.expectEqual(@as(i8, 2), codepointWidth(0x20000)); // 𠀀 try testing.expectEqual(@as(i8, 2), codepointWidth(0x30000)); // 𠀀 - // try testing.expectEqual(@as(i8, 1), @import("uucode").get(.wcwidth, 0x100)); + // try testing.expectEqual(@as(i8, 1), @import("uucode").get(.width, 0x100)); } // This is not very fast in debug modes, so its commented by default. From b5c6c044a724ec161a1793e308531bd82b75d56c Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 23 Sep 2025 09:54:09 -0400 Subject: [PATCH 050/319] Fix merge --- src/build/UnicodeTables.zig | 4 ++-- src/main_ghostty.zig | 4 ++-- src/unicode/props_table.zig | 2 +- src/unicode/props_uucode.zig | 2 +- src/unicode/symbols_table.zig | 2 +- src/unicode/symbols_uucode.zig | 41 ++++++++++++++++++++++++++++++++++ 6 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 src/unicode/symbols_uucode.zig diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index 5c6d6e1a4..9972c851a 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -15,7 +15,7 @@ pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables { const props_exe = b.addExecutable(.{ .name = "props-unigen", .root_module = b.createModule(.{ - .root_source_file = b.path("src/unicode/props_ziglyph.zig"), + .root_source_file = b.path("src/unicode/props_uucode.zig"), .target = b.graph.host, .strip = false, .omit_frame_pointer = false, @@ -26,7 +26,7 @@ pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables { const symbols_exe = b.addExecutable(.{ .name = "symbols-unigen", .root_module = b.createModule(.{ - .root_source_file = b.path("src/unicode/symbols_ziglyph.zig"), + .root_source_file = b.path("src/unicode/symbols_uucode.zig"), .target = b.graph.host, .strip = false, .omit_frame_pointer = false, diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 555dd16bf..9c121b950 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -191,8 +191,8 @@ test { _ = @import("simd/main.zig"); _ = @import("synthetic/main.zig"); _ = @import("unicode/main.zig"); - _ = @import("unicode/props_ziglyph.zig"); - _ = @import("unicode/symbols_ziglyph.zig"); + _ = @import("unicode/props_uucode.zig"); + _ = @import("unicode/symbols_uucode.zig"); // Extra _ = @import("extra/bash.zig"); diff --git a/src/unicode/props_table.zig b/src/unicode/props_table.zig index d4ddfebbb..cac0a38b3 100644 --- a/src/unicode/props_table.zig +++ b/src/unicode/props_table.zig @@ -8,7 +8,7 @@ pub const table = table: { // build.zig process, but due to Zig's lazy analysis we can still reference // it here. // - // An example process is the `main` in `props_ziglyph.zig` + // An example process is the `main` in `props_uucode.zig` const generated = @import("unicode_tables").Tables(Properties); const Tables = lut.Tables(Properties); break :table Tables{ diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index 449c04ddf..fe9de37ab 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -55,7 +55,7 @@ pub fn get(cp: u21) Properties { return .{ .width = width, - .grapheme_boundary_class = .init(cp), + .grapheme_boundary_class = graphemeBoundaryClass(cp), }; } diff --git a/src/unicode/symbols_table.zig b/src/unicode/symbols_table.zig index af77d88fd..da2614cae 100644 --- a/src/unicode/symbols_table.zig +++ b/src/unicode/symbols_table.zig @@ -7,7 +7,7 @@ pub const table = table: { // build.zig process, but due to Zig's lazy analysis we can still reference // it here. // - // An example process is the `main` in `symbols_ziglyph.zig` + // An example process is the `main` in `symbols_uucode.zig` const generated = @import("symbols_tables").Tables(bool); const Tables = lut.Tables(bool); break :table Tables{ diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig new file mode 100644 index 000000000..d78a2b234 --- /dev/null +++ b/src/unicode/symbols_uucode.zig @@ -0,0 +1,41 @@ +const std = @import("std"); +const uucode = @import("uucode"); +const lut = @import("lut.zig"); + +/// Runnable binary to generate the lookup tables and output to stdout. +pub fn main() !void { + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena_state.deinit(); + const alloc = arena_state.allocator(); + + const gen: lut.Generator( + bool, + struct { + pub fn get(ctx: @This(), cp: u21) !bool { + _ = ctx; + return if (cp > uucode.config.max_code_point) + false + else + uucode.get(.is_symbol, @intCast(cp)); + } + + pub fn eql(ctx: @This(), a: bool, b: bool) bool { + _ = ctx; + return a == b; + } + }, + ) = .{}; + + const t = try gen.generate(alloc); + defer alloc.free(t.stage1); + defer alloc.free(t.stage2); + defer alloc.free(t.stage3); + try t.writeZig(std.io.getStdOut().writer()); + + // Uncomment when manually debugging to see our table sizes. + // std.log.warn("stage1={} stage2={} stage3={}", .{ + // t.stage1.len, + // t.stage2.len, + // t.stage3.len, + // }); +} From c7ad29ca91385f224fabae06f323cd14b48a8656 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 23 Sep 2025 10:49:59 -0400 Subject: [PATCH 051/319] move tests over to _uucode.zig files to avoid needing deps for vt tests --- src/unicode/props_table.zig | 78 ---------------------------------- src/unicode/props_uucode.zig | 78 ++++++++++++++++++++++++++++++++++ src/unicode/symbols_table.zig | 46 -------------------- src/unicode/symbols_uucode.zig | 46 ++++++++++++++++++++ 4 files changed, 124 insertions(+), 124 deletions(-) diff --git a/src/unicode/props_table.zig b/src/unicode/props_table.zig index cac0a38b3..d168fbb9c 100644 --- a/src/unicode/props_table.zig +++ b/src/unicode/props_table.zig @@ -1,4 +1,3 @@ -const std = @import("std"); const Properties = @import("Properties.zig"); const lut = @import("lut.zig"); @@ -17,80 +16,3 @@ pub const table = table: { .stage3 = &generated.stage3, }; }; - -test "unicode props: tables match uucode" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const uucode = @import("uucode"); - const testing = std.testing; - - const min = 0xFF + 1; // start outside ascii - const max = std.math.maxInt(u21) + 1; - for (min..max) |cp| { - const t = table.get(@intCast(cp)); - const uu = if (cp > uucode.config.max_code_point) - 1 - else - uucode.get(.width, @intCast(cp)); - if (t.width != uu) { - std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t.width, uu }); - try testing.expect(false); - } - } -} - -test "unicode props: tables match ziglyph" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const ziglyph = @import("ziglyph"); - const testing = std.testing; - - const min = 0xFF + 1; // start outside ascii - const max = std.math.maxInt(u21) + 1; - for (min..max) |cp| { - const t = table.get(@intCast(cp)); - const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half))); - if (t.width != zg) { - - // Known exceptions - if (cp == 0x0897) continue; // non-spacing mark (t = 0) - if (cp == 0x2065) continue; // unassigned (t = 1) - if (cp >= 0x2630 and cp <= 0x2637) continue; // east asian width is wide (t = 2) - if (cp >= 0x268A and cp <= 0x268F) continue; // east asian width is wide (t = 2) - if (cp >= 0x2FFC and cp <= 0x2FFF) continue; // east asian width is wide (t = 2) - if (cp == 0x31E4 or cp == 0x31E5) continue; // east asian width is wide (t = 2) - if (cp == 0x31EF) continue; // east asian width is wide (t = 2) - if (cp >= 0x4DC0 and cp <= 0x4DFF) continue; // east asian width is wide (t = 2) - if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) - if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) - if (cp >= 0x10D69 and cp <= 0x10D6D) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0x10EFC and cp <= 0x10EFF) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0x113BB and cp <= 0x113C0) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113CE) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113D0) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113D2) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113E1) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113E2) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1171E) continue; // mark spacing combining (t = 1) - if (cp == 0x11F5A) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1611E) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1611F) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0x16120 and cp <= 0x1612F) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0xE0000 and cp <= 0xE0FFF) continue; // ziglyph ignores these with 0, but many are unassigned (t = 1) - if (cp == 0x18CFF) continue; // east asian width is wide (t = 2) - if (cp >= 0x1D300 and cp <= 0x1D376) continue; // east asian width is wide (t = 2) - if (cp == 0x1E5EE) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1E5EF) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1FA89) continue; // east asian width is wide (t = 2) - if (cp == 0x1FA8F) continue; // east asian width is wide (t = 2) - if (cp == 0x1FABE) continue; // east asian width is wide (t = 2) - if (cp == 0x1FAC6) continue; // east asian width is wide (t = 2) - if (cp == 0x1FADC) continue; // east asian width is wide (t = 2) - if (cp == 0x1FADF) continue; // east asian width is wide (t = 2) - if (cp == 0x1FAE9) continue; // east asian width is wide (t = 2) - - std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t.width, zg }); - try testing.expect(false); - } - } -} diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index fe9de37ab..71edac4fb 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -93,3 +93,81 @@ pub fn main() !void { // t.stage3.len, // }); } + +test "unicode props: tables match uucode" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + const table = @import("props_table.zig").table; + + const min = 0xFF + 1; // start outside ascii + const max = std.math.maxInt(u21) + 1; + for (min..max) |cp| { + const t = table.get(@intCast(cp)); + const uu = if (cp > uucode.config.max_code_point) + 1 + else + uucode.get(.width, @intCast(cp)); + if (t.width != uu) { + std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t.width, uu }); + try testing.expect(false); + } + } +} + +test "unicode props: tables match ziglyph" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + const table = @import("props_table.zig").table; + const ziglyph = @import("ziglyph"); + + const min = 0xFF + 1; // start outside ascii + const max = std.math.maxInt(u21) + 1; + for (min..max) |cp| { + const t = table.get(@intCast(cp)); + const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half))); + if (t.width != zg) { + + // Known exceptions + if (cp == 0x0897) continue; // non-spacing mark (t = 0) + if (cp == 0x2065) continue; // unassigned (t = 1) + if (cp >= 0x2630 and cp <= 0x2637) continue; // east asian width is wide (t = 2) + if (cp >= 0x268A and cp <= 0x268F) continue; // east asian width is wide (t = 2) + if (cp >= 0x2FFC and cp <= 0x2FFF) continue; // east asian width is wide (t = 2) + if (cp == 0x31E4 or cp == 0x31E5) continue; // east asian width is wide (t = 2) + if (cp == 0x31EF) continue; // east asian width is wide (t = 2) + if (cp >= 0x4DC0 and cp <= 0x4DFF) continue; // east asian width is wide (t = 2) + if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) + if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) + if (cp >= 0x10D69 and cp <= 0x10D6D) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0x10EFC and cp <= 0x10EFF) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0x113BB and cp <= 0x113C0) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113CE) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113D0) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113D2) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113E1) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x113E2) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1171E) continue; // mark spacing combining (t = 1) + if (cp == 0x11F5A) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1611E) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1611F) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0x16120 and cp <= 0x1612F) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp >= 0xE0000 and cp <= 0xE0FFF) continue; // ziglyph ignores these with 0, but many are unassigned (t = 1) + if (cp == 0x18CFF) continue; // east asian width is wide (t = 2) + if (cp >= 0x1D300 and cp <= 0x1D376) continue; // east asian width is wide (t = 2) + if (cp == 0x1E5EE) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1E5EF) continue; // non-spacing mark, despite being east asian width normal (t = 0) + if (cp == 0x1FA89) continue; // east asian width is wide (t = 2) + if (cp == 0x1FA8F) continue; // east asian width is wide (t = 2) + if (cp == 0x1FABE) continue; // east asian width is wide (t = 2) + if (cp == 0x1FAC6) continue; // east asian width is wide (t = 2) + if (cp == 0x1FADC) continue; // east asian width is wide (t = 2) + if (cp == 0x1FADF) continue; // east asian width is wide (t = 2) + if (cp == 0x1FAE9) continue; // east asian width is wide (t = 2) + + std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t.width, zg }); + try testing.expect(false); + } + } +} diff --git a/src/unicode/symbols_table.zig b/src/unicode/symbols_table.zig index da2614cae..034d34428 100644 --- a/src/unicode/symbols_table.zig +++ b/src/unicode/symbols_table.zig @@ -1,4 +1,3 @@ -const std = @import("std"); const lut = @import("lut.zig"); /// The lookup tables for Ghostty. @@ -16,48 +15,3 @@ pub const table = table: { .stage3 = &generated.stage3, }; }; - -test "unicode symbols: tables match uucode" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const uucode = @import("uucode"); - const testing = std.testing; - - for (0..std.math.maxInt(u21)) |cp| { - const t = table.get(@intCast(cp)); - const uu = if (cp > uucode.config.max_code_point) - false - else - uucode.get(.is_symbol, @intCast(cp)); - - if (t != uu) { - std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t, uu }); - try testing.expect(false); - } - } -} - -test "unicode symbols: tables match ziglyph" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const ziglyph = @import("ziglyph"); - const testing = std.testing; - - for (0..std.math.maxInt(u21)) |cp_usize| { - const cp: u21 = @intCast(cp_usize); - const t = table.get(cp); - const zg = ziglyph.general_category.isPrivateUse(cp) or - ziglyph.blocks.isDingbats(cp) or - ziglyph.blocks.isEmoticons(cp) or - ziglyph.blocks.isMiscellaneousSymbols(cp) or - ziglyph.blocks.isEnclosedAlphanumerics(cp) or - ziglyph.blocks.isEnclosedAlphanumericSupplement(cp) or - ziglyph.blocks.isMiscellaneousSymbolsAndPictographs(cp) or - ziglyph.blocks.isTransportAndMapSymbols(cp); - - if (t != zg) { - std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); - try testing.expect(false); - } - } -} diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig index d78a2b234..985ed1380 100644 --- a/src/unicode/symbols_uucode.zig +++ b/src/unicode/symbols_uucode.zig @@ -39,3 +39,49 @@ pub fn main() !void { // t.stage3.len, // }); } + +test "unicode symbols: tables match uucode" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + const table = @import("symbols_table.zig").table; + + for (0..std.math.maxInt(u21)) |cp| { + const t = table.get(@intCast(cp)); + const uu = if (cp > uucode.config.max_code_point) + false + else + uucode.get(.is_symbol, @intCast(cp)); + + if (t != uu) { + std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t, uu }); + try testing.expect(false); + } + } +} + +test "unicode symbols: tables match ziglyph" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + const table = @import("symbols_table.zig").table; + const ziglyph = @import("ziglyph"); + + for (0..std.math.maxInt(u21)) |cp_usize| { + const cp: u21 = @intCast(cp_usize); + const t = table.get(cp); + const zg = ziglyph.general_category.isPrivateUse(cp) or + ziglyph.blocks.isDingbats(cp) or + ziglyph.blocks.isEmoticons(cp) or + ziglyph.blocks.isMiscellaneousSymbols(cp) or + ziglyph.blocks.isEnclosedAlphanumerics(cp) or + ziglyph.blocks.isEnclosedAlphanumericSupplement(cp) or + ziglyph.blocks.isMiscellaneousSymbolsAndPictographs(cp) or + ziglyph.blocks.isTransportAndMapSymbols(cp); + + if (t != zg) { + std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); + try testing.expect(false); + } + } +} From a96cb9ab574e099ea4f792582facdddfb89e959c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gautier=20Ben=20A=C3=AFm?= Date: Tue, 23 Sep 2025 20:42:57 +0200 Subject: [PATCH 052/319] chore: pin zig 0.14.1 in .zigversion --- .zigversion | 1 + 1 file changed, 1 insertion(+) create mode 100644 .zigversion diff --git a/.zigversion b/.zigversion new file mode 100644 index 000000000..930e3000b --- /dev/null +++ b/.zigversion @@ -0,0 +1 @@ +0.14.1 From b0e85d900ec0d383d548bf3962e65ba1851c5260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gautier=20Ben=20A=C3=AFm?= Date: Tue, 23 Sep 2025 21:14:09 +0200 Subject: [PATCH 053/319] use build.zig.zon instead --- .zigversion | 1 - build.zig.zon | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .zigversion diff --git a/.zigversion b/.zigversion deleted file mode 100644 index 930e3000b..000000000 --- a/.zigversion +++ /dev/null @@ -1 +0,0 @@ -0.14.1 diff --git a/build.zig.zon b/build.zig.zon index b297f3bb0..91f2c5772 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,6 +3,7 @@ .version = "1.2.1", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, + .minimum_zig_version = "0.14.0", .dependencies = .{ // Zig libs From 8bcab93c21737b2537b2657aeaed4059ef24c318 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 24 Sep 2025 08:34:21 -0400 Subject: [PATCH 054/319] separate out runtime and buildtime uucode tables --- src/build/uucode_config.zig | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index de0549f3d..085ca2561 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -60,29 +60,22 @@ const is_symbol = config.Extension{ pub const tables = [_]config.Table{ .{ - .extensions = &.{wcwidth}, + .name = "runtime", + .extensions = &.{}, .fields = &.{ d.field("is_emoji_presentation"), d.field("case_folding_full"), + }, + }, + .{ + .name = "buildtime", + .extensions = &.{ wcwidth, width, is_symbol }, + .fields = &.{ + width.field("width"), + d.field("grapheme_break"), + is_symbol.field("is_symbol"), d.field("is_emoji_modifier"), d.field("is_emoji_modifier_base"), }, }, - .{ - .extensions = &.{ wcwidth, width }, - .fields = &.{ - width.field("width"), - }, - }, - .{ - .fields = &.{ - d.field("grapheme_break"), - }, - }, - .{ - .extensions = &.{is_symbol}, - .fields = &.{ - is_symbol.field("is_symbol"), - }, - }, }; From c5786c5d38f1f7edd986ed4a4d5339e450511df3 Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:28:32 -0400 Subject: [PATCH 055/319] fix: alloc free off by one --- include/ghostty.h | 1 + src/config/CApi.zig | 3 ++- src/main_c.zig | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 7888b380c..a2964c227 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,6 +353,7 @@ typedef struct { typedef struct { const char* ptr; uintptr_t len; + uintptr_t cap; } ghostty_string_s; typedef struct { diff --git a/src/config/CApi.zig b/src/config/CApi.zig index bdc59797a..f90b0ca24 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -130,7 +130,8 @@ export fn ghostty_config_open_path() c.String { return .empty; }; - return .fromSlice(path); + // Capacity is len + 1 due to sentinel + return .fromSlice(path, path.len + 1); } /// Sync with ghostty_diagnostic_s diff --git a/src/main_c.zig b/src/main_c.zig index 9a9bcc6d2..1212e0b07 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -63,16 +63,19 @@ const Info = extern struct { pub const String = extern struct { ptr: ?[*]const u8, len: usize, + cap: usize, pub const empty: String = .{ .ptr = null, .len = 0, + .cap = 0, }; - pub fn fromSlice(slice: []const u8) String { + pub fn fromSlice(slice: []const u8, cap: usize) String { return .{ .ptr = slice.ptr, .len = slice.len, + .cap = cap, }; } }; @@ -129,5 +132,5 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { /// Free a string allocated by Ghostty. pub export fn ghostty_string_free(str: String) void { - state.alloc.free(str.ptr.?[0..str.len]); + state.alloc.free(str.ptr.?[0..str.cap]); } From 79685f87c420ca7a9d73fee5865bb5046b7df74b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 24 Sep 2025 16:33:18 -0500 Subject: [PATCH 056/319] use comptime to make C String interface nicer --- src/config/CApi.zig | 2 +- src/main_c.zig | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/config/CApi.zig b/src/config/CApi.zig index f90b0ca24..154cc0c9c 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -131,7 +131,7 @@ export fn ghostty_config_open_path() c.String { }; // Capacity is len + 1 due to sentinel - return .fromSlice(path, path.len + 1); + return .fromSlice(path); } /// Sync with ghostty_diagnostic_s diff --git a/src/main_c.zig b/src/main_c.zig index 1212e0b07..a72d82a3e 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -63,21 +63,42 @@ const Info = extern struct { pub const String = extern struct { ptr: ?[*]const u8, len: usize, - cap: usize, + sentinel: bool, pub const empty: String = .{ .ptr = null, .len = 0, - .cap = 0, + .sentinel = false, }; - pub fn fromSlice(slice: []const u8, cap: usize) String { + pub fn fromSlice(slice: anytype) String { return .{ .ptr = slice.ptr, .len = slice.len, - .cap = cap, + .sentinel = sentinel: { + const info = @typeInfo(@TypeOf(slice)); + switch (info) { + .pointer => |p| { + if (p.size != .slice) @compileError("only slices supported"); + if (p.child != u8) @compileError("only u8 slices supported"); + const sentinel_ = p.sentinel(); + if (sentinel_) |sentinel| if (sentinel != 0) @compileError("only 0 is supported for sentinels"); + break :sentinel sentinel_ != null; + }, + else => @compileError("only []const u8 and [:0]const u8"), + } + }, }; } + + pub fn deinit(self: *const String) void { + const ptr = self.ptr orelse return; + if (self.sentinel) { + state.alloc.free(ptr[0..self.len :0]); + } else { + state.alloc.free(ptr[0..self.len]); + } + } }; /// Initialize ghostty global state. @@ -132,5 +153,5 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { /// Free a string allocated by Ghostty. pub export fn ghostty_string_free(str: String) void { - state.alloc.free(str.ptr.?[0..str.cap]); + str.deinit(); } From dc03a47558572dc66ae03350739cda70e1ea4cc5 Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:47:24 -0400 Subject: [PATCH 057/319] chore: sync changes with ghostty_string_s --- include/ghostty.h | 2 +- src/config/CApi.zig | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index a2964c227..3f1e0c9d9 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,7 +353,7 @@ typedef struct { typedef struct { const char* ptr; uintptr_t len; - uintptr_t cap; + bool sentinel; } ghostty_string_s; typedef struct { diff --git a/src/config/CApi.zig b/src/config/CApi.zig index 154cc0c9c..bdc59797a 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -130,7 +130,6 @@ export fn ghostty_config_open_path() c.String { return .empty; }; - // Capacity is len + 1 due to sentinel return .fromSlice(path); } From 22cf46aefcbab3c95da1c6ccc58008f2f94e5b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gautier=20Ben=20A=C3=AFm?= <48261497+GauBen@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:08:57 +0200 Subject: [PATCH 058/319] use 0.14.1 --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index 91f2c5772..028f1a0d6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,7 +3,7 @@ .version = "1.2.1", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.14.1", .dependencies = .{ // Zig libs From 76cafeb95726670c7051acc5a0afda856bc6f3ca Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 25 Sep 2025 08:48:19 -0400 Subject: [PATCH 059/319] move ziglyph dep to SharedDeps with .@"test" condition --- build.zig | 8 -------- src/build/SharedDeps.zig | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index 764db3438..c6c461b4c 100644 --- a/build.zig +++ b/build.zig @@ -271,14 +271,6 @@ pub fn build(b: *std.Build) !void { if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe); - // Only need ziglyph for tests - if (b.lazyDependency("ziglyph", .{ - .target = test_exe.root_module.resolved_target.?, - .optimize = test_exe.root_module.optimize.?, - })) |dep| { - test_exe.root_module.addImport("ziglyph", dep.module("ziglyph")); - } - // Normal test running const test_run = b.addRunArtifact(test_exe); test_step.dependOn(&test_run.step); diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 9461d48b7..7c0619b5e 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -403,6 +403,14 @@ pub fn add( })) |dep| { step.root_module.addImport("z2d", dep.module("z2d")); } + if (step.kind == .@"test") { + if (b.lazyDependency("ziglyph", .{ + .target = step.root_module.resolved_target.?, + .optimize = step.root_module.optimize.?, + })) |dep| { + step.root_module.addImport("ziglyph", dep.module("ziglyph")); + } + } if (b.lazyDependency("uucode", .{ .target = target, .optimize = optimize, From 1d89a63f2995eadb61283f582325387d6bbbc3af Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 25 Sep 2025 10:00:03 -0400 Subject: [PATCH 060/319] Use commit pointed to by signed tag `v0.1.0-zig-0.14` --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 3df433bef..2114dad3f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,8 +42,8 @@ .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz", - .hash = "uucode-0.0.0-ZZjBPk0GQACuYIoFqT_Vzkvn8Ur_M3dE7o4DNUE65Z7v", + .url = "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", + .hash = "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland From d79441edd1ec28132f10e867040cd5f646196238 Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:53:02 -0400 Subject: [PATCH 061/319] test: valid string slices for ghostty_string_s --- src/main_c.zig | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main_c.zig b/src/main_c.zig index a72d82a3e..d3fb753ef 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -155,3 +155,43 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { pub export fn ghostty_string_free(str: String) void { str.deinit(); } + +test "ghostty_string_s empty string" { + const testing = std.testing; + const empty_string = String.empty; + defer empty_string.deinit(); + + try testing.expect(empty_string.len == 0); + try testing.expect(empty_string.sentinel == false); +} + +test "ghostty_string_s c string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: [:0]const u8 = "hello"; + const allocated_slice = try testing.allocator.dupeZ(u8, slice); + const c_null_string = String.fromSlice(allocated_slice); + defer c_null_string.deinit(); + + try testing.expect(allocated_slice[5] == 0); + try testing.expect(@TypeOf(slice) == [:0]const u8); + try testing.expect(@TypeOf(allocated_slice) == [:0]u8); + try testing.expect(c_null_string.len == 5); + try testing.expect(c_null_string.sentinel == true); +} + +test "ghostty_string_s zig string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: []const u8 = "hello"; + const allocated_slice = try testing.allocator.dupe(u8, slice); + const zig_string = String.fromSlice(allocated_slice); + defer zig_string.deinit(); + + try testing.expect(@TypeOf(slice) == []const u8); + try testing.expect(@TypeOf(allocated_slice) == []u8); + try testing.expect(zig_string.len == 5); + try testing.expect(zig_string.sentinel == false); +} From 79a5902ef23a30782df7bb1a2fac6dc20375697b Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 25 Sep 2025 16:39:11 -0400 Subject: [PATCH 062/319] vim: use :setf to set the filetype This is nicer because it only sets the filetype if it hasn't already been set. :setf[iletype] has been available since vim version 6. See: https://vimhelp.org/options.txt.html#%3Asetf --- src/extra/vim.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extra/vim.zig b/src/extra/vim.zig index e5261cd74..4443fd168 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -10,7 +10,7 @@ pub const ftdetect = \\" \\" THIS FILE IS AUTO-GENERATED \\ - \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* set ft=ghostty + \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* setf ghostty \\ ; pub const ftplugin = From fdbf0c624204174a49afb6276e88ee70b1ae3182 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 00:06:22 +0000 Subject: [PATCH 063/319] build(deps): bump namespacelabs/nscloud-cache-action Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.17 to 1.2.18. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/a289cf5d2fcd6874376aa92f0ef7f99dc923592a...7baedde84bbf5063413d621f282834bc2654d0c1) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.2.18 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/snap.yml | 2 +- .github/workflows/test.yml | 48 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 09ec4aeed..ef6f96555 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 66dfe5fc2..af912215c 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 853378d43..7f7b85e2f 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -163,7 +163,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 6feb39887..4e9aa168c 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -38,7 +38,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9efd257ca..1638b0fd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,7 +73,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -107,7 +107,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -140,7 +140,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -174,7 +174,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -216,7 +216,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -252,7 +252,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -281,7 +281,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -314,7 +314,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -360,7 +360,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -572,7 +572,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -614,7 +614,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -662,7 +662,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -697,7 +697,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -761,7 +761,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -790,7 +790,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -818,7 +818,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -845,7 +845,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -872,7 +872,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -899,7 +899,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -926,7 +926,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -960,7 +960,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -987,7 +987,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -1022,7 +1022,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -1110,7 +1110,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 3f0d1d1e2..4e9db4225 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix From 311f8ec70b675d553eccbb7a2d2042b374fab93e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 26 Sep 2025 11:10:58 -0500 Subject: [PATCH 064/319] build: limit cpu affinity to 32 cpus on Linux Related to #8924 Zig currenly has a bug where it crashes when compiling Ghostty on systems with more than 32 cpus (See the linked issue for the gory details). As a temporary hack, use `sched_setaffinity` on Linux systems to limit the compile to the first 32 cores. Note that this affects the build only. The resulting Ghostty executable is not limited in any way. This is a more general fix than wrapping the Zig compiler with `taskset`. First of all, it requires no action from the user or packagers. Second, it will be easier for us to remove once the upstream Zig bug is fixed. --- build.zig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/build.zig b/build.zig index c6c461b4c..008fc849e 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,10 @@ comptime { } pub fn build(b: *std.Build) !void { + // Works around a Zig but still present in 0.15.1. Remove when fixed. + // https://github.com/ghostty-org/ghostty/issues/8924 + try limitCoresForZigBug(); + // This defines all the available build options (e.g. `-D`). If you // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. @@ -298,3 +302,13 @@ pub fn build(b: *std.Build) !void { try translations_step.addError("cannot update translations when i18n is disabled", .{}); } } + +// WARNING: Remove this when https://github.com/ghostty-org/ghostty/issues/8924 is resolved! +// Limit ourselves to 32 cpus on Linux because of an upstream Zig bug. +fn limitCoresForZigBug() !void { + if (comptime builtin.os.tag != .linux) return; + const pid = std.os.linux.getpid(); + var set: std.bit_set.ArrayBitSet(usize, std.os.linux.CPU_SETSIZE * 8) = .initEmpty(); + for (0..32) |cpu| set.set(cpu); + try std.os.linux.sched_setaffinity(pid, &set.masks); +} From 8a1dc5bd9763fb537606a690fadc5103a1384df8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 13:20:50 -0700 Subject: [PATCH 065/319] terminal: shuffle some C APIs to make it more long term maintainable --- src/terminal/c/main.zig | 12 +++++++++++ src/terminal/{c_api.zig => c/osc.zig} | 29 +++++++++------------------ src/terminal/c/result.zig | 5 +++++ src/terminal/main.zig | 2 +- 4 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 src/terminal/c/main.zig rename src/terminal/{c_api.zig => c/osc.zig} (61%) create mode 100644 src/terminal/c/result.zig diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig new file mode 100644 index 000000000..fa1fd0c6a --- /dev/null +++ b/src/terminal/c/main.zig @@ -0,0 +1,12 @@ +pub const osc = @import("osc.zig"); + +// The full C API, unexported. +pub const osc_new = osc.new; +pub const osc_free = osc.free; + +test { + _ = osc; + + // We want to make sure we run the tests for the C allocator interface. + _ = @import("../../lib/allocator.zig"); +} diff --git a/src/terminal/c_api.zig b/src/terminal/c/osc.zig similarity index 61% rename from src/terminal/c_api.zig rename to src/terminal/c/osc.zig index 194a91d6d..e0024bc17 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c/osc.zig @@ -1,22 +1,17 @@ const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); -const lib_alloc = @import("../lib/allocator.zig"); +const lib_alloc = @import("../../lib/allocator.zig"); const CAllocator = lib_alloc.Allocator; -const osc = @import("osc.zig"); +const osc = @import("../osc.zig"); +const Result = @import("result.zig").Result; /// C: GhosttyOscParser -pub const OscParser = ?*osc.Parser; +pub const Parser = ?*osc.Parser; -/// C: GhosttyResult -pub const Result = enum(c_int) { - success = 0, - out_of_memory = -1, -}; - -pub fn osc_new( +pub fn new( alloc_: ?*const CAllocator, - result: *OscParser, + result: *Parser, ) callconv(.c) Result { const alloc = lib_alloc.default(alloc_); const ptr = alloc.create(osc.Parser) catch @@ -26,7 +21,7 @@ pub fn osc_new( return .success; } -pub fn osc_free(parser_: OscParser) callconv(.c) void { +pub fn free(parser_: Parser) callconv(.c) void { // C-built parsers always have an associated allocator. const parser = parser_ orelse return; const alloc = parser.alloc.?; @@ -34,16 +29,12 @@ pub fn osc_free(parser_: OscParser) callconv(.c) void { alloc.destroy(parser); } -test { - _ = lib_alloc; -} - test "osc" { const testing = std.testing; - var p: OscParser = undefined; - try testing.expectEqual(Result.success, osc_new( + var p: Parser = undefined; + try testing.expectEqual(Result.success, new( &lib_alloc.test_allocator, &p, )); - osc_free(p); + free(p); } diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig new file mode 100644 index 000000000..a2ebc9b69 --- /dev/null +++ b/src/terminal/c/result.zig @@ -0,0 +1,5 @@ +/// C: GhosttyResult +pub const Result = enum(c_int) { + success = 0, + out_of_memory = -1, +}; diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 4064c0c9c..832fe6a29 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -64,7 +64,7 @@ pub const isSafePaste = sanitize.isSafePaste; /// This is set to true when we're building the C library. pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); -pub const c_api = @import("c_api.zig"); +pub const c_api = @import("c/main.zig"); test { @import("std").testing.refAllDecls(@This()); From b3d1802c89ce10d73c28821d2d071edbd6ec3df4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 13:22:56 -0700 Subject: [PATCH 066/319] lib_vt: osc_next/reset --- include/ghostty/vt.h | 26 ++++++++++++++++++++++++++ src/lib_vt.zig | 2 ++ src/terminal/c/main.zig | 2 ++ src/terminal/c/osc.zig | 8 ++++++++ src/terminal/osc.zig | 2 +- 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 12ed2d015..657a9e60f 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -214,6 +214,32 @@ GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParse */ void ghostty_osc_free(GhosttyOscParser parser); +/** + * Reset an OSC parser instance to its initial state. + * + * Resets the parser state, clearing any partially parsed OSC sequences + * and returning the parser to its initial state. This is useful for + * reusing a parser instance or recovering from parse errors. + * + * @param parser The parser handle to reset, must not be null. + */ +void ghostty_osc_reset(GhosttyOscParser parser); + +/** + * Parse the next byte in an OSC sequence. + * + * Processes a single byte as part of an OSC sequence. The parser maintains + * internal state to track the progress through the sequence. Call this + * function for each byte in the sequence data. + * + * When finished pumping the parser with bytes, call ghostty_osc_end + * to get the final result. + * + * @param parser The parser handle, must not be null. + * @param byte The next byte to parse + */ +void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); + #ifdef __cplusplus } #endif diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 656509cce..63a84ad63 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -72,6 +72,8 @@ comptime { const c = terminal.c_api; @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); + @export(&c.osc_next, .{ .name = "ghostty_osc_next" }); + @export(&c.osc_reset, .{ .name = "ghostty_osc_reset" }); } } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index fa1fd0c6a..1a25685ba 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -3,6 +3,8 @@ pub const osc = @import("osc.zig"); // The full C API, unexported. pub const osc_new = osc.new; pub const osc_free = osc.free; +pub const osc_reset = osc.reset; +pub const osc_next = osc.next; test { _ = osc; diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index e0024bc17..59761cfba 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -29,6 +29,14 @@ pub fn free(parser_: Parser) callconv(.c) void { alloc.destroy(parser); } +pub fn reset(parser_: Parser) callconv(.c) void { + parser_.?.reset(); +} + +pub fn next(parser_: Parser, byte: u8) callconv(.c) void { + parser_.?.next(byte); +} + test "osc" { const testing = std.testing; var p: Parser = undefined; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index bd7337b42..5b0ea0847 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -431,7 +431,7 @@ pub const Parser = struct { self.reset(); } - /// Reset the parser start. + /// Reset the parser state. pub fn reset(self: *Parser) void { // If the state is already empty then we do nothing because // we may touch uninitialized memory. From a79e68ace105815f45a52ee0b4ff03bf5558fa0b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 14:25:00 -0700 Subject: [PATCH 067/319] lib: enum func --- src/lib/enum.zig | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib/main.zig | 10 ++++++ src/lib_vt.zig | 3 +- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/lib/enum.zig create mode 100644 src/lib/main.zig diff --git a/src/lib/enum.zig b/src/lib/enum.zig new file mode 100644 index 000000000..01006f46f --- /dev/null +++ b/src/lib/enum.zig @@ -0,0 +1,85 @@ +const std = @import("std"); + +/// Create an enum type with the given keys that is C ABI compatible +/// if we're targeting C, otherwise a Zig enum with smallest possible +/// backing type. +/// +/// In all cases, the enum keys will be created in the order given. +/// For C ABI, this means that the order MUST NOT be changed in order +/// to preserve ABI compatibility. You can set a key to null to +/// remove it from the Zig enum while keeping the "hole" in the C enum +/// to preserve ABI compatibility. +/// +/// C detection is up to the caller, since there are multiple ways +/// to do that. We rely on the `target` parameter to determine whether we +/// should create a C compatible enum or a Zig enum. +/// +/// For the Zig enum, the enum value is not guaranteed to be stable, so +/// it shouldn't be relied for things like serialization. +pub fn Enum( + target: Target, + keys: []const ?[:0]const u8, +) type { + var fields: [keys.len]std.builtin.Type.EnumField = undefined; + var fields_i: usize = 0; + for (keys, 0..) |key_, key_i| { + const key: [:0]const u8 = key_ orelse switch (target) { + .c => std.fmt.comptimePrint("__unused_{d}", .{key_i}), + .zig => continue, + }; + + fields[fields_i] = .{ + .name = key, + .value = fields_i, + }; + fields_i += 1; + } + + return @Type(.{ .@"enum" = .{ + .tag_type = switch (target) { + .c => c_int, + .zig => std.math.IntFittingRange(0, fields_i - 1), + }, + .fields = fields[0..fields_i], + .decls = &.{}, + .is_exhaustive = true, + } }); +} + +pub const Target = union(enum) { + c, + zig, +}; + +test "zig" { + const testing = std.testing; + const T = Enum(.zig, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); +} + +test "c" { + const testing = std.testing; + const T = Enum(.c, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); +} + +test "abi by removing a key" { + const testing = std.testing; + // C + { + const T = Enum(.c, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); + try testing.expectEqual(3, @intFromEnum(T.d)); + } + + // Zig + { + const T = Enum(.zig, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); + try testing.expectEqual(2, @intFromEnum(T.d)); + } +} diff --git a/src/lib/main.zig b/src/lib/main.zig new file mode 100644 index 000000000..4ef8dcb2d --- /dev/null +++ b/src/lib/main.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const enumpkg = @import("enum.zig"); + +pub const allocator = @import("allocator.zig"); +pub const Enum = enumpkg.Enum; +pub const EnumTarget = enumpkg.Target; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 63a84ad63..6d9c042d8 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -80,6 +80,7 @@ comptime { test { _ = terminal; - // Tests always test the C API + // Tests always test the C API and shared C functions _ = terminal.c_api; + _ = @import("lib/main.zig"); } From 397e47c274fb5077e16b37f28c8f9327cc995b2e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 14:32:45 -0700 Subject: [PATCH 068/319] terminal: use LibEnum for the command keys --- src/lib/enum.zig | 4 +++- src/terminal/build_options.zig | 3 +++ src/terminal/main.zig | 2 +- src/terminal/osc.zig | 32 +++++++++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/lib/enum.zig b/src/lib/enum.zig index 01006f46f..063232176 100644 --- a/src/lib/enum.zig +++ b/src/lib/enum.zig @@ -35,7 +35,8 @@ pub fn Enum( fields_i += 1; } - return @Type(.{ .@"enum" = .{ + // Assigned to var so that the type name is nicer in stack traces. + const Result = @Type(.{ .@"enum" = .{ .tag_type = switch (target) { .c => c_int, .zig => std.math.IntFittingRange(0, fields_i - 1), @@ -44,6 +45,7 @@ pub fn Enum( .decls = &.{}, .is_exhaustive = true, } }); + return Result; } pub const Target = union(enum) { diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 1b0449bbf..2085e2243 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -1,5 +1,8 @@ const std = @import("std"); +/// True if we're building the C library libghostty-vt. +pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); + pub const Options = struct { /// The target artifact to build. This will gate some functionality. artifact: Artifact, diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 832fe6a29..70b5742cd 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -63,7 +63,7 @@ pub const Attribute = sgr.Attribute; pub const isSafePaste = sanitize.isSafePaste; /// This is set to true when we're building the C library. -pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); +pub const is_c_lib = @import("build_options.zig").is_c_lib; pub const c_api = @import("c/main.zig"); test { diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 5b0ea0847..9ba394c67 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -10,6 +10,8 @@ const builtin = @import("builtin"); const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; +const LibEnum = @import("../lib/enum.zig").Enum; +const is_c_lib = @import("build_options.zig").is_c_lib; const RGB = @import("color.zig").RGB; const kitty_color = @import("kitty/color.zig"); const osc_color = @import("osc/color.zig"); @@ -17,7 +19,7 @@ pub const color = osc_color; const log = std.log.scoped(.osc); -pub const Command = union(enum) { +pub const Command = union(Key) { /// This generally shouldn't ever be set except as an initial zero value. /// Ignore it. invalid, @@ -172,6 +174,34 @@ pub const Command = union(enum) { /// ConEmu GUI macro (OSC 9;6) conemu_guimacro: []const u8, + pub const Key = LibEnum( + if (is_c_lib) .c else .zig, + // NOTE: Order matters, see LibEnum documentation. + &.{ + "invalid", + "change_window_title", + "change_window_icon", + "prompt_start", + "prompt_end", + "end_of_input", + "end_of_command", + "clipboard_contents", + "report_pwd", + "mouse_shape", + "color_operation", + "kitty_color_protocol", + "show_desktop_notification", + "hyperlink_start", + "hyperlink_end", + "conemu_sleep", + "conemu_show_message_box", + "conemu_change_tab_title", + "conemu_progress_report", + "conemu_wait_input", + "conemu_guimacro", + }, + ); + pub const ProgressReport = struct { pub const State = enum(c_int) { remove, From 6a0a94c82728d93440fc6f693154ec5aa4c79dc9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 14:42:59 -0700 Subject: [PATCH 069/319] lib: fix holes handling for C --- src/lib/enum.zig | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/lib/enum.zig b/src/lib/enum.zig index 063232176..c3971ebde 100644 --- a/src/lib/enum.zig +++ b/src/lib/enum.zig @@ -22,15 +22,25 @@ pub fn Enum( ) type { var fields: [keys.len]std.builtin.Type.EnumField = undefined; var fields_i: usize = 0; - for (keys, 0..) |key_, key_i| { - const key: [:0]const u8 = key_ orelse switch (target) { - .c => std.fmt.comptimePrint("__unused_{d}", .{key_i}), - .zig => continue, + var holes: usize = 0; + for (keys) |key_| { + const key: [:0]const u8 = key_ orelse { + switch (target) { + // For Zig we don't track holes because the enum value + // isn't guaranteed to be stable and we want to use the + // smallest possible backing type. + .zig => {}, + + // For C we must track holes to preserve ABI compatibility + // with subsequent values. + .c => holes += 1, + } + continue; }; fields[fields_i] = .{ .name = key, - .value = fields_i, + .value = fields_i + holes, }; fields_i += 1; } From 6b1f4088dd15ccf8377ba778554421ec61b30ce0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 14:57:51 -0700 Subject: [PATCH 070/319] lib-vt: add the C functions for command inspection --- include/ghostty/vt.h | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 657a9e60f..c784dcb0e 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -35,6 +35,15 @@ extern "C" { */ typedef struct GhosttyOscParser *GhosttyOscParser; +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data using + * `ghostty_osc_command_type` and `ghostty_osc_command_data`. + */ +typedef struct GhosttyOscCommand *GhosttyOscCommand; + /** * Result codes for libghostty-vt operations. */ @@ -45,6 +54,46 @@ typedef enum { GHOSTTY_OUT_OF_MEMORY = -1, } GhosttyResult; +/** + * OSC command types. + */ +typedef enum { + GHOSTTY_OSC_COMMAND_INVALID = 0, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, + GHOSTTY_OSC_COMMAND_PROMPT_START = 3, + GHOSTTY_OSC_COMMAND_PROMPT_END = 4, + GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5, + GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 8, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, +} GhosttyOscCommandType; + +/** + * OSC command data types. The values returned are documented + * on each type. + * */ +typedef enum { + /** + * The window title string. + * + * Type: const char* + * */ + GHOSTTY_OSC_DATA_WINDOW_TITLE, +} GhosttyOscCommandData; + //------------------------------------------------------------------- // Allocator Interface @@ -240,6 +289,10 @@ void ghostty_osc_reset(GhosttyOscParser parser); */ void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); +GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser); +GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); +bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData, void *result); + #ifdef __cplusplus } #endif From cc0f2e79cd75add2cb2b82a0372c92fc4fb4b4c5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 15:05:38 -0700 Subject: [PATCH 071/319] terminal: osc parser end returns a pointer --- src/terminal/Parser.zig | 2 +- src/terminal/osc.zig | 157 ++++++++++++++++++++-------------------- 2 files changed, 81 insertions(+), 78 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 1f2e814f6..6deb03da5 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -274,7 +274,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { // Exit depends on current state if (self.state == next_state) null else switch (self.state) { .osc_string => if (self.osc_parser.end(c)) |cmd| - Action{ .osc_dispatch = cmd } + Action{ .osc_dispatch = cmd.* } else null, .dcs_passthrough => Action{ .dcs_unhook = {} }, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 9ba394c67..71d2f8598 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1597,7 +1597,10 @@ pub const Parser = struct { /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine /// the response terminator. - pub fn end(self: *Parser, terminator_ch: ?u8) ?Command { + /// + /// The returned pointer is only valid until the next call to the parser. + /// Callers should copy out any data they wish to retain across calls. + pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command { if (!self.complete) { if (comptime !builtin.is_test) log.warn( "invalid OSC command: {s}", @@ -1656,7 +1659,7 @@ pub const Parser = struct { else => {}, } - return self.command; + return &self.command; } }; @@ -1672,7 +1675,7 @@ test "OSC: change_window_title" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } @@ -1685,7 +1688,7 @@ test "OSC: change_window_title with 2" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } @@ -1707,7 +1710,7 @@ test "OSC: change_window_title with utf8" { p.next(0xE2); p.next(0x80); p.next(0x90); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("— ‐", cmd.change_window_title); } @@ -1718,7 +1721,7 @@ test "OSC: change_window_title empty" { var p: Parser = .init(); p.next('2'); p.next(';'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("", cmd.change_window_title); } @@ -1731,7 +1734,7 @@ test "OSC: change_window_icon" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_icon); try testing.expectEqualStrings("ab", cmd.change_window_icon); } @@ -1744,7 +1747,7 @@ test "OSC: prompt_start" { const input = "133;A"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.aid == null); try testing.expect(cmd.prompt_start.redraw); @@ -1758,7 +1761,7 @@ test "OSC: prompt_start with single option" { const input = "133;A;aid=14"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); } @@ -1771,7 +1774,7 @@ test "OSC: prompt_start with redraw disabled" { const input = "133;A;redraw=0"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(!cmd.prompt_start.redraw); } @@ -1784,7 +1787,7 @@ test "OSC: prompt_start with redraw invalid value" { const input = "133;A;redraw=42"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.redraw); try testing.expect(cmd.prompt_start.kind == .primary); @@ -1798,7 +1801,7 @@ test "OSC: prompt_start with continuation" { const input = "133;A;k=c"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.kind == .continuation); } @@ -1811,7 +1814,7 @@ test "OSC: prompt_start with secondary" { const input = "133;A;k=s"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.kind == .secondary); } @@ -1824,7 +1827,7 @@ test "OSC: end_of_command no exit code" { const input = "133;D"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_command); } @@ -1836,7 +1839,7 @@ test "OSC: end_of_command with exit code" { const input = "133;D;25"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_command); try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); } @@ -1849,7 +1852,7 @@ test "OSC: prompt_end" { const input = "133;B"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_end); } @@ -1861,7 +1864,7 @@ test "OSC: end_of_input" { const input = "133;C"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_input); } @@ -1873,7 +1876,7 @@ test "OSC: get/set clipboard" { const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 's'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); @@ -1887,7 +1890,7 @@ test "OSC: get/set clipboard (optional parameter)" { const input = "52;;?"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 'c'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); @@ -1902,7 +1905,7 @@ test "OSC: get/set clipboard with allocator" { const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 's'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); @@ -1917,7 +1920,7 @@ test "OSC: clear clipboard" { const input = "52;;"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 'c'); try testing.expectEqualStrings("", cmd.clipboard_contents.data); @@ -1931,7 +1934,7 @@ test "OSC: report pwd" { const input = "7;file:///tmp/example"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .report_pwd); try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); } @@ -1943,7 +1946,7 @@ test "OSC: report pwd empty" { const input = "7;"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .report_pwd); try testing.expectEqualStrings("", cmd.report_pwd.value); } @@ -1956,7 +1959,7 @@ test "OSC: pointer cursor" { const input = "22;pointer"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .mouse_shape); try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); } @@ -1981,7 +1984,7 @@ test "OSC: OSC 9;1 ConEmu sleep" { const input = "9;1;420"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); @@ -1995,7 +1998,7 @@ test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" { const input = "9;1;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); @@ -2009,7 +2012,7 @@ test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" { const input = "9;1;12345"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); @@ -2023,7 +2026,7 @@ test "OSC: OSC 9;1 conemu sleep invalid input" { const input = "9;1;foo"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); @@ -2037,7 +2040,7 @@ test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" { const input = "9;1"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); @@ -2051,7 +2054,7 @@ test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" { const input = "9;1a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); @@ -2065,7 +2068,7 @@ test "OSC: OSC 9 show desktop notification" { const input = "9;Hello world"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("", cmd.show_desktop_notification.title); try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); @@ -2079,7 +2082,7 @@ test "OSC: OSC 9 show single character desktop notification" { const input = "9;H"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("", cmd.show_desktop_notification.title); try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); @@ -2093,7 +2096,7 @@ test "OSC: OSC 777 show desktop notification with title" { const input = "777;notify;Title;Body"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); @@ -2107,7 +2110,7 @@ test "OSC: OSC 9;2 ConEmu message box" { const input = "9;2;hello world"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_show_message_box); try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); } @@ -2120,7 +2123,7 @@ test "OSC: 9;2 ConEmu message box invalid input" { const input = "9;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); } @@ -2133,7 +2136,7 @@ test "OSC: 9;2 ConEmu message box empty message" { const input = "9;2;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_show_message_box); try testing.expectEqualStrings("", cmd.conemu_show_message_box); } @@ -2146,7 +2149,7 @@ test "OSC: 9;2 ConEmu message box spaces only message" { const input = "9;2; "; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_show_message_box); try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); } @@ -2159,7 +2162,7 @@ test "OSC: OSC 9;2 message box -> desktop notification 1" { const input = "9;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); @@ -2173,7 +2176,7 @@ test "OSC: OSC 9;2 message box -> desktop notification 2" { const input = "9;2a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); @@ -2187,7 +2190,7 @@ test "OSC: 9;3 ConEmu change tab title" { const input = "9;3;foo bar"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_change_tab_title); try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); } @@ -2200,7 +2203,7 @@ test "OSC: 9;3 ConEmu change tab title reset" { const input = "9;3;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; const expected_command: Command = .{ .conemu_change_tab_title = .reset }; try testing.expectEqual(expected_command, cmd); @@ -2214,7 +2217,7 @@ test "OSC: 9;3 ConEmu change tab title spaces only" { const input = "9;3; "; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_change_tab_title); try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); @@ -2228,7 +2231,7 @@ test "OSC: OSC 9;3 change tab title -> desktop notification 1" { const input = "9;3"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); @@ -2242,7 +2245,7 @@ test "OSC: OSC 9;3 message box -> desktop notification 2" { const input = "9;3a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); @@ -2256,7 +2259,7 @@ test "OSC: OSC 9;4 ConEmu progress set" { const input = "9;4;1;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expect(cmd.conemu_progress_report.progress == 100); @@ -2270,7 +2273,7 @@ test "OSC: OSC 9;4 ConEmu progress set overflow" { const input = "9;4;1;900"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expectEqual(100, cmd.conemu_progress_report.progress); @@ -2284,7 +2287,7 @@ test "OSC: OSC 9;4 ConEmu progress set single digit" { const input = "9;4;1;9"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expect(cmd.conemu_progress_report.progress == 9); @@ -2298,7 +2301,7 @@ test "OSC: OSC 9;4 ConEmu progress set double digit" { const input = "9;4;1;94"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expectEqual(94, cmd.conemu_progress_report.progress); @@ -2312,7 +2315,7 @@ test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" { const input = "9;4;1;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expectEqual(100, cmd.conemu_progress_report.progress); @@ -2326,7 +2329,7 @@ test "OSC: OSC 9;4 ConEmu progress remove with no progress" { const input = "9;4;0;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2340,7 +2343,7 @@ test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" { const input = "9;4;0;;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2354,7 +2357,7 @@ test "OSC: OSC 9;4 ConEmu progress remove ignores progress" { const input = "9;4;0;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2368,7 +2371,7 @@ test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" { const input = "9;4;0;100;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); } @@ -2381,7 +2384,7 @@ test "OSC: OSC 9;4 ConEmu progress error" { const input = "9;4;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .@"error"); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2395,7 +2398,7 @@ test "OSC: OSC 9;4 ConEmu progress error with progress" { const input = "9;4;2;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .@"error"); try testing.expect(cmd.conemu_progress_report.progress == 100); @@ -2409,7 +2412,7 @@ test "OSC: OSC 9;4 progress pause" { const input = "9;4;4"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .pause); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2423,7 +2426,7 @@ test "OSC: OSC 9;4 ConEmu progress pause with progress" { const input = "9;4;4;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .pause); try testing.expect(cmd.conemu_progress_report.progress == 100); @@ -2437,7 +2440,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 1" { const input = "9;4"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); @@ -2451,7 +2454,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 2" { const input = "9;4;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); @@ -2465,7 +2468,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 3" { const input = "9;4;5"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); @@ -2479,7 +2482,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 4" { const input = "9;4;5a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); @@ -2493,7 +2496,7 @@ test "OSC: OSC 9;5 ConEmu wait input" { const input = "9;5"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_wait_input); } @@ -2505,7 +2508,7 @@ test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" { const input = "9;5;foo"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_wait_input); } @@ -2529,7 +2532,7 @@ test "OSC: hyperlink" { const input = "8;;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } @@ -2542,7 +2545,7 @@ test "OSC: hyperlink with id set" { const input = "8;id=foo;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2556,7 +2559,7 @@ test "OSC: hyperlink with empty id" { const input = "8;id=;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqual(null, cmd.hyperlink_start.id); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2570,7 +2573,7 @@ test "OSC: hyperlink with incomplete key" { const input = "8;id;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqual(null, cmd.hyperlink_start.id); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2584,7 +2587,7 @@ test "OSC: hyperlink with empty key" { const input = "8;=value;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqual(null, cmd.hyperlink_start.id); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2598,7 +2601,7 @@ test "OSC: hyperlink with empty key and id" { const input = "8;=value:id=foo;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2624,7 +2627,7 @@ test "OSC: hyperlink end" { const input = "8;;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_end); } @@ -2638,7 +2641,7 @@ test "OSC: kitty color protocol" { const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len); { @@ -2720,7 +2723,7 @@ test "OSC: kitty color protocol double reset" { const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); p.reset(); @@ -2736,7 +2739,7 @@ test "OSC: kitty color protocol reset after invalid" { const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); p.reset(); @@ -2757,7 +2760,7 @@ test "OSC: kitty color protocol no key" { const input = "21;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); } @@ -2771,7 +2774,7 @@ test "OSC: 9;6: ConEmu guimacro 1" { const input = "9;6;a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_guimacro); try testing.expectEqualStrings("a", cmd.conemu_guimacro); } @@ -2785,7 +2788,7 @@ test "OSC: 9;6: ConEmu guimacro 2" { const input = "9;6;ab"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_guimacro); try testing.expectEqualStrings("ab", cmd.conemu_guimacro); } @@ -2799,7 +2802,7 @@ test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { const input = "9;6"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); } From f564ffa30b8b78ec9f480864edda96e4b63051c7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Sep 2025 15:15:11 -0700 Subject: [PATCH 072/319] lib-vt: expose ghostty_osc_end --- include/ghostty/vt.h | 34 +++++++++++++++++++++++++++++++--- src/lib_vt.zig | 1 + src/terminal/c/main.zig | 1 + src/terminal/c/osc.zig | 7 +++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index c784dcb0e..fc5eb1812 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -289,9 +289,37 @@ void ghostty_osc_reset(GhosttyOscParser parser); */ void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); -GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser); -GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); -bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData, void *result); +/** + * Finalize OSC parsing and retrieve the parsed command. + * + * Call this function after feeding all bytes of an OSC sequence to the parser + * using ghostty_osc_next() with the exception of the terminating character + * (ESC or ST). This function finalizes the parsing process and returns the + * parsed OSC command. + * + * The return value is never NULL. Invalid commands will return a command + * with type GHOSTTY_OSC_COMMAND_INVALID. + * + * The terminator parameter specifies the byte that terminated the OSC sequence + * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is + * preserved in the parsed command so that responses can use the same terminator + * format for better compatibility with the calling program. For commands that + * do not require a response, this parameter is ignored and the resulting + * command will not retain the terminator information. + * + * The returned command handle is valid until the next call to any + * `ghostty_osc_*` function with the same parser instance with the exception + * of command introspection functions such as `ghostty_osc_command_type`. + * + * @param parser The parser handle, must not be null. + * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) + * @return Handle to the parsed OSC command + */ +GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); + +// TODO +// GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); +// bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData, void *result); #ifdef __cplusplus } diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 6d9c042d8..4de7e390e 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -74,6 +74,7 @@ comptime { @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); @export(&c.osc_next, .{ .name = "ghostty_osc_next" }); @export(&c.osc_reset, .{ .name = "ghostty_osc_reset" }); + @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); } } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 1a25685ba..2779beebd 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -5,6 +5,7 @@ pub const osc_new = osc.new; pub const osc_free = osc.free; pub const osc_reset = osc.reset; pub const osc_next = osc.next; +pub const osc_end = osc.end; test { _ = osc; diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index 59761cfba..3859b3cc3 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -9,6 +9,9 @@ const Result = @import("result.zig").Result; /// C: GhosttyOscParser pub const Parser = ?*osc.Parser; +/// C: GhosttyOscCommand +pub const Command = ?*osc.Command; + pub fn new( alloc_: ?*const CAllocator, result: *Parser, @@ -37,6 +40,10 @@ pub fn next(parser_: Parser, byte: u8) callconv(.c) void { parser_.?.next(byte); } +pub fn end(parser_: Parser, terminator: u8) callconv(.c) Command { + return parser_.?.end(terminator); +} + test "osc" { const testing = std.testing; var p: Parser = undefined; From 89fc7139ae18591191d55c6dee276aa5e1153eae Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 28 Sep 2025 00:15:36 +0000 Subject: [PATCH 073/319] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 028f1a0d6..70bc28b75 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -116,8 +116,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - .hash = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + .hash = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 702df5026..e33d29164 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3": { + "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - "hash": "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + "hash": "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index c38b34e4d..513badffd 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3"; + name = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz"; - hash = "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz"; + hash = "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 1caa7b000..eefb990e4 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 46f6c950d..a5723ff38 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3", - "sha256": "24f63d339d1dfe7eab1b35add1a419214ec804c5abbb6200a9ef55bb5c7908cc" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", + "sha256": "99d854c4002a2b14515f0103da569a6d4a3d659a996bf31902c3b4f98f4beee4" }, { "type": "archive", From 9d33545a55e4a2ed3346408ef3acf50e80964e29 Mon Sep 17 00:00:00 2001 From: himura467 Date: Sun, 28 Sep 2025 19:20:00 +0900 Subject: [PATCH 074/319] feat: focus terminal in basic cases --- .../App Intents/FocusTerminalIntent.swift | 35 +++++++++++++++++++ .../Terminal/BaseTerminalController.swift | 10 ++++++ 2 files changed, 45 insertions(+) create mode 100644 macos/Sources/Features/App Intents/FocusTerminalIntent.swift diff --git a/macos/Sources/Features/App Intents/FocusTerminalIntent.swift b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift new file mode 100644 index 000000000..4e813e842 --- /dev/null +++ b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift @@ -0,0 +1,35 @@ +import AppKit +import AppIntents +import GhosttyKit + +struct FocusTerminalIntent: AppIntent { + static var title: LocalizedStringResource = "Focus Terminal" + static var description = IntentDescription("Move focus to an existing terminal.") + + @Parameter( + title: "Terminal", + description: "The terminal to focus.", + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + @MainActor + func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + + guard let surfaceView = terminal.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + return .result() + } + + controller.focusSurface(surfaceView) + return .result() + } +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 2de967daf..abf7d4a61 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -233,6 +233,16 @@ class BaseTerminalController: NSWindowController, return newView } + func focusSurface(_ view: Ghostty.SurfaceView) { + guard surfaceTree.contains(view) else { return } + + DispatchQueue.main.async { + Ghostty.moveFocus(to: view, from: self.focusedSurface) + view.window?.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + } + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. From cfe9f194542c521c51a3d1ab6f563fe5b318da77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Sep 2025 07:15:34 -0700 Subject: [PATCH 075/319] lib-vt: expose command type enum --- include/ghostty/vt.h | 27 +++++++++++---------------- src/lib_vt.zig | 1 + src/terminal/c/main.zig | 1 + src/terminal/c/osc.zig | 28 +++++++++++++++++++++++++++- src/terminal/main.zig | 2 +- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index fc5eb1812..5d80cb653 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -81,19 +81,6 @@ typedef enum { GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, } GhosttyOscCommandType; -/** - * OSC command data types. The values returned are documented - * on each type. - * */ -typedef enum { - /** - * The window title string. - * - * Type: const char* - * */ - GHOSTTY_OSC_DATA_WINDOW_TITLE, -} GhosttyOscCommandData; - //------------------------------------------------------------------- // Allocator Interface @@ -317,9 +304,17 @@ void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); */ GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); -// TODO -// GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); -// bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData, void *result); +/** + * Get the type of an OSC command. + * + * Returns the type identifier for the given OSC command. This can be used + * to determine what kind of command was parsed and what data might be + * available from it. + * + * @param command The OSC command handle to query (may be NULL) + * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL + */ +GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); #ifdef __cplusplus } diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 4de7e390e..37ab7ae68 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -75,6 +75,7 @@ comptime { @export(&c.osc_next, .{ .name = "ghostty_osc_next" }); @export(&c.osc_reset, .{ .name = "ghostty_osc_reset" }); @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); + @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); } } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 2779beebd..f32dd226f 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -6,6 +6,7 @@ pub const osc_free = osc.free; pub const osc_reset = osc.reset; pub const osc_next = osc.next; pub const osc_end = osc.end; +pub const osc_command_type = osc.commandType; test { _ = osc; diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index 3859b3cc3..c04626b69 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -44,7 +44,12 @@ pub fn end(parser_: Parser, terminator: u8) callconv(.c) Command { return parser_.?.end(terminator); } -test "osc" { +pub fn commandType(command_: Command) callconv(.c) osc.Command.Key { + const command = command_ orelse return .invalid; + return command.*; +} + +test "alloc" { const testing = std.testing; var p: Parser = undefined; try testing.expectEqual(Result.success, new( @@ -53,3 +58,24 @@ test "osc" { )); free(p); } + +test "command type null" { + const testing = std.testing; + try testing.expectEqual(.invalid, commandType(null)); +} + +test "command type" { + const testing = std.testing; + var p: Parser = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &p, + )); + defer free(p); + + p.next('0'); + p.next(';'); + p.next('a'); + const cmd = p.end(0); + try testing.expectEqual(.change_window_title, commandType(cmd)); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 70b5742cd..7403ff309 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -64,7 +64,7 @@ pub const isSafePaste = sanitize.isSafePaste; /// This is set to true when we're building the C library. pub const is_c_lib = @import("build_options.zig").is_c_lib; -pub const c_api = @import("c/main.zig"); +pub const c_api = if (is_c_lib) @import("c/main.zig") else void; test { @import("std").testing.refAllDecls(@This()); From a76297058fd2da5c8452dcaf356dfcb6343e1574 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Sep 2025 07:24:08 -0700 Subject: [PATCH 076/319] example/c-vt: parse a full OSC command --- example/c-vt/src/main.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 1eaa659d2..00ea3618f 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,4 +1,5 @@ #include +#include #include int main() { @@ -6,6 +7,19 @@ int main() { if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { return 1; } + + // Setup change window title command to change the title to "a" + ghostty_osc_next(parser, '0'); + ghostty_osc_next(parser, ';'); + ghostty_osc_next(parser, 'a'); + + // End parsing and get command + GhosttyOscCommand command = ghostty_osc_end(parser, 0); + + // Get and print command type + GhosttyOscCommandType type = ghostty_osc_command_type(command); + printf("Command type: %d\n", type); + ghostty_osc_free(parser); return 0; } From 8151f4bbf5ef90fc404a096c0bc33f910bc1311f Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 00:00:41 +0900 Subject: [PATCH 077/319] feat: focusSurface for quick terminal --- .../QuickTerminal/QuickTerminalController.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 65186c5d7..c7080d7de 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -247,6 +247,18 @@ class QuickTerminalController: BaseTerminalController { // MARK: Base Controller Overrides + override func focusSurface(_ view: Ghostty.SurfaceView) { + if visible { + // If we're visible, we just focus the surface as normal. + super.focusSurface(view) + return + } + animateIn() + DispatchQueue.main.asyncAfter(deadline: .now() + derivedConfig.quickTerminalAnimationDuration) { + super.focusSurface(view) + } + } + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) From 337ecdd0b36ae6da34adf6c222cec23ec79158f1 Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 00:02:43 +0900 Subject: [PATCH 078/319] refactor(focusSurface): check app status in advance --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index abf7d4a61..ec9ddf83b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -239,7 +239,9 @@ class BaseTerminalController: NSWindowController, DispatchQueue.main.async { Ghostty.moveFocus(to: view, from: self.focusedSurface) view.window?.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } } } From 7ab0a7814b82d5867aff2caabcb2de890aa2ff34 Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 00:40:47 +0900 Subject: [PATCH 079/319] docs(BaseTerminalController) --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ec9ddf83b..c1c350f9d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -233,9 +233,12 @@ class BaseTerminalController: NSWindowController, return newView } + /// Move focus to a surface view. func focusSurface(_ view: Ghostty.SurfaceView) { + // Check if target surface is in our tree guard surfaceTree.contains(view) else { return } + // Move focus to the target surface and activate the window/app DispatchQueue.main.async { Ghostty.moveFocus(to: view, from: self.focusedSurface) view.window?.makeKeyAndOrderFront(nil) From a6dd7bbeee5b6d879732aa31e25f07c47a9694cd Mon Sep 17 00:00:00 2001 From: himura467 Date: Mon, 29 Sep 2025 02:02:42 +0900 Subject: [PATCH 080/319] refactor: improve asynchronous delay by delegating window/app activation process to animateIn --- .../QuickTerminal/QuickTerminalController.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index c7080d7de..17650b5c6 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -253,10 +253,14 @@ class QuickTerminalController: BaseTerminalController { super.focusSurface(view) return } - animateIn() - DispatchQueue.main.asyncAfter(deadline: .now() + derivedConfig.quickTerminalAnimationDuration) { - super.focusSurface(view) + // Check if target surface belongs to this quick terminal + guard surfaceTree.contains(view) else { return } + // Set the target surface as focused before animation + DispatchQueue.main.async { + Ghostty.moveFocus(to: view, from: self.focusedSurface) } + // Animation completion handler will handle window/app activation + animateIn() } override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { From f614fb7c1b0bcf0f7256aea0040ec5425be09929 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Sep 2025 13:59:13 -0700 Subject: [PATCH 081/319] build: use build options to configure terminal C ABI mode Fixes various issues: - C ABI detection was faulty, which caused some Zig programs to use the C ABI mode and some C programs not to. Let's be explicit. - Unit tests now tests C ABI mode. - Build binary no longer rebuilds on any terminal change (a regression). - Zig programs can choose to depend on the C ABI version of the terminal lib by using the `ghostty-vt-c` module. --- build.zig | 9 +++++++ src/build/Config.zig | 1 + src/build/GhosttyLibVt.zig | 2 +- src/build/GhosttyZig.zig | 45 +++++++++++++++++++++++++++++----- src/lib_vt.zig | 9 ++++--- src/terminal/build_options.zig | 9 ++++--- src/terminal/c/osc.zig | 8 +++--- src/terminal/main.zig | 9 ++++--- src/terminal/osc.zig | 4 +-- 9 files changed, 72 insertions(+), 24 deletions(-) diff --git a/build.zig b/build.zig index 008fc849e..62fa77511 100644 --- a/build.zig +++ b/build.zig @@ -255,6 +255,15 @@ pub fn build(b: *std.Build) !void { }); const mod_vt_test_run = b.addRunArtifact(mod_vt_test); test_lib_vt_step.dependOn(&mod_vt_test_run.step); + + const mod_vt_c_test = b.addTest(.{ + .root_module = mod.vt_c, + .target = config.target, + .optimize = config.optimize, + .filters = test_filters, + }); + const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test); + test_lib_vt_step.dependOn(&mod_vt_c_test_run.step); } // Tests diff --git a/src/build/Config.zig b/src/build/Config.zig index 474674d3a..0b7dae14d 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -498,6 +498,7 @@ pub fn terminalOptions(self: *const Config) TerminalBuildOptions { .artifact = .ghostty, .simd = self.simd, .oniguruma = true, + .c_abi = false, .slow_runtime_safety = switch (self.optimize) { .Debug => true, .ReleaseSafe, diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 0029d6756..80f2bf9ad 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -26,7 +26,7 @@ pub fn initShared( const target = zig.vt.resolved_target.?; const lib = b.addSharedLibrary(.{ .name = "ghostty-vt", - .root_module = zig.vt, + .root_module = zig.vt_c, }); lib.installHeader( b.path("include/ghostty/vt.h"), diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index f175eb957..a8d2726bc 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -5,18 +5,17 @@ const GhosttyZig = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); +const TerminalBuildOptions = @import("../terminal/build_options.zig").Options; +/// The `_c`-suffixed modules are built with the C ABI enabled. vt: *std.Build.Module, +vt_c: *std.Build.Module, pub fn init( b: *std.Build, cfg: *const Config, deps: *const SharedDeps, ) !GhosttyZig { - // General build options - const general_options = b.addOptions(); - try cfg.addOptions(general_options); - // Terminal module build options var vt_options = cfg.terminalOptions(); vt_options.artifact = .lib; @@ -25,7 +24,41 @@ pub fn init( // conditionally do this. vt_options.oniguruma = false; - const vt = b.addModule("ghostty-vt", .{ + return .{ + .vt = try initVt( + "ghostty-vt", + b, + cfg, + deps, + vt_options, + ), + + .vt_c = try initVt( + "ghostty-vt-c", + b, + cfg, + deps, + options: { + var dup = vt_options; + dup.c_abi = true; + break :options dup; + }, + ), + }; +} + +fn initVt( + name: []const u8, + b: *std.Build, + cfg: *const Config, + deps: *const SharedDeps, + vt_options: TerminalBuildOptions, +) !*std.Build.Module { + // General build options + const general_options = b.addOptions(); + try cfg.addOptions(general_options); + + const vt = b.addModule(name, .{ .root_source_file = b.path("src/lib_vt.zig"), .target = cfg.target, .optimize = cfg.optimize, @@ -45,5 +78,5 @@ pub fn init( try SharedDeps.addSimd(b, vt, null); } - return .{ .vt = vt }; + return vt; } diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 37ab7ae68..b7ef9459a 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -7,6 +7,7 @@ //! by thousands of users for years. However, the API itself (functions, //! types, etc.) may change without warning. We're working on stabilizing //! this in the future. +const lib = @This(); // The public API below reproduces a lot of terminal/main.zig but // is separate because (1) we need our root file to be in `src/` @@ -68,7 +69,7 @@ pub const Attribute = terminal.Attribute; comptime { // If we're building the C library (vs. the Zig module) then // we want to reference the C API so that it gets exported. - if (terminal.is_c_lib) { + if (@import("root") == lib) { const c = terminal.c_api; @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); @@ -81,8 +82,8 @@ comptime { test { _ = terminal; - - // Tests always test the C API and shared C functions - _ = terminal.c_api; _ = @import("lib/main.zig"); + if (comptime terminal.options.c_abi) { + _ = terminal.c_api; + } } diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 2085e2243..e209a56fa 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -1,8 +1,6 @@ const std = @import("std"); -/// True if we're building the C library libghostty-vt. -pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); - +/// Options set by Zig build.zig and exposed via `terminal_options`. pub const Options = struct { /// The target artifact to build. This will gate some functionality. artifact: Artifact, @@ -26,6 +24,10 @@ pub const Options = struct { /// generally be disabled in production builds. slow_runtime_safety: bool, + /// Force C ABI mode on or off. If not set, then it will be set based on + /// Options. + c_abi: bool, + /// Add the required build options for the terminal module. pub fn add( self: Options, @@ -34,6 +36,7 @@ pub const Options = struct { ) void { const opts = b.addOptions(); opts.addOption(Artifact, "artifact", self.artifact); + opts.addOption(bool, "c_abi", self.c_abi); opts.addOption(bool, "oniguruma", self.oniguruma); opts.addOption(bool, "simd", self.simd); opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety); diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index c04626b69..d1998f4e1 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -73,9 +73,9 @@ test "command type" { )); defer free(p); - p.next('0'); - p.next(';'); - p.next('a'); - const cmd = p.end(0); + next(p, '0'); + next(p, ';'); + next(p, 'a'); + const cmd = end(p, 0); try testing.expectEqual(.change_window_title, commandType(cmd)); } diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 7403ff309..6875ba89d 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -1,5 +1,4 @@ const builtin = @import("builtin"); -const build_options = @import("terminal_options"); const charsets = @import("charsets.zig"); const sanitize = @import("sanitize.zig"); @@ -21,7 +20,7 @@ pub const page = @import("page.zig"); pub const parse_table = @import("parse_table.zig"); pub const search = @import("search.zig"); pub const size = @import("size.zig"); -pub const tmux = if (build_options.tmux_control_mode) @import("tmux.zig") else struct {}; +pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {}; pub const x11_color = @import("x11_color.zig"); pub const Charset = charsets.Charset; @@ -62,9 +61,11 @@ pub const Attribute = sgr.Attribute; pub const isSafePaste = sanitize.isSafePaste; +pub const Options = @import("build_options.zig").Options; +pub const options = @import("terminal_options"); + /// This is set to true when we're building the C library. -pub const is_c_lib = @import("build_options.zig").is_c_lib; -pub const c_api = if (is_c_lib) @import("c/main.zig") else void; +pub const c_api = if (options.c_abi) @import("c/main.zig") else void; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 71d2f8598..20b22d1ef 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -7,11 +7,11 @@ const osc = @This(); const std = @import("std"); const builtin = @import("builtin"); +const build_options = @import("terminal_options"); const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; const LibEnum = @import("../lib/enum.zig").Enum; -const is_c_lib = @import("build_options.zig").is_c_lib; const RGB = @import("color.zig").RGB; const kitty_color = @import("kitty/color.zig"); const osc_color = @import("osc/color.zig"); @@ -175,7 +175,7 @@ pub const Command = union(Key) { conemu_guimacro: []const u8, pub const Key = LibEnum( - if (is_c_lib) .c else .zig, + if (build_options.c_abi) .c else .zig, // NOTE: Order matters, see LibEnum documentation. &.{ "invalid", From 41c1c6b3e633ae63a8499eaaf2351ba631afe64c Mon Sep 17 00:00:00 2001 From: Bernd Kaiser Date: Mon, 29 Sep 2025 14:44:20 +0200 Subject: [PATCH 082/319] gtk: make Enter confirm "Change Terminal Title" --- src/apprt/gtk/ui/1.5/surface-title-dialog.blp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/ui/1.5/surface-title-dialog.blp b/src/apprt/gtk/ui/1.5/surface-title-dialog.blp index 24ae26f37..90d9f9c0b 100644 --- a/src/apprt/gtk/ui/1.5/surface-title-dialog.blp +++ b/src/apprt/gtk/ui/1.5/surface-title-dialog.blp @@ -6,11 +6,14 @@ template $GhosttySurfaceTitleDialog: Adw.AlertDialog { body: _("Leave blank to restore the default title."); responses [ - cancel: _("Cancel") suggested, - ok: _("OK") destructive, + cancel: _("Cancel"), + ok: _("OK") suggested, ] + default-response: "ok"; focus-widget: entry; - extra-child: Entry entry {}; + extra-child: Entry entry { + activates-default: true; + }; } From 3bc07c24aaac4cf58cbb845bc54c8c1cbf2ffa0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Sep 2025 14:55:15 -0700 Subject: [PATCH 083/319] lib-vt: OSC data extraction boilerplate This also changes OSC strings to be null-terminated to ease lib-vt integration. This shouldn't have any practical effect on terminal performance, but it does lower the maximum length of OSC strings by 1 since we always reserve space for the null terminator. --- example/c-vt/src/main.c | 15 +++++- include/ghostty/vt.h | 69 ++++++++++++++++++++++++++++ src/inspector/termio.zig | 8 +++- src/lib_vt.zig | 1 + src/terminal/c/main.zig | 1 + src/terminal/c/osc.zig | 53 +++++++++++++++++++++- src/terminal/osc.zig | 98 ++++++++++++++++++++++++++++++---------- 7 files changed, 216 insertions(+), 29 deletions(-) diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 00ea3618f..b1297d7a7 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,5 +1,6 @@ #include #include +#include #include int main() { @@ -8,10 +9,13 @@ int main() { return 1; } - // Setup change window title command to change the title to "a" + // Setup change window title command to change the title to "hello" ghostty_osc_next(parser, '0'); ghostty_osc_next(parser, ';'); - ghostty_osc_next(parser, 'a'); + const char *title = "hello"; + for (size_t i = 0; i < strlen(title); i++) { + ghostty_osc_next(parser, title[i]); + } // End parsing and get command GhosttyOscCommand command = ghostty_osc_end(parser, 0); @@ -20,6 +24,13 @@ int main() { GhosttyOscCommandType type = ghostty_osc_command_type(command); printf("Command type: %d\n", type); + // Extract and print the title + if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) { + printf("Extracted title: %s\n", title); + } else { + printf("Failed to extract title\n"); + } + ghostty_osc_free(parser); return 0; } diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 5d80cb653..33ff2a961 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -32,6 +32,8 @@ extern "C" { * be used to parse the contents of OSC sequences. This isn't a full VT * parser; it is only the OSC parser component. This is useful if you have * a parser already and want to only extract and handle OSC sequences. + * + * @ingroup osc */ typedef struct GhosttyOscParser *GhosttyOscParser; @@ -41,6 +43,8 @@ typedef struct GhosttyOscParser *GhosttyOscParser; * This handle represents a parsed OSC (Operating System Command) command. * The command can be queried for its type and associated data using * `ghostty_osc_command_type` and `ghostty_osc_command_data`. + * + * @ingroup osc */ typedef struct GhosttyOscCommand *GhosttyOscCommand; @@ -56,6 +60,8 @@ typedef enum { /** * OSC command types. + * + * @ingroup osc */ typedef enum { GHOSTTY_OSC_COMMAND_INVALID = 0, @@ -81,6 +87,31 @@ typedef enum { GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, } GhosttyOscCommandType; +/** + * OSC command data types. + * + * These values specify what type of data to extract from an OSC command + * using `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_OSC_DATA_INVALID = 0, + + /** + * Window title string data. + * + * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE + * + * Output type: const char ** (pointer to null-terminated string) + * + * Lifetime: Valid until the next call to any ghostty_osc_* function with + * the same parser instance. Memory is owned by the parser. + */ + GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, +} GhosttyOscCommandData; + //------------------------------------------------------------------- // Allocator Interface @@ -227,6 +258,27 @@ typedef struct { //------------------------------------------------------------------- // Functions +/** @defgroup osc OSC Parser + * + * OSC (Operating System Command) sequence parser and command handling. + * + * The parser operates in a streaming fashion, processing input byte-by-byte + * to handle OSC sequences that may arrive in fragments across multiple reads. + * This interface makes it easy to integrate into most environments and avoids + * over-allocating buffers. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_osc_new() + * 2. Feed bytes to the parser using ghostty_osc_next() + * 3. Finalize parsing with ghostty_osc_end() to get the command + * 4. Query command type and extract data using ghostty_osc_command_type() + * and ghostty_osc_command_data() + * 5. Free the parser with ghostty_osc_free() when done + * + * @{ + */ + /** * Create a new OSC parser instance. * @@ -316,6 +368,23 @@ GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); */ GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); +/** + * Extract data from an OSC command. + * + * Extracts typed data from the given OSC command based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid command types, output types, and memory + * safety information are documented in the `GhosttyOscCommandData` enum. + * + * @param command The OSC command handle to query (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return true if data extraction was successful, false otherwise + */ +bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); + +/** @} */ // end of osc group + #ifdef __cplusplus } #endif diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 5ab9d3cd4..49ab00ecd 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -197,7 +197,9 @@ pub const VTEvent = struct { ) !void { switch (@TypeOf(v)) { void => {}, - []const u8 => try md.put("data", try alloc.dupeZ(u8, v)), + []const u8, + [:0]const u8, + => try md.put("data", try alloc.dupeZ(u8, v)), else => |T| switch (@typeInfo(T)) { .@"struct" => |info| inline for (info.fields) |field| { try encodeMetadataSingle( @@ -284,7 +286,9 @@ pub const VTEvent = struct { try std.fmt.allocPrintZ(alloc, "{}", .{value}), ), - []const u8 => try md.put(key, try alloc.dupeZ(u8, value)), + []const u8, + [:0]const u8, + => try md.put(key, try alloc.dupeZ(u8, value)), else => |T| { @compileLog(T); diff --git a/src/lib_vt.zig b/src/lib_vt.zig index b7ef9459a..763f17f98 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -77,6 +77,7 @@ comptime { @export(&c.osc_reset, .{ .name = "ghostty_osc_reset" }); @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); + @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); } } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index f32dd226f..68fd77edd 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -7,6 +7,7 @@ pub const osc_reset = osc.reset; pub const osc_next = osc.next; pub const osc_end = osc.end; pub const osc_command_type = osc.commandType; +pub const osc_command_data = osc.commandData; test { _ = osc; diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index d1998f4e1..8b6a8409c 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -49,6 +49,51 @@ pub fn commandType(command_: Command) callconv(.c) osc.Command.Key { return command.*; } +/// C: GhosttyOscCommandData +pub const CommandData = enum(c_int) { + invalid = 0, + change_window_title_str = 1, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: CommandData) type { + return switch (self) { + .invalid => void, + .change_window_title_str => [*:0]const u8, + }; + } +}; + +pub fn commandData( + command_: Command, + data: CommandData, + out: ?*anyopaque, +) callconv(.c) bool { + return switch (data) { + inline else => |comptime_data| commandDataTyped( + command_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn commandDataTyped( + command_: Command, + comptime data: CommandData, + out: *data.OutType(), +) bool { + const command = command_.?; + switch (data) { + .invalid => return false, + .change_window_title_str => switch (command.*) { + .change_window_title => |v| out.* = v.ptr, + else => return false, + }, + } + + return true; +} + test "alloc" { const testing = std.testing; var p: Parser = undefined; @@ -64,7 +109,7 @@ test "command type null" { try testing.expectEqual(.invalid, commandType(null)); } -test "command type" { +test "change window title" { const testing = std.testing; var p: Parser = undefined; try testing.expectEqual(Result.success, new( @@ -73,9 +118,15 @@ test "command type" { )); defer free(p); + // Parse it next(p, '0'); next(p, ';'); next(p, 'a'); const cmd = end(p, 0); try testing.expectEqual(.change_window_title, commandType(cmd)); + + // Extract the title + var title: [*:0]const u8 = undefined; + try testing.expect(commandData(cmd, .change_window_title_str, @ptrCast(&title))); + try testing.expectEqualStrings("a", std.mem.span(title)); } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 20b22d1ef..800257c3d 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -26,19 +26,19 @@ pub const Command = union(Key) { /// Set the window title of the terminal /// - /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 + /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 /// with each code unit further encoded with two hex digits). /// /// If title mode 2 is set or the terminal is setup for unconditional /// utf-8 titles text is interpreted as utf-8. Else text is interpreted /// as latin1. - change_window_title: []const u8, + change_window_title: [:0]const u8, /// Set the icon of the terminal window. The name of the icon is not /// well defined, so this is currently ignored by Ghostty at the time /// of writing this. We just parse it so that we don't get parse errors /// in the log. - change_window_icon: []const u8, + change_window_icon: [:0]const u8, /// First do a fresh-line. Then start a new command, and enter prompt mode: /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a @@ -54,7 +54,7 @@ pub const Command = union(Key) { /// - secondary: a non-editable continuation line /// - right: a right-aligned prompt that may need adjustment during reflow prompt_start: struct { - aid: ?[]const u8 = null, + aid: ?[:0]const u8 = null, kind: enum { primary, continuation, secondary, right } = .primary, redraw: bool = true, }, @@ -96,7 +96,7 @@ pub const Command = union(Key) { /// contents is set on the clipboard. clipboard_contents: struct { kind: u8, - data: []const u8, + data: [:0]const u8, }, /// OSC 7. Reports the current working directory of the shell. This is @@ -106,7 +106,7 @@ pub const Command = union(Key) { report_pwd: struct { /// The reported pwd value. This is not checked for validity. It should /// be a file URL but it is up to the caller to utilize this value. - value: []const u8, + value: [:0]const u8, }, /// OSC 22. Set the mouse shape. There doesn't seem to be a standard @@ -114,7 +114,7 @@ pub const Command = union(Key) { /// are moving towards using the W3C CSS cursor names. For OSC parsing, /// we just parse whatever string is given. mouse_shape: struct { - value: []const u8, + value: [:0]const u8, }, /// OSC color operations to set, reset, or report color settings. Some OSCs @@ -138,14 +138,14 @@ pub const Command = union(Key) { /// Show a desktop notification (OSC 9 or OSC 777) show_desktop_notification: struct { - title: []const u8, - body: []const u8, + title: [:0]const u8, + body: [:0]const u8, }, /// Start a hyperlink (OSC 8) hyperlink_start: struct { - id: ?[]const u8 = null, - uri: []const u8, + id: ?[:0]const u8 = null, + uri: [:0]const u8, }, /// End a hyperlink (OSC 8) @@ -157,12 +157,12 @@ pub const Command = union(Key) { }, /// ConEmu show GUI message box (OSC 9;2) - conemu_show_message_box: []const u8, + conemu_show_message_box: [:0]const u8, /// ConEmu change tab title (OSC 9;3) conemu_change_tab_title: union(enum) { reset, - value: []const u8, + value: [:0]const u8, }, /// ConEmu progress report (OSC 9;4) @@ -172,7 +172,7 @@ pub const Command = union(Key) { conemu_wait_input, /// ConEmu GUI macro (OSC 9;6) - conemu_guimacro: []const u8, + conemu_guimacro: [:0]const u8, pub const Key = LibEnum( if (build_options.c_abi) .c else .zig, @@ -305,7 +305,7 @@ pub const Parser = struct { /// Temporary state that is dependent on the current state. temp_state: union { /// Current string parameter being populated - str: *[]const u8, + str: *[:0]const u8, /// Current numeric parameter being populated num: u16, @@ -498,7 +498,10 @@ pub const Parser = struct { // If our buffer is full then we're invalid, so we set our state // accordingly and indicate the sequence is incomplete so that we // don't accidentally issue a command when ending. - if (self.buf_idx >= self.buf.len) { + // + // We always keep space for 1 byte at the end to null-terminate + // values. + if (self.buf_idx >= self.buf.len - 1) { if (self.state != .invalid) { log.warn( "OSC sequence too long (> {d}), ignoring. state={}", @@ -1037,7 +1040,8 @@ pub const Parser = struct { .notification_title => switch (c) { ';' => { - self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1]; + self.buf[self.buf_idx - 1] = 0; + self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1 :0]; self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; self.buf_start = self.buf_idx; self.state = .string; @@ -1406,7 +1410,8 @@ pub const Parser = struct { fn endHyperlink(self: *Parser) void { switch (self.command) { .hyperlink_start => |*v| { - const value = self.buf[self.buf_start..self.buf_idx]; + self.buf[self.buf_idx] = 0; + const value = self.buf[self.buf_start..self.buf_idx :0]; if (v.id == null and value.len == 0) { self.command = .{ .hyperlink_end = {} }; return; @@ -1420,10 +1425,12 @@ pub const Parser = struct { } fn endHyperlinkOptionValue(self: *Parser) void { - const value = if (self.buf_start == self.buf_idx) + const value: [:0]const u8 = if (self.buf_start == self.buf_idx) "" - else - self.buf[self.buf_start .. self.buf_idx - 1]; + else buf: { + self.buf[self.buf_idx - 1] = 0; + break :buf self.buf[self.buf_start .. self.buf_idx - 1 :0]; + }; if (mem.eql(u8, self.temp_state.key, "id")) { switch (self.command) { @@ -1438,7 +1445,11 @@ pub const Parser = struct { } fn endSemanticOptionValue(self: *Parser) void { - const value = self.buf[self.buf_start..self.buf_idx]; + const value = value: { + self.buf[self.buf_idx] = 0; + defer self.buf_idx += 1; + break :value self.buf[self.buf_start..self.buf_idx :0]; + }; if (mem.eql(u8, self.temp_state.key, "aid")) { switch (self.command) { @@ -1495,7 +1506,9 @@ pub const Parser = struct { } fn endString(self: *Parser) void { - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx]; + self.buf[self.buf_idx] = 0; + defer self.buf_idx += 1; + self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx :0]; } fn endConEmuSleepValue(self: *Parser) void { @@ -1589,8 +1602,15 @@ pub const Parser = struct { } fn endAllocableString(self: *Parser) void { + const alloc = self.alloc.?; const list = self.buf_dynamic.?; - self.temp_state.str.* = list.items; + list.append(alloc, 0) catch { + log.warn("allocation failed on allocable string termination", .{}); + self.temp_state.str.* = ""; + return; + }; + + self.temp_state.str.* = list.items[0 .. list.items.len - 1 :0]; } /// End the sequence and return the command, if any. If the return value @@ -1976,6 +1996,36 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } +test "OSC: one shorter than buffer length" { + const testing = std.testing; + + var p: Parser = .init(); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings(title, cmd.change_window_title); +} + +test "OSC: exactly at buffer length" { + const testing = std.testing; + + var p: Parser = .init(); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - prefix.len); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + // This should be null because we always reserve space for a null terminator. + try testing.expect(p.end(null) == null); + try testing.expect(p.complete == false); +} + test "OSC: OSC 9;1 ConEmu sleep" { const testing = std.testing; From 3a95920edf01dbec33d6a0ab607b5f47b586ea51 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 08:31:27 -0700 Subject: [PATCH 084/319] build: add Dockerfile to generate and server libghostty c docs --- src/build/docker/lib-c-docs/Dockerfile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/build/docker/lib-c-docs/Dockerfile diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile new file mode 100644 index 000000000..ce9f3e4c0 --- /dev/null +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -0,0 +1,21 @@ +#-------------------------------------------------------------------- +# Generate documentation with Doxygen +#-------------------------------------------------------------------- +FROM ubuntu:24.04 AS builder +RUN apt-get update && apt-get install -y \ + doxygen \ + graphviz \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /ghostty +COPY include/ ./include/ +COPY Doxyfile ./ +RUN mkdir -p zig-out/share/ghostty/doc/libghostty +RUN doxygen + +#-------------------------------------------------------------------- +# Host the static HTML +#-------------------------------------------------------------------- +FROM nginx:alpine AS runtime +COPY --from=builder /ghostty/zig-out/share/ghostty/doc/libghostty /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] From 904e09a1daa0caa04f05e7cf0de733cc7b0661f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 08:46:05 -0700 Subject: [PATCH 085/319] build: add NOINDEX argument for libghostty-vt docs --- src/build/docker/lib-c-docs/Dockerfile | 35 +++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index ce9f3e4c0..782e99994 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -2,6 +2,9 @@ # Generate documentation with Doxygen #-------------------------------------------------------------------- FROM ubuntu:24.04 AS builder + +# Build argument for noindex header +ARG ADD_NOINDEX_HEADER=false RUN apt-get update && apt-get install -y \ doxygen \ graphviz \ @@ -16,6 +19,36 @@ RUN doxygen # Host the static HTML #-------------------------------------------------------------------- FROM nginx:alpine AS runtime + +# Pass build arg to runtime stage +ARG ADD_NOINDEX_HEADER=false +ENV ADD_NOINDEX_HEADER=$ADD_NOINDEX_HEADER + +# Copy documentation COPY --from=builder /ghostty/zig-out/share/ghostty/doc/libghostty /usr/share/nginx/html + +# Create entrypoint script +RUN cat > /entrypoint.sh << 'EOF' +#!/bin/sh +if [ "$ADD_NOINDEX_HEADER" = "true" ]; then + cat > /etc/nginx/conf.d/noindex.conf << 'INNER_EOF' +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html; + add_header X-Robots-Tag "noindex, nofollow" always; + } +} +INNER_EOF + + # Remove default server config + rm -f /etc/nginx/conf.d/default.conf +fi +exec nginx -g "daemon off;" +EOF + +RUN chmod +x /entrypoint.sh + EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +CMD ["/entrypoint.sh"] From 16077f054296080086731e6bd523e6d993322763 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 08:51:55 -0700 Subject: [PATCH 086/319] build: move entrypoint out to separate file --- src/build/docker/lib-c-docs/Dockerfile | 25 ++--------------------- src/build/docker/lib-c-docs/entrypoint.sh | 16 +++++++++++++++ 2 files changed, 18 insertions(+), 23 deletions(-) create mode 100755 src/build/docker/lib-c-docs/entrypoint.sh diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index 782e99994..f30dfba90 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -24,30 +24,9 @@ FROM nginx:alpine AS runtime ARG ADD_NOINDEX_HEADER=false ENV ADD_NOINDEX_HEADER=$ADD_NOINDEX_HEADER -# Copy documentation +# Copy documentation and entrypoint script COPY --from=builder /ghostty/zig-out/share/ghostty/doc/libghostty /usr/share/nginx/html - -# Create entrypoint script -RUN cat > /entrypoint.sh << 'EOF' -#!/bin/sh -if [ "$ADD_NOINDEX_HEADER" = "true" ]; then - cat > /etc/nginx/conf.d/noindex.conf << 'INNER_EOF' -server { - listen 80; - location / { - root /usr/share/nginx/html; - index index.html; - add_header X-Robots-Tag "noindex, nofollow" always; - } -} -INNER_EOF - - # Remove default server config - rm -f /etc/nginx/conf.d/default.conf -fi -exec nginx -g "daemon off;" -EOF - +COPY src/build/docker/lib-c-docs/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh EXPOSE 80 diff --git a/src/build/docker/lib-c-docs/entrypoint.sh b/src/build/docker/lib-c-docs/entrypoint.sh new file mode 100755 index 000000000..928d6e163 --- /dev/null +++ b/src/build/docker/lib-c-docs/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +if [ "$ADD_NOINDEX_HEADER" = "true" ]; then + cat > /etc/nginx/conf.d/noindex.conf << 'EOF' +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html; + add_header X-Robots-Tag "noindex, nofollow" always; + } +} +EOF + # Remove default server config + rm -f /etc/nginx/conf.d/default.conf +fi +exec nginx -g "daemon off;" From 15670a77f3010961f94303fcb8f94e87605cdba2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 08:59:16 -0700 Subject: [PATCH 087/319] lib-vt: document main html page --- include/ghostty/vt.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 33ff2a961..a33d2c9ee 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -11,6 +11,26 @@ * stable and is definitely going to change. */ +/** + * @mainpage libghostty-vt - Virtual Terminal Sequence Parser + * + * libghostty-vt is a C library which implements a modern terminal emulator, + * extracted from the [Ghostty](https://ghostty.org) terminal emulator. + * + * libghostty-vt contains the logic for handling the core parts of a terminal + * emulator: parsing terminal escape sequences and maintaining terminal state. + * It can handle scrollback, line wrapping, reflow on resize, and more. + * + * @warning This library is currently in development and the API is not yet stable. + * Breaking changes are expected in future versions. Use with caution in production code. + * + * @section groups_sec API Reference + * + * The API is organized into the following groups: + * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H From cc0b7f74fdbbc56ae0005c0a76ee21731355d9e8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 09:12:09 -0700 Subject: [PATCH 088/319] lib-vt: document allocators --- include/ghostty/vt.h | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index a33d2c9ee..4b930a96f 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -28,6 +28,7 @@ * * The API is organized into the following groups: * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref allocator "Memory Management" - Memory management and custom allocators * */ @@ -135,12 +136,50 @@ typedef enum { //------------------------------------------------------------------- // Allocator Interface +/** @defgroup allocator Memory Management + * + * libghostty-vt does require memory allocation for various operations, + * but is resilient to allocation failures and will gracefully handle + * out-of-memory situations by returning error codes. + * + * The exact memory management semantics are documented in the relevant + * functions and data structures. + * + * libghostty-vt uses explicit memory allocation via an allocator + * interface provided by GhosttyAllocator. The interface is based on the + * [Zig](https://ziglang.org) allocator interface, since this has been + * shown to be a flexible and powerful interface in practice and enables + * a wide variety of allocation strategies. + * + * **For the common case, you can pass NULL as the allocator for any + * function that accepts one,** and libghostty will use a default allocator. + * The default allocator will be libc malloc/free if libc is linked. + * Otherwise, a custom allocator is used (currently Zig's SMP allocator) + * that doesn't require any external dependencies. + * + * ## Basic Usage + * + * For simple use cases, you can ignore this interface entirely by passing NULL + * as the allocator parameter to functions that accept one. This will use the + * default allocator (typically libc malloc/free, if libc is linked, but + * we provide our own default allocator if libc isn't linked). + * + * To use a custom allocator: + * 1. Implement the GhosttyAllocatorVtable function pointers + * 2. Create a GhosttyAllocator struct with your vtable and context + * 3. Pass the allocator to functions that accept one + * + * @{ + */ + /** * Function table for custom memory allocator operations. * * This vtable defines the interface for a custom memory allocator. All * function pointers must be valid and non-NULL. * + * @ingroup allocator + * * If you're not going to use a custom allocator, you can ignore all of * this. All functions that take an allocator pointer allow NULL to use a * default allocator. @@ -252,6 +291,8 @@ typedef struct { * be libc malloc/free if we're linking to libc. If libc isn't linked, * a custom allocator is used (currently Zig's SMP allocator). * + * @ingroup allocator + * * Usage example: * @code * GhosttyAllocator allocator = { @@ -275,6 +316,8 @@ typedef struct { const GhosttyAllocatorVtable *vtable; } GhosttyAllocator; +/** @} */ // end of allocator group + //------------------------------------------------------------------- // Functions From 9ba45b21639870af1f675969a67fca4a52ab9fa6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 09:18:41 -0700 Subject: [PATCH 089/319] lib-vt: fix invalid Zig forwards --- src/lib_vt.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 763f17f98..8c49b4900 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -31,8 +31,8 @@ pub const size = terminal.size; pub const x11_color = terminal.x11_color; pub const Charset = terminal.Charset; -pub const CharsetSlot = terminal.Slots; -pub const CharsetActiveSlot = terminal.ActiveSlot; +pub const CharsetSlot = terminal.CharsetSlot; +pub const CharsetActiveSlot = terminal.CharsetActiveSlot; pub const Cell = page.Cell; pub const Coordinate = point.Coordinate; pub const CSI = Parser.Action.CSI; From 33e0701965303d880365529ff71e816b29bfd33d Mon Sep 17 00:00:00 2001 From: Toufiq Shishir Date: Fri, 26 Sep 2025 23:35:49 +0600 Subject: [PATCH 090/319] feat: enable separate scaling for precision and discrete mouse scrolling --- src/Surface.zig | 6 +-- src/config.zig | 1 + src/config/Config.zig | 121 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 8edeadf83..03974dfc6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -260,7 +260,7 @@ const DerivedConfig = struct { font: font.SharedGridSet.DerivedConfig, mouse_interval: u64, mouse_hide_while_typing: bool, - mouse_scroll_multiplier: f64, + mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: ?configpkg.OptionAsAlt, @@ -2829,7 +2829,7 @@ pub fn scrollCallback( // scroll events to pixels by multiplying the wheel tick value and the cell size. This means // that a wheel tick of 1 results in single scroll event. const yoff_adjusted: f64 = if (scroll_mods.precision) - yoff + yoff * self.config.mouse_scroll_multiplier.precision else yoff_adjusted: { // Round out the yoff to an absolute minimum of 1. macos tries to // simulate precision scrolling with non precision events by @@ -2843,7 +2843,7 @@ pub fn scrollCallback( else @min(yoff, -1); - break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier; + break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier.discrete; }; // Add our previously saved pending amount to the offset to get the diff --git a/src/config.zig b/src/config.zig index e83dff530..569d4bec2 100644 --- a/src/config.zig +++ b/src/config.zig @@ -27,6 +27,7 @@ pub const FontStyle = Config.FontStyle; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; +pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; pub const OptionAsAlt = Config.OptionAsAlt; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; diff --git a/src/config/Config.zig b/src/config/Config.zig index 66e63fd3f..27966fee0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -834,13 +834,14 @@ palette: Palette = .{}, @"mouse-shift-capture": MouseShiftCapture = .false, /// Multiplier for scrolling distance with the mouse wheel. Any value less -/// than 0.01 or greater than 10,000 will be clamped to the nearest valid -/// value. +/// than 0.01 (0.01 for precision scroll) or greater than 10,000 will be clamped +/// to the nearest valid value. /// -/// A value of "3" (default) scrolls 3 lines per tick. +/// A discrete value of "3" (default) scrolls about 3 lines per wheel tick. +/// And a precision value of "0.1" (default) scales pixel-level scrolling. /// -/// Available since: 1.2.0 -@"mouse-scroll-multiplier": f64 = 3.0, +/// Available since: 1.2.1 +@"mouse-scroll-multiplier": MouseScrollMultiplier = .{ .precision = 0.1, .discrete = 3.0 }, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -4077,7 +4078,8 @@ pub fn finalize(self: *Config) !void { } // Clamp our mouse scroll multiplier - self.@"mouse-scroll-multiplier" = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier")); + self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.1, self.@"mouse-scroll-multiplier".precision)); + self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete)); // Clamp our split opacity self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity")); @@ -6508,7 +6510,7 @@ pub const RepeatableCodepointMap = struct { return .{ .map = try self.map.clone(alloc) }; } - /// Compare if two of our value are requal. Required by Config. + /// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.map.list.slice(); const itemsB = other.map.list.slice(); @@ -7319,6 +7321,111 @@ pub const MouseShiftCapture = enum { never, }; +/// See mouse-scroll-multiplier +pub const MouseScrollMultiplier = struct { + const Self = @This(); + + precision: f64, + discrete: f64, + + pub fn parseCLI(self: *Self, input_: ?[]const u8) !void { + const input_raw = input_ orelse return error.ValueRequired; + const whitespace = " \t"; + const input = std.mem.trim(u8, input_raw, whitespace); + if (input.len == 0) return error.ValueRequired; + + const value = std.fmt.parseFloat(f64, input) catch null; + if (value) |val| { + self.precision = val; + self.discrete = val; + return; + } + + const comma_idx = std.mem.indexOf(u8, input, ","); + if (comma_idx) |idx| { + if (std.mem.indexOf(u8, input[idx + 1 ..], ",")) |_| return error.InvalidValue; + + const lhs = std.mem.trim(u8, input[0..idx], whitespace); + const rhs = std.mem.trim(u8, input[idx + 1 ..], whitespace); + if (lhs.len == 0 or rhs.len == 0) return error.InvalidValue; + + const lcolon_idx = std.mem.indexOf(u8, lhs, ":") orelse return error.InvalidValue; + const rcolon_idx = std.mem.indexOf(u8, rhs, ":") orelse return error.InvalidValue; + const lkey = lhs[0..lcolon_idx]; + const lvalstr = std.mem.trim(u8, lhs[lcolon_idx + 1 ..], whitespace); + const rkey = rhs[0..rcolon_idx]; + const rvalstr = std.mem.trim(u8, rhs[rcolon_idx + 1 ..], whitespace); + + // Only "precision" and "discrete" are valid keys. They + // must be different. + if (std.mem.eql(u8, lkey, rkey)) return error.InvalidValue; + + var found_precision = false; + var found_discrete = false; + var precision_val = self.precision; + var discrete_val = self.discrete; + + if (std.mem.eql(u8, lkey, "precision")) { + precision_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; + found_precision = true; + } else if (std.mem.eql(u8, lkey, "discrete")) { + discrete_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; + found_discrete = true; + } else return error.InvalidValue; + + if (std.mem.eql(u8, rkey, "precision")) { + precision_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; + found_precision = true; + } else if (std.mem.eql(u8, rkey, "discrete")) { + discrete_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; + found_discrete = true; + } else return error.InvalidValue; + + if (!found_precision or !found_discrete) return error.InvalidValue; + if (precision_val == 0 or discrete_val == 0) return error.InvalidValue; + + self.precision = precision_val; + self.discrete = discrete_val; + + return; + } else { + const colon_idx = std.mem.indexOf(u8, input, ":") orelse return error.InvalidValue; + const key = input[0..colon_idx]; + const valstr = std.mem.trim(u8, input[colon_idx + 1 ..], whitespace); + if (valstr.len == 0) return error.InvalidValue; + + const val = std.fmt.parseFloat(f64, valstr) catch return error.InvalidValue; + if (val == 0) return error.InvalidValue; + + if (std.mem.eql(u8, key, "precision")) { + self.precision = val; + return; + } else if (std.mem.eql(u8, key, "discrete")) { + self.discrete = val; + return; + } else return error.InvalidValue; + } + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + _ = alloc; + return self.*; + } + + /// Compare if two of our value are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return self.precision == other.precision and self.discrete == other.discrete; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + var buf: [32]u8 = undefined; + const formatted = try std.fmt.bufPrint(&buf, "precision:{d},discrete:{d}", .{ self.precision, self.discrete }); + try formatter.formatEntry([]const u8, formatted); + } +}; + /// How to treat requests to write to or read from the clipboard pub const ClipboardAccess = enum { allow, From 9597cead92eaf053ff38113a54882a016bc0c40b Mon Sep 17 00:00:00 2001 From: Toufiq Shishir Date: Sat, 27 Sep 2025 00:52:56 +0600 Subject: [PATCH 091/319] add: unit tests for MouseScrollMultiplier parsing and formatting --- src/config/Config.zig | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 27966fee0..63db07235 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7424,6 +7424,53 @@ pub const MouseScrollMultiplier = struct { const formatted = try std.fmt.bufPrint(&buf, "precision:{d},discrete:{d}", .{ self.precision, self.discrete }); try formatter.formatEntry([]const u8, formatted); } + + test "parse MouseScrollMultiplier" { + const testing = std.testing; + + var args: Self = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("3"); + try testing.expect(args.precision == 3 and args.discrete == 3); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("precision:1"); + try testing.expect(args.precision == 1 and args.discrete == 3); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("discrete:5"); + try testing.expect(args.precision == 0.1 and args.discrete == 5); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("precision:3,discrete:7"); + try testing.expect(args.precision == 3 and args.discrete == 7); + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI("discrete:8,precision:6"); + try testing.expect(args.precision == 6 and args.discrete == 8); + + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("foo:1")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:bar")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,precision:3")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.ValueRequired, args.parseCLI("")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,foo:5")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,,discrete:3")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,")); + args = .{ .precision = 0.1, .discrete = 3 }; + try testing.expectError(error.InvalidValue, args.parseCLI(",precision:1,discrete:3")); + } + + test "format entry MouseScrollMultiplier" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var args: Self = .{ .precision = 1.5, .discrete = 2.5 }; + try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", buf.writer())); + try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.items); + } }; /// How to treat requests to write to or read from the clipboard From 10316297412e9a57dbfbdc19b7fdff66e17b2a60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 10:00:17 -0700 Subject: [PATCH 092/319] config: modify MouseScrollMultiplier to lean on args parsing --- src/cli/args.zig | 37 +++++++-- src/config/Config.zig | 185 ++++++++++++++++-------------------------- 2 files changed, 102 insertions(+), 120 deletions(-) diff --git a/src/cli/args.zig b/src/cli/args.zig index 2d2d199be..b8f393864 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -507,13 +507,18 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T { fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { return switch (@typeInfo(T).@"struct".layout) { - .auto => parseAutoStruct(T, alloc, v), + .auto => parseAutoStruct(T, alloc, v, null), .@"packed" => parsePackedStruct(T, v), else => @compileError("unsupported struct layout"), }; } -pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { +pub fn parseAutoStruct( + comptime T: type, + alloc: Allocator, + v: []const u8, + default_: ?T, +) !T { const info = @typeInfo(T).@"struct"; comptime assert(info.layout == .auto); @@ -573,9 +578,18 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { // Ensure all required fields are set inline for (info.fields, 0..) |field, i| { if (!fields_set.isSet(i)) { - const default_ptr = field.default_value_ptr orelse return error.InvalidValue; - const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); - @field(result, field.name) = typed_ptr.*; + @field(result, field.name) = default: { + // If we're given a default value then we inherit those. + // Otherwise we use the default values as specified by the + // struct. + if (default_) |default| { + break :default @field(default, field.name); + } else { + const default_ptr = field.default_value_ptr orelse return error.InvalidValue; + const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); + break :default typed_ptr.*; + } + }; } } @@ -1194,7 +1208,18 @@ test "parseIntoField: struct with basic fields" { try testing.expectEqual(84, data.value.b); try testing.expectEqual(24, data.value.c); - // Missing require dfield + // Set with explicit default + data.value = try parseAutoStruct( + @TypeOf(data.value), + alloc, + "a:hello", + .{ .a = "oh no", .b = 42 }, + ); + try testing.expectEqualStrings("hello", data.value.a); + try testing.expectEqual(42, data.value.b); + try testing.expectEqual(12, data.value.c); + + // Missing required field try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), diff --git a/src/config/Config.zig b/src/config/Config.zig index 63db07235..fdea944ad 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -833,15 +833,20 @@ palette: Palette = .{}, /// * `never` @"mouse-shift-capture": MouseShiftCapture = .false, -/// Multiplier for scrolling distance with the mouse wheel. Any value less -/// than 0.01 (0.01 for precision scroll) or greater than 10,000 will be clamped -/// to the nearest valid value. +/// Multiplier for scrolling distance with the mouse wheel. /// -/// A discrete value of "3" (default) scrolls about 3 lines per wheel tick. -/// And a precision value of "0.1" (default) scales pixel-level scrolling. +/// A prefix of `precision:` or `discrete:` can be used to set the multiplier +/// only for scrolling with the specific type of devices. These can be +/// comma-separated to set both types of multipliers at the same time, e.g. +/// `precision:0.1,discrete:3`. If no prefix is used, the multiplier applies +/// to all scrolling devices. Specifying a prefix was introduced in Ghostty +/// 1.2.1. /// -/// Available since: 1.2.1 -@"mouse-scroll-multiplier": MouseScrollMultiplier = .{ .precision = 0.1, .discrete = 3.0 }, +/// The value will be clamped to [0.01, 10,000]. Both of these are extreme +/// and you're likely to have a bad experience if you set either extreme. +/// +/// The default value is "3" for discrete devices and "1" for precision devices. +@"mouse-scroll-multiplier": MouseScrollMultiplier = .default, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -4078,7 +4083,7 @@ pub fn finalize(self: *Config) !void { } // Clamp our mouse scroll multiplier - self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.1, self.@"mouse-scroll-multiplier".precision)); + self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".precision)); self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete)); // Clamp our split opacity @@ -7012,6 +7017,7 @@ pub const RepeatableCommand = struct { inputpkg.Command, alloc, input, + null, ); try self.value.append(alloc, cmd); } @@ -7325,86 +7331,31 @@ pub const MouseShiftCapture = enum { pub const MouseScrollMultiplier = struct { const Self = @This(); - precision: f64, - discrete: f64, + precision: f64 = 1, + discrete: f64 = 3, - pub fn parseCLI(self: *Self, input_: ?[]const u8) !void { - const input_raw = input_ orelse return error.ValueRequired; - const whitespace = " \t"; - const input = std.mem.trim(u8, input_raw, whitespace); - if (input.len == 0) return error.ValueRequired; + pub const default: MouseScrollMultiplier = .{}; - const value = std.fmt.parseFloat(f64, input) catch null; - if (value) |val| { - self.precision = val; - self.discrete = val; - return; - } - - const comma_idx = std.mem.indexOf(u8, input, ","); - if (comma_idx) |idx| { - if (std.mem.indexOf(u8, input[idx + 1 ..], ",")) |_| return error.InvalidValue; - - const lhs = std.mem.trim(u8, input[0..idx], whitespace); - const rhs = std.mem.trim(u8, input[idx + 1 ..], whitespace); - if (lhs.len == 0 or rhs.len == 0) return error.InvalidValue; - - const lcolon_idx = std.mem.indexOf(u8, lhs, ":") orelse return error.InvalidValue; - const rcolon_idx = std.mem.indexOf(u8, rhs, ":") orelse return error.InvalidValue; - const lkey = lhs[0..lcolon_idx]; - const lvalstr = std.mem.trim(u8, lhs[lcolon_idx + 1 ..], whitespace); - const rkey = rhs[0..rcolon_idx]; - const rvalstr = std.mem.trim(u8, rhs[rcolon_idx + 1 ..], whitespace); - - // Only "precision" and "discrete" are valid keys. They - // must be different. - if (std.mem.eql(u8, lkey, rkey)) return error.InvalidValue; - - var found_precision = false; - var found_discrete = false; - var precision_val = self.precision; - var discrete_val = self.discrete; - - if (std.mem.eql(u8, lkey, "precision")) { - precision_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; - found_precision = true; - } else if (std.mem.eql(u8, lkey, "discrete")) { - discrete_val = std.fmt.parseFloat(f64, lvalstr) catch return error.InvalidValue; - found_discrete = true; - } else return error.InvalidValue; - - if (std.mem.eql(u8, rkey, "precision")) { - precision_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; - found_precision = true; - } else if (std.mem.eql(u8, rkey, "discrete")) { - discrete_val = std.fmt.parseFloat(f64, rvalstr) catch return error.InvalidValue; - found_discrete = true; - } else return error.InvalidValue; - - if (!found_precision or !found_discrete) return error.InvalidValue; - if (precision_val == 0 or discrete_val == 0) return error.InvalidValue; - - self.precision = precision_val; - self.discrete = discrete_val; - - return; - } else { - const colon_idx = std.mem.indexOf(u8, input, ":") orelse return error.InvalidValue; - const key = input[0..colon_idx]; - const valstr = std.mem.trim(u8, input[colon_idx + 1 ..], whitespace); - if (valstr.len == 0) return error.InvalidValue; - - const val = std.fmt.parseFloat(f64, valstr) catch return error.InvalidValue; - if (val == 0) return error.InvalidValue; - - if (std.mem.eql(u8, key, "precision")) { - self.precision = val; - return; - } else if (std.mem.eql(u8, key, "discrete")) { - self.discrete = val; - return; - } else return error.InvalidValue; - } + pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { + const input = input_ orelse return error.ValueRequired; + self.* = cli.args.parseAutoStruct( + MouseScrollMultiplier, + alloc, + input, + self.*, + ) catch |err| switch (err) { + error.InvalidValue => bare: { + const v = std.fmt.parseFloat( + f64, + input, + ) catch return error.InvalidValue; + break :bare .{ + .precision = v, + .discrete = v, + }; + }, + else => return err, + }; } /// Deep copy of the struct. Required by Config. @@ -7421,45 +7372,50 @@ pub const MouseScrollMultiplier = struct { /// Used by Formatter pub fn formatEntry(self: Self, formatter: anytype) !void { var buf: [32]u8 = undefined; - const formatted = try std.fmt.bufPrint(&buf, "precision:{d},discrete:{d}", .{ self.precision, self.discrete }); + const formatted = std.fmt.bufPrint( + &buf, + "precision:{d},discrete:{d}", + .{ self.precision, self.discrete }, + ) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, formatted); } - test "parse MouseScrollMultiplier" { + test "parse" { const testing = std.testing; + const alloc = testing.allocator; + const epsilon = 0.00001; var args: Self = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("3"); - try testing.expect(args.precision == 3 and args.discrete == 3); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("precision:1"); - try testing.expect(args.precision == 1 and args.discrete == 3); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("discrete:5"); - try testing.expect(args.precision == 0.1 and args.discrete == 5); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("precision:3,discrete:7"); - try testing.expect(args.precision == 3 and args.discrete == 7); - args = .{ .precision = 0.1, .discrete = 3 }; - try args.parseCLI("discrete:8,precision:6"); - try testing.expect(args.precision == 6 and args.discrete == 8); + try args.parseCLI(alloc, "3"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("foo:1")); + try args.parseCLI(alloc, "precision:1"); + try testing.expectApproxEqAbs(1, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); + args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:bar")); + try args.parseCLI(alloc, "discrete:5"); + try testing.expectApproxEqAbs(0.1, args.precision, epsilon); + try testing.expectApproxEqAbs(5, args.discrete, epsilon); + args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,precision:3")); + try args.parseCLI(alloc, "precision:3,discrete:7"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(7, args.discrete, epsilon); + args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.ValueRequired, args.parseCLI("")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,foo:5")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,,discrete:3")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI("precision:1,discrete:3,")); - args = .{ .precision = 0.1, .discrete = 3 }; - try testing.expectError(error.InvalidValue, args.parseCLI(",precision:1,discrete:3")); + try args.parseCLI(alloc, "discrete:8,precision:6"); + try testing.expectApproxEqAbs(6, args.precision, epsilon); + try testing.expectApproxEqAbs(8, args.discrete, epsilon); + + args = .default; + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "foo:1")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:bar")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,discrete:3,foo:5")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,,discrete:3")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, ",precision:1,discrete:3")); } test "format entry MouseScrollMultiplier" { @@ -8087,6 +8043,7 @@ pub const Theme = struct { Theme, alloc, input, + null, ); return; } From 3fdb52e48d528ad80698d59c4194c13e84eff51e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 10:48:16 -0700 Subject: [PATCH 093/319] apprt/gtk: do not close window if tab overview is open with no tabs Fixes #8944 When we drag the only tab out of the tab overview, this triggers an `n-pages` signal with 0 pages. If we close the window in this state, it causes both Ghostty to exit AND the drag/drop to fail. Even if we pre-empt Ghostty exiting by modifying the application class, the drag/drop still fails and the application leaks memory and enters a bad state. The solution is to keep the window open if we go to `n-pages == 0` and we have the tab overview open. Interestingly, if you click to close the final tab from the tab overview, Adwaita closes the tab overview so it still triggers the window closing behavior (this is good). --- src/apprt/gtk/class/application.zig | 10 ++++++++-- src/apprt/gtk/class/window.zig | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 6ab3ad282..f7ed0d38c 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -524,13 +524,19 @@ pub const Application = extern struct { if (!config.@"quit-after-last-window-closed") break :q false; // If the quit timer has expired, quit. - if (priv.quit_timer == .expired) break :q true; + if (priv.quit_timer == .expired) { + log.debug("must_quit due to quit timer expired", .{}); + break :q true; + } // If we have no windows attached to our app, also quit. if (priv.requested_window and @as( ?*glib.List, self.as(gtk.Application).getWindows(), - ) == null) break :q true; + ) == null) { + log.debug("must_quit due to no app windows", .{}); + break :q true; + } // No quit conditions met break :q false; diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index df6ea647f..c0dd6ab1f 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1489,6 +1489,13 @@ pub const Window = extern struct { const priv = self.private(); if (priv.tab_view.getNPages() == 0) { // If we have no pages left then we want to close window. + + // If the tab overview is open, then we don't close the window + // because its a rather abrupt experience. This also fixes an + // issue where dragging out the last tab in the tab overview + // won't cause Ghostty to exit. + if (priv.tab_overview.getOpen() != 0) return; + self.as(gtk.Window).close(); } } From e3ebdc79756985ee541b6b1ed402da596b39318b Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 6 Sep 2025 19:21:25 -0700 Subject: [PATCH 094/319] Rewrite constraint code for improved icon scaling/alignment --- src/config/Config.zig | 9 +- src/font/Collection.zig | 13 +- src/font/Metrics.zig | 109 ++-- src/font/SharedGrid.zig | 8 +- src/font/face.zig | 389 ++++++------ src/font/face/coretext.zig | 21 +- src/font/face/freetype.zig | 65 +- src/font/nerd_font_attributes.zig | 944 ++++++++++++++---------------- src/font/nerd_font_codegen.py | 86 +-- src/renderer/generic.zig | 3 +- 10 files changed, 819 insertions(+), 828 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index fdea944ad..46eb03fe2 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -416,9 +416,12 @@ pub const compatibility = std.StaticStringMap( /// necessarily force them to be. Decreasing this value will make nerd font /// icons smaller. /// -/// The default value for the icon height is 1.2 times the height of capital -/// letters in your primary font, so something like -16.6% would make icons -/// roughly the same height as capital letters. +/// This value only applies to icons that are constrained to a single cell by +/// neighboring characters. An icon that is free to spread across two cells +/// can always use up to the full line height of the primary font. +/// +/// The default value is 2/3 times the height of capital letters in your primary +/// font plus 1/3 times the font's line height. /// /// See the notes about adjustments in `adjust-cell-width`. /// diff --git a/src/font/Collection.zig b/src/font/Collection.zig index ad9590d70..997c72aa7 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1213,6 +1213,9 @@ test "metrics" { // and 1em should be the point size * dpi scale, so 12 * (96/72) // which is 16, and 16 * 1.049 = 16.784, which finally is rounded // to 17. + // + // The icon height is (2 * cap_height + face_height) / 3 + // = (2 * 623 + 1049) / 3 = 765, and 16 * 0.765 = 12.24. .cell_height = 17, .cell_baseline = 3, .underline_position = 17, @@ -1223,7 +1226,10 @@ test "metrics" { .overline_thickness = 1, .box_thickness = 1, .cursor_height = 17, - .icon_height = 11, + .icon_height = 12.24, + .face_width = 8.0, + .face_height = 16.784, + .face_y = @round(3.04) - @as(f64, 3.04), // use f64, not comptime float, for exact match with runtime value }, c.metrics); // Resize should change metrics @@ -1240,7 +1246,10 @@ test "metrics" { .overline_thickness = 2, .box_thickness = 2, .cursor_height = 34, - .icon_height = 23, + .icon_height = 24.48, + .face_width = 16.0, + .face_height = 33.568, + .face_y = @round(6.08) - @as(f64, 6.08), // use f64, not comptime float, for exact match with runtime value }, c.metrics); } diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 9f6df9dc3..a0bc047c4 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -36,11 +36,17 @@ cursor_thickness: u32 = 1, cursor_height: u32, /// The constraint height for nerd fonts icons. -icon_height: u32, +icon_height: f64, -/// Original cell width in pixels. This is used to keep -/// glyphs centered if the cell width is adjusted wider. -original_cell_width: ?u32 = null, +/// The unrounded face width, used in scaling calculations. +face_width: f64, + +/// The unrounded face height, used in scaling calculations. +face_height: f64, + +/// The vertical bearing of face within the pixel-rounded +/// and possibly height-adjusted cell +face_y: f64, /// Minimum acceptable values for some fields to prevent modifiers /// from being able to, for example, cause 0-thickness underlines. @@ -53,7 +59,9 @@ const Minimums = struct { const box_thickness = 1; const cursor_thickness = 1; const cursor_height = 1; - const icon_height = 1; + const icon_height = 1.0; + const face_height = 1.0; + const face_width = 1.0; }; /// Metrics extracted from a font face, based on @@ -214,8 +222,10 @@ pub fn calc(face: FaceMetrics) Metrics { // We use the ceiling of the provided cell width and height to ensure // that the cell is large enough for the provided size, since we cast // it to an integer later. - const cell_width = @ceil(face.cell_width); - const cell_height = @ceil(face.lineHeight()); + const face_width = face.cell_width; + const face_height = face.lineHeight(); + const cell_width = @ceil(face_width); + const cell_height = @ceil(face_height); // We split our line gap in two parts, and put half of it on the top // of the cell and the other half on the bottom, so that our text never @@ -224,7 +234,11 @@ pub fn calc(face: FaceMetrics) Metrics { // Unlike all our other metrics, `cell_baseline` is relative to the // BOTTOM of the cell. - const cell_baseline = @round(half_line_gap - face.descent); + const face_baseline = half_line_gap - face.descent; + const cell_baseline = @round(face_baseline); + + // We keep track of the vertical bearing of the face in the cell + const face_y = cell_baseline - face_baseline; // We calculate a top_to_baseline to make following calculations simpler. const top_to_baseline = cell_height - cell_baseline; @@ -237,16 +251,8 @@ pub fn calc(face: FaceMetrics) Metrics { const underline_position = @round(top_to_baseline - face.underlinePosition()); const strikethrough_position = @round(top_to_baseline - face.strikethroughPosition()); - // The calculation for icon height in the nerd fonts patcher - // is two thirds cap height to one third line height, but we - // use an opinionated default of 1.2 * cap height instead. - // - // Doing this prevents fonts with very large line heights - // from having excessively oversized icons, and allows fonts - // with very small line heights to still have roomy icons. - // - // We do cap it at `cell_height` though for obvious reasons. - const icon_height = @min(cell_height, cap_height * 1.2); + // Same heuristic as the font_patcher script + const icon_height = (2 * cap_height + face_height) / 3; var result: Metrics = .{ .cell_width = @intFromFloat(cell_width), @@ -260,7 +266,10 @@ pub fn calc(face: FaceMetrics) Metrics { .overline_thickness = @intFromFloat(underline_thickness), .box_thickness = @intFromFloat(underline_thickness), .cursor_height = @intFromFloat(cell_height), - .icon_height = @intFromFloat(icon_height), + .icon_height = icon_height, + .face_width = face_width, + .face_height = face_height, + .face_y = face_y, }; // Ensure all metrics are within their allowable range. @@ -286,11 +295,6 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const new = @max(entry.value_ptr.apply(original), 1); if (new == original) continue; - // Preserve the original cell width if not set. - if (self.original_cell_width == null) { - self.original_cell_width = self.cell_width; - } - // Set the new value @field(self, @tagName(tag)) = new; @@ -307,6 +311,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const diff = new - original; const diff_bottom = diff / 2; const diff_top = diff - diff_bottom; + self.face_y += @floatFromInt(diff_bottom); self.cell_baseline +|= diff_bottom; self.underline_position +|= diff_top; self.strikethrough_position +|= diff_top; @@ -315,6 +320,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const diff = original - new; const diff_bottom = diff / 2; const diff_top = diff - diff_bottom; + self.face_y -= @floatFromInt(diff_bottom); self.cell_baseline -|= diff_bottom; self.underline_position -|= diff_top; self.strikethrough_position -|= diff_top; @@ -417,25 +423,35 @@ pub const Modifier = union(enum) { /// Apply a modifier to a numeric value. pub fn apply(self: Modifier, v: anytype) @TypeOf(v) { const T = @TypeOf(v); - const signed = @typeInfo(T).int.signedness == .signed; - return switch (self) { - .percent => |p| percent: { - const p_clamped: f64 = @max(0, p); - const v_f64: f64 = @floatFromInt(v); - const applied_f64: f64 = @round(v_f64 * p_clamped); - const applied_T: T = @intFromFloat(applied_f64); - break :percent applied_T; - }, + const Tinfo = @typeInfo(T); + return switch (comptime Tinfo) { + .int, .comptime_int => switch (self) { + .percent => |p| percent: { + const p_clamped: f64 = @max(0, p); + const v_f64: f64 = @floatFromInt(v); + const applied_f64: f64 = @round(v_f64 * p_clamped); + const applied_T: T = @intFromFloat(applied_f64); + break :percent applied_T; + }, - .absolute => |abs| absolute: { - const v_i64: i64 = @intCast(v); - const abs_i64: i64 = @intCast(abs); - const applied_i64: i64 = v_i64 +| abs_i64; - const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64); - const applied_T: T = std.math.cast(T, clamped_i64) orelse - std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64))); - break :absolute applied_T; + .absolute => |abs| absolute: { + const v_i64: i64 = @intCast(v); + const abs_i64: i64 = @intCast(abs); + const applied_i64: i64 = v_i64 +| abs_i64; + const clamped_i64: i64 = if (Tinfo.int.signedness == .signed) + applied_i64 + else + @max(0, applied_i64); + const applied_T: T = std.math.cast(T, clamped_i64) orelse + std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64))); + break :absolute applied_T; + }, }, + .float, .comptime_float => return switch (self) { + .percent => |p| v * @max(0, p), + .absolute => |abs| v + @as(T, @floatFromInt(abs)), + }, + else => {}, }; } @@ -481,7 +497,7 @@ pub const Key = key: { var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined; var count: usize = 0; for (field_infos, 0..) |field, i| { - if (field.type != u32 and field.type != i32) continue; + if (field.type != u32 and field.type != i32 and field.type != f64) continue; enumFields[i] = .{ .name = field.name, .value = i }; count += 1; } @@ -512,7 +528,10 @@ fn init() Metrics { .overline_thickness = 0, .box_thickness = 0, .cursor_height = 0, - .icon_height = 0, + .icon_height = 0.0, + .face_width = 0.0, + .face_height = 0.0, + .face_y = 0.0, }; } @@ -542,6 +561,7 @@ test "Metrics: adjust cell height smaller" { try set.put(alloc, .cell_height, .{ .percent = 0.75 }); var m: Metrics = init(); + m.face_y = 0.33; m.cell_baseline = 50; m.underline_position = 55; m.strikethrough_position = 30; @@ -549,6 +569,7 @@ test "Metrics: adjust cell height smaller" { m.cell_height = 100; m.cursor_height = 100; m.apply(set); + try testing.expectEqual(-11.67, m.face_y); try testing.expectEqual(@as(u32, 75), m.cell_height); try testing.expectEqual(@as(u32, 38), m.cell_baseline); try testing.expectEqual(@as(u32, 42), m.underline_position); @@ -570,6 +591,7 @@ test "Metrics: adjust cell height larger" { try set.put(alloc, .cell_height, .{ .percent = 1.75 }); var m: Metrics = init(); + m.face_y = 0.33; m.cell_baseline = 50; m.underline_position = 55; m.strikethrough_position = 30; @@ -577,6 +599,7 @@ test "Metrics: adjust cell height larger" { m.cell_height = 100; m.cursor_height = 100; m.apply(set); + try testing.expectEqual(37.33, m.face_y); try testing.expectEqual(@as(u32, 175), m.cell_height); try testing.expectEqual(@as(u32, 87), m.cell_baseline); try testing.expectEqual(@as(u32, 93), m.underline_position); diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index e79fd117f..3fd9cf204 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -270,11 +270,9 @@ pub fn renderGlyph( // Always use these constraints for emoji. if (p == .emoji) { render_opts.constraint = .{ - // Make the emoji as wide as possible, scaling proportionally, - // but then scale it down as necessary if its new size exceeds - // the cell height. - .size_horizontal = .cover, - .size_vertical = .fit, + // Scale emoji to be as large as possible + // while preserving their aspect ratio. + .size = .cover, // Center the emoji in its cells. .align_horizontal = .center, diff --git a/src/font/face.zig b/src/font/face.zig index 9da3c30f6..0f882a77f 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -136,10 +136,8 @@ pub const RenderOptions = struct { /// Don't constrain the glyph in any way. pub const none: Constraint = .{}; - /// Vertical sizing rule. - size_vertical: Size = .none, - /// Horizontal sizing rule. - size_horizontal: Size = .none, + /// Sizing rule. + size: Size = .none, /// Vertical alignment rule. align_vertical: Align = .none, @@ -155,42 +153,40 @@ pub const RenderOptions = struct { /// Bottom padding when resizing. pad_bottom: f64 = 0.0, - // This acts as a multiple of the provided width when applying - // constraints, so if this is 1.6 for example, then a width of - // 10 would be treated as though it were 16. - group_width: f64 = 1.0, - // This acts as a multiple of the provided height when applying - // constraints, so if this is 1.6 for example, then a height of - // 10 would be treated as though it were 16. - group_height: f64 = 1.0, - // This is an x offset for the actual width within the group width. - // If this is 0.5 then the glyph will be offset so that its left - // edge sits at the halfway point of the group width. - group_x: f64 = 0.0, - // This is a y offset for the actual height within the group height. - // If this is 0.5 then the glyph will be offset so that its bottom - // edge sits at the halfway point of the group height. - group_y: f64 = 0.0, + // Size and bearings of the glyph relative + // to the bounding box of its scale group. + relative_width: f64 = 1.0, + relative_height: f64 = 1.0, + relative_x: f64 = 0.0, + relative_y: f64 = 0.0, - /// Maximum ratio of width to height when resizing. + /// Maximum aspect ratio (width/height) to allow when stretching. max_xy_ratio: ?f64 = null, /// Maximum number of cells horizontally to use. max_constraint_width: u2 = 2, - /// What to use as the height metric when constraining the glyph. + /// What to use as the height metric when constraining the glyph and + /// the constraint width is 1, height: Height = .cell, pub const Size = enum { /// Don't change the size of this glyph. none, - /// Move the glyph and optionally scale it down - /// proportionally to fit within the given axis. + /// Scale the glyph down if needed to fit within the bounds, + /// preserving aspect ratio. fit, - /// Move and resize the glyph proportionally to - /// cover the given axis. + /// Scale the glyph up or down to exactly match the bounds, + /// preserving aspect ratio. cover, - /// Same as `cover` but not proportional. + /// Scale the glyph down if needed to fit within the bounds, + /// preserving aspect ratio. If the glyph doesn't cover a + /// single cell, scale up. If the glyph exceeds a single + /// cell but is within the bounds, do nothing. + /// (Nerd Font specific rule.) + fit_cover1, + /// Stretch the glyph to exactly fit the bounds in both + /// directions, disregarding aspect ratio. stretch, }; @@ -205,12 +201,18 @@ pub const RenderOptions = struct { end, /// Move the glyph so that it is centered on this axis. center, + /// Move the glyph so that it is centered on this axis, + /// but always with respect to the first cell even for + /// multi-cell constraints. (Nerd Font specific rule.) + center1, }; pub const Height = enum { - /// Use the full height of the cell for constraining this glyph. + /// Always use the full height of the cell for constraining this glyph. cell, - /// Use the "icon height" from the grid metrics as the height. + /// When the constraint width is 1, use the "icon height" from the grid + /// metrics as the height. (When the constraint width is >1, the + /// constraint height is always the full cell height.) icon, }; @@ -226,9 +228,8 @@ pub const RenderOptions = struct { /// because it neither sizes nor positions the glyph, then this /// returns false. pub inline fn doesAnything(self: Constraint) bool { - return self.size_horizontal != .none or + return self.size != .none or self.align_horizontal != .none or - self.size_vertical != .none or self.align_vertical != .none; } @@ -241,156 +242,202 @@ pub const RenderOptions = struct { /// Number of cells horizontally available for this glyph. constraint_width: u2, ) GlyphSize { - var g = glyph; + if (!self.doesAnything()) return glyph; - const available_width: f64 = @floatFromInt( - metrics.cell_width * @min( - self.max_constraint_width, - constraint_width, - ), - ); - const available_height: f64 = @floatFromInt(switch (self.height) { - .cell => metrics.cell_height, - .icon => metrics.icon_height, - }); + // For extra wide font faces, never stretch glyphs across two cells. + // This mirrors font_patcher. + const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height)) + 1 + else + @min(self.max_constraint_width, constraint_width); - const w = available_width - - self.pad_left * available_width - - self.pad_right * available_width; - const h = available_height - - self.pad_top * available_height - - self.pad_bottom * available_height; - - // Subtract padding from the bearings so that our - // alignment and sizing code works correctly. We - // re-add before returning. - g.x -= self.pad_left * available_width; - g.y -= self.pad_bottom * available_height; - - // Multiply by group width and height for better sizing. - g.width *= self.group_width; - g.height *= self.group_height; - - switch (self.size_horizontal) { - .none => {}, - .fit => if (g.width > w) { - const orig_height = g.height; - // Adjust our height and width to proportionally - // scale them to fit the glyph to the cell width. - g.height *= w / g.width; - g.width = w; - // Set our x to 0 since anything else would mean - // the glyph extends outside of the cell width. - g.x = 0; - // Compensate our y to keep things vertically - // centered as they're scaled down. - g.y += (orig_height - g.height) / 2; - } else if (g.width + g.x > w) { - // If the width of the glyph can fit in the cell but - // is currently outside due to the left bearing, then - // we reduce the left bearing just enough to fit it - // back in the cell. - g.x = w - g.width; - } else if (g.x < 0) { - g.x = 0; - }, - .cover => { - const orig_height = g.height; - - g.height *= w / g.width; - g.width = w; - - g.x = 0; - - g.y += (orig_height - g.height) / 2; - }, - .stretch => { - g.width = w; - g.x = 0; - }, - } - - switch (self.size_vertical) { - .none => {}, - .fit => if (g.height > h) { - const orig_width = g.width; - // Adjust our height and width to proportionally - // scale them to fit the glyph to the cell height. - g.width *= h / g.height; - g.height = h; - // Set our y to 0 since anything else would mean - // the glyph extends outside of the cell height. - g.y = 0; - // Compensate our x to keep things horizontally - // centered as they're scaled down. - g.x += (orig_width - g.width) / 2; - } else if (g.height + g.y > h) { - // If the height of the glyph can fit in the cell but - // is currently outside due to the bottom bearing, then - // we reduce the bottom bearing just enough to fit it - // back in the cell. - g.y = h - g.height; - } else if (g.y < 0) { - g.y = 0; - }, - .cover => { - const orig_width = g.width; - - g.width *= h / g.height; - g.height = h; - - g.y = 0; - - g.x += (orig_width - g.width) / 2; - }, - .stretch => { - g.height = h; - g.y = 0; - }, - } - - // Add group-relative position - g.x += self.group_x * g.width; - g.y += self.group_y * g.height; - - // Divide group width and height back out before we align. - g.width /= self.group_width; - g.height /= self.group_height; - - if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) { - const orig_width = g.width; - g.width = g.height * ratio; - g.x += (orig_width - g.width) / 2; + // The bounding box for the glyph's scale group. + // Scaling and alignment rules are calculated for + // this box and then applied to the glyph. + var group: GlyphSize = group: { + const group_width = glyph.width / self.relative_width; + const group_height = glyph.height / self.relative_height; + break :group .{ + .width = group_width, + .height = group_height, + .x = glyph.x - (group_width * self.relative_x), + .y = glyph.y - (group_height * self.relative_y), + }; }; - switch (self.align_horizontal) { - .none => {}, - .start => g.x = 0, - .end => g.x = w - g.width, - .center => g.x = (w - g.width) / 2, + // The new, constrained glyph size + var constrained_glyph = glyph; + + // Apply prescribed scaling + const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width); + constrained_glyph.width *= width_factor; + constrained_glyph.x *= width_factor; + constrained_glyph.height *= height_factor; + constrained_glyph.y *= height_factor; + + // NOTE: font_patcher jumps through a lot of hoops at this + // point to ensure that the glyph remains within the target + // bounding box after rounding to font definition units. + // This is irrelevant here as we're not rounding, we're + // staying in f64 and heading straight to rendering. + + // Align vertically + if (self.align_vertical != .none) { + // Vertically scale group bounding box. + group.height *= height_factor; + group.y *= height_factor; + + // Calculate offset and shift the glyph + constrained_glyph.y += self.offset_vertical(group, metrics); } - switch (self.align_vertical) { - .none => {}, - .start => g.y = 0, - .end => g.y = h - g.height, - .center => g.y = (h - g.height) / 2, + // Align horizontally + if (self.align_horizontal != .none) { + // Horizontally scale group bounding box. + group.width *= width_factor; + group.x *= width_factor; + + // Calculate offset and shift the glyph + constrained_glyph.x += self.offset_horizontal(group, metrics, min_constraint_width); } - // Re-add our padding before returning. - g.x += self.pad_left * available_width; - g.y += self.pad_bottom * available_height; + return constrained_glyph; + } - // If the available height is less than the cell height, we - // add half of the difference to center it in the full height. - // - // If necessary, in the future, we can adjust this to account - // for alignment, but that isn't necessary with any of the nf - // icons afaict. - const cell_height: f64 = @floatFromInt(metrics.cell_height); - g.y += (cell_height - available_height) / 2; + /// Return width and height scaling factors for this scaling group. + fn scale_factors( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + min_constraint_width: u2, + ) struct { f64, f64 } { + if (self.size == .none) { + return .{ 1.0, 1.0 }; + } - return g; + const multi_cell = (min_constraint_width > 1); + + const pad_width_factor = @as(f64, @floatFromInt(min_constraint_width)) - (self.pad_left + self.pad_right); + const pad_height_factor = 1 - (self.pad_bottom + self.pad_top); + + const target_width = pad_width_factor * metrics.face_width; + const target_height = pad_height_factor * switch (self.height) { + .cell => metrics.face_height, + // icon_height only applies with single-cell constraints. + // This mirrors font_patcher. + .icon => if (multi_cell) + metrics.face_height + else + metrics.icon_height, + }; + + var width_factor = target_width / group.width; + var height_factor = target_height / group.height; + + switch (self.size) { + .none => unreachable, + .fit => { + // Scale down to fit if needed + height_factor = @min(1, width_factor, height_factor); + width_factor = height_factor; + }, + .cover => { + // Scale to cover + height_factor = @min(width_factor, height_factor); + width_factor = height_factor; + }, + .fit_cover1 => { + // Scale down to fit or up to cover at least one cell + // NOTE: This is similar to font_patcher's "pa" mode, + // however, font_patcher will only do the upscaling + // part if the constraint width is 1, resulting in + // some icons becoming smaller when the constraint + // width increases. You'd see icons shrinking when + // opening up a space after them. This makes no + // sense, so we've fixed the rule such that these + // icons are scaled to the same size for multi-cell + // constraints as they would be for single-cell. + height_factor = @min(width_factor, height_factor); + if (multi_cell and (height_factor > 1)) { + // Call back into this function with + // constraint width 1 to get single-cell scale + // factors. We use the height factor as width + // could have been modified by max_xy_ratio. + _, const single_height_factor = self.scale_factors(group, metrics, 1); + height_factor = @max(1, single_height_factor); + } + width_factor = height_factor; + }, + .stretch => {}, + } + + // Reduce aspect ratio if required + if (self.max_xy_ratio) |ratio| { + if (group.width * width_factor > group.height * height_factor * ratio) { + width_factor = group.height * height_factor * ratio / group.width; + } + } + + return .{ width_factor, height_factor }; + } + + /// Return vertical offset needed to align this group + fn offset_vertical( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + ) f64 { + // We use face_height and offset by face_y, rather than + // using cell_height directly, to account for the asymmetry + // of the pixel cell around the face (a consequence of + // aligning the baseline with a pixel boundary rather than + // vertically centering the face). + const new_group_y = metrics.face_y + switch (self.align_vertical) { + .none => return 0.0, + .start => self.pad_bottom * metrics.face_height, + .end => end: { + const pad_top_dy = self.pad_top * metrics.face_height; + break :end metrics.face_height - pad_top_dy - group.height; + }, + .center, .center1 => (metrics.face_height - group.height) / 2, + }; + return new_group_y - group.y; + } + + /// Return horizontal offset needed to align this group + fn offset_horizontal( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + min_constraint_width: u2, + ) f64 { + // For multi-cell constraints, we align relative to the span + // from the left edge of the first face cell to the right + // edge of the last face cell as they sit within the rounded + // and adjusted pixel cell (centered if narrower than the + // pixel cell, left-aligned if wider). + const face_x, const full_face_span = facecalcs: { + const cell_width: f64 = @floatFromInt(metrics.cell_width); + const full_width: f64 = @floatFromInt(min_constraint_width * metrics.cell_width); + const cell_margin = cell_width - metrics.face_width; + break :facecalcs .{ @max(0, cell_margin / 2), full_width - cell_margin }; + }; + const pad_left_x = self.pad_left * metrics.face_width; + const new_group_x = face_x + switch (self.align_horizontal) { + .none => return 0.0, + .start => pad_left_x, + .end => end: { + const pad_right_dx = self.pad_right * metrics.face_width; + break :end @max(pad_left_x, full_face_span - pad_right_dx - group.width); + }, + .center => @max(pad_left_x, (full_face_span - group.width) / 2), + // NOTE: .center1 implements the font_patcher rule of centering + // in the first cell even for multi-cell constraints. Since glyphs + // are not allowed to protrude to the left, this results in the + // left-alignment like .start when the glyph is wider than a cell. + .center1 => @max(pad_left_x, (metrics.face_width - group.width) / 2), + }; + return new_group_x - group.x; } }; }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index cb9993cbf..8c9611c04 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -388,19 +388,16 @@ pub const Face = struct { y = @round(y); } - // If the cell width was adjusted wider, we re-center all glyphs - // in the new width, so that they aren't weirdly off to the left. - if (metrics.original_cell_width) |original| recenter: { - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if (opts.constraint.align_horizontal != .none) break :recenter; - - // If the original width was wider then we don't do anything. - if (original >= metrics.cell_width) break :recenter; - + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) { // We add half the difference to re-center. - x += (cell_width - @as(f64, @floatFromInt(original))) / 2; + x += (cell_width - metrics.face_width) / 2; } // Our whole-pixel bearings for the final glyph. diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 82cf107c8..3094d8076 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -498,17 +498,14 @@ pub const Face = struct { y = @round(y); } - // If the cell width was adjusted wider, we re-center all glyphs - // in the new width, so that they aren't weirdly off to the left. - if (metrics.original_cell_width) |original| recenter: { - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if (opts.constraint.align_horizontal != .none) break :recenter; - - // If the original width was wider then we don't do anything. - if (original >= metrics.cell_width) break :recenter; - + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) { // We add half the difference to re-center. // // NOTE: We round this to a whole-pixel amount because under @@ -516,7 +513,7 @@ pub const Face = struct { // the case under CoreText. If we move the outlines by // a non-whole-pixel amount, it completely ruins the // hinting. - x += @round((cell_width - @as(f64, @floatFromInt(original))) / 2); + x += @round((cell_width - metrics.face_width) / 2); } // Now we can render the glyph. @@ -1211,25 +1208,31 @@ test "color emoji" { alloc, &atlas, ft_font.glyphIndex('🥸').?, - .{ .grid_metrics = .{ - .cell_width = 13, - .cell_height = 24, - .cell_baseline = 0, - .underline_position = 0, - .underline_thickness = 0, - .strikethrough_position = 0, - .strikethrough_thickness = 0, - .overline_position = 0, - .overline_thickness = 0, - .box_thickness = 0, - .cursor_height = 0, - .icon_height = 0, - }, .constraint_width = 2, .constraint = .{ - .size_horizontal = .cover, - .size_vertical = .cover, - .align_horizontal = .center, - .align_vertical = .center, - } }, + .{ + .grid_metrics = .{ + .cell_width = 13, + .cell_height = 24, + .cell_baseline = 0, + .underline_position = 0, + .underline_thickness = 0, + .strikethrough_position = 0, + .strikethrough_thickness = 0, + .overline_position = 0, + .overline_thickness = 0, + .box_thickness = 0, + .cursor_height = 0, + .icon_height = 0, + .face_width = 13, + .face_height = 24, + .face_y = 0, + }, + .constraint_width = 2, + .constraint = .{ + .size = .fit, + .align_horizontal = .center, + .align_vertical = .center, + }, + }, ); try testing.expectEqual(@as(u32, 24), glyph.height); } diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 11902d310..04088b1aa 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -6,16 +6,15 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; -/// Get the a constraints for the provided codepoint. +/// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { 0x2500...0x259f, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -23,12 +22,11 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0x2630, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .height = .icon, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_left = 0.1, .pad_right = 0.1, .pad_top = 0.1, @@ -36,49 +34,45 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0x276c...0x276d, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3999999999999999, - .group_height = 1.1222570532915361, - .group_x = 0.1428571428571428, - .group_y = 0.0349162011173184, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7142857142857143, + .relative_height = 0.8910614525139665, + .relative_x = 0.1428571428571428, + .relative_y = 0.0349162011173184, .pad_top = 0.15, .pad_bottom = 0.15, }, 0x276e...0x276f, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0115606936416186, - .group_height = 1.1222570532915361, - .group_x = 0.0057142857142857, - .group_y = 0.0125698324022346, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9885714285714285, + .relative_height = 0.8910614525139665, + .relative_x = 0.0057142857142857, + .relative_y = 0.0125698324022346, .pad_top = 0.15, .pad_bottom = 0.15, }, 0x2770...0x2771, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_top = 0.15, .pad_bottom = 0.15, }, 0xe0b0, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -87,20 +81,18 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b1, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.7, }, 0xe0b2, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -109,20 +101,18 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b3, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.7, }, 0xe0b4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -131,20 +121,18 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b5, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.5, }, 0xe0b6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -153,21 +141,19 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.5, }, 0xe0b8, 0xe0bc, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -176,20 +162,18 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xe0b9, 0xe0bd, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0ba, 0xe0be, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -198,19 +182,17 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xe0bb, 0xe0bf, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c0, 0xe0c8, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -218,18 +200,16 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c1, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c2, 0xe0ca, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -237,17 +217,15 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c3, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -256,10 +234,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c5, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -268,10 +245,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -280,10 +256,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -292,10 +267,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0cc, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -304,36 +278,32 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0cd, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.865, }, 0xe0ce, 0xe0d0...0xe0d1, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .fit_cover1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0cf, 0xe0d3, 0xe0d5, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, - .align_horizontal = .center, - .align_vertical = .center, + .size = .fit_cover1, + .align_horizontal = .center1, + .align_vertical = .center1, }, 0xe0d2, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -342,11 +312,10 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0d4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -355,11 +324,10 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0d6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -368,11 +336,10 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0d7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -425,640 +392,583 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf307...0xf847, 0xf0001...0xf1af0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, }, 0xea61, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3310225303292895, - .group_height = 1.0762439807383628, - .group_x = 0.0846354166666667, - .group_y = 0.0708426547352722, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7513020833333334, + .relative_height = 0.9291573452647278, + .relative_x = 0.0846354166666667, + .relative_y = 0.0708426547352722, }, 0xea7d, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1912058627581612, - .group_height = 1.1426759670259987, - .group_x = 0.0917225950782998, - .group_y = 0.0416204217536071, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8394854586129754, + .relative_height = 0.8751387347391787, + .relative_x = 0.0917225950782998, + .relative_y = 0.0416204217536071, }, 0xea99, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0642857142857143, - .group_height = 2.0929152148664345, - .group_x = 0.0302013422818792, - .group_y = 0.2269700332963374, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9395973154362416, + .relative_height = 0.4778024417314096, + .relative_x = 0.0302013422818792, + .relative_y = 0.2269700332963374, }, 0xea9a, 0xeaa1, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3032069970845481, - .group_height = 1.1731770833333333, - .group_x = 0.1526845637583893, - .group_y = 0.0754716981132075, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7673378076062640, + .relative_height = 0.8523862375138734, + .relative_x = 0.1526845637583893, + .relative_y = 0.0754716981132075, }, 0xea9b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1640625000000000, - .group_height = 1.3134110787172011, - .group_x = 0.0721476510067114, - .group_y = 0.0871254162042175, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8590604026845637, + .relative_height = 0.7613762486126526, + .relative_x = 0.0721476510067114, + .relative_y = 0.0871254162042175, }, 0xea9c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1640625000000000, - .group_height = 1.3201465201465201, - .group_x = 0.0721476510067114, - .group_y = 0.0832408435072142, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8590604026845637, + .relative_height = 0.7574916759156493, + .relative_x = 0.0721476510067114, + .relative_y = 0.0832408435072142, }, 0xea9d, 0xeaa0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.4493150684931506, - .group_height = 1.9693989071038251, - .group_x = 0.2863534675615212, - .group_y = 0.2763596004439512, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4082774049217002, + .relative_height = 0.5077691453940066, + .relative_x = 0.2863534675615212, + .relative_y = 0.2763596004439512, }, 0xea9e...0xea9f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.9540983606557376, - .group_height = 2.4684931506849317, - .group_x = 0.2136465324384788, - .group_y = 0.3068812430632630, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5117449664429530, + .relative_height = 0.4051054384017758, + .relative_x = 0.2136465324384788, + .relative_y = 0.3068812430632630, }, 0xeaa2, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2405228758169935, - .group_height = 1.0595187680461982, - .group_x = 0.0679662802950474, - .group_y = 0.0147523709167545, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8061116965226555, + .relative_height = 0.9438247156716689, + .relative_x = 0.0679662802950474, + .relative_y = 0.0147523709167545, }, 0xeab4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0054815974941269, - .group_height = 1.8994082840236686, - .group_y = 0.2024922118380062, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9945482866043613, + .relative_height = 0.5264797507788161, + .relative_y = 0.2024922118380062, }, 0xeab5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8994082840236686, - .group_height = 1.0054815974941269, - .group_x = 0.2024922118380062, - .group_y = 0.0054517133956386, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5264797507788161, + .relative_height = 0.9945482866043613, + .relative_x = 0.2024922118380062, + .relative_y = 0.0054517133956386, }, 0xeab6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8994082840236686, - .group_height = 1.0054815974941269, - .group_x = 0.2710280373831775, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5264797507788161, + .relative_height = 0.9945482866043613, + .relative_x = 0.2710280373831775, }, 0xeab7, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0054815974941269, - .group_height = 1.8994082840236686, - .group_x = 0.0054517133956386, - .group_y = 0.2710280373831775, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9945482866043613, + .relative_height = 0.5264797507788161, + .relative_x = 0.0054517133956386, + .relative_y = 0.2710280373831775, }, 0xead4...0xead5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.4144620811287478, - .group_x = 0.1483790523690773, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7069825436408977, + .relative_x = 0.1483790523690773, }, 0xead6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.1388535031847133, - .group_y = 0.0687919463087248, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8780760626398211, + .relative_y = 0.0687919463087248, }, 0xeb43, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3631840796019901, - .group_height = 1.0003813300793167, - .group_x = 0.1991657977059437, - .group_y = 0.0003811847221163, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7335766423357665, + .relative_height = 0.9996188152778837, + .relative_x = 0.1991657977059437, + .relative_y = 0.0003811847221163, }, 0xeb6e, 0xeb71, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 2.0183246073298431, - .group_y = 0.2522697795071336, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4954604409857328, + .relative_y = 0.2522697795071336, }, 0xeb6f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0104712041884816, - .group_x = 0.2493489583333333, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4973958333333333, + .relative_x = 0.2493489583333333, }, 0xeb70, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0104712041884816, - .group_height = 1.0039062500000000, - .group_x = 0.2493489583333333, - .group_y = 0.0038910505836576, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4973958333333333, + .relative_height = 0.9961089494163424, + .relative_x = 0.2493489583333333, + .relative_y = 0.0038910505836576, }, 0xeb8a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.8828125000000000, - .group_height = 2.9818561935339356, - .group_x = 0.2642276422764228, - .group_y = 0.3313050881410256, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3468834688346883, + .relative_height = 0.3353615785256410, + .relative_x = 0.2642276422764228, + .relative_y = 0.3313050881410256, }, 0xeb9a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1440626883664857, - .group_height = 1.0595187680461982, - .group_x = 0.0679662802950474, - .group_y = 0.0147523709167545, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8740779768177028, + .relative_height = 0.9438247156716689, + .relative_x = 0.0679662802950474, + .relative_y = 0.0147523709167545, }, 0xebd5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0727069351230425, - .group_height = 1.0730882652023592, - .group_y = 0.0681102082395584, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9322210636079249, + .relative_height = 0.9318897917604415, + .relative_y = 0.0681102082395584, }, 0xebd6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.0003554839321263, - .group_y = 0.0003553576082064, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9996446423917936, + .relative_y = 0.0003553576082064, }, 0xec07, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.8604846818377689, - .group_height = 2.9804665603035656, - .group_x = 0.2615335565120357, - .group_y = 0.3311487268518519, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3495911047345768, + .relative_height = 0.3355179398148149, + .relative_x = 0.2615335565120357, + .relative_y = 0.3311487268518519, }, 0xec0b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0721073225265512, - .group_height = 1.0003813300793167, - .group_y = 0.0003811847221163, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9327424400417101, + .relative_height = 0.9996188152778837, + .relative_y = 0.0003811847221163, }, 0xec0c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2486979166666667, - .group_x = 0.1991657977059437, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8008342022940563, + .relative_x = 0.1991657977059437, }, 0xf019, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, }, 0xf030, 0xf03e, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.1426844014510278, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, }, 0xf03d, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.3328631875881523, - .group_y = 0.1248677248677249, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7502645502645503, + .relative_y = 0.1248677248677249, }, 0xf03f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8003104407193382, - .group_x = 0.0005406676069582, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5554597570408116, + .relative_x = 0.0005406676069582, }, 0xf040, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1263939384681190, - .group_height = 1.0007255897868335, - .group_x = 0.0003164442515641, - .group_y = 0.0001959631589261, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8877888683953564, + .relative_height = 0.9992749363119733, + .relative_x = 0.0003164442515641, + .relative_y = 0.0001959631589261, }, 0xf044, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0087313432835820, - .group_height = 1.0077472527472529, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9913442331878375, + .relative_height = 0.9923123057630445, + .relative_y = 0.0002010014265405, }, 0xf04a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.3321224771947897, - .group_y = 0.1247354497354497, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.7506817256817256, + .relative_y = 0.1247354497354497, }, 0xf051, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.7994923857868019, - .group_height = 1.3321224771947897, - .group_y = 0.1247354497354497, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5557122708039492, + .relative_height = 0.7506817256817256, + .relative_y = 0.1247354497354497, }, 0xf052, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1439802384724422, - .group_height = 1.1430071621244535, - .group_y = 0.0626172338785870, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8741409740917385, + .relative_height = 0.8748851565736010, + .relative_y = 0.0626172338785870, }, 0xf053, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0025185185185186, - .group_height = 1.1416267186919362, - .group_y = 0.0620882827561120, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4993711622401420, + .relative_height = 0.8759430588185509, + .relative_y = 0.0620882827561120, }, 0xf05a...0xf05b, 0xf0aa, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.0002824582824583, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987423244802840, + .relative_height = 0.9997176214776941, + .relative_y = 0.0002010014265405, }, 0xf071, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.1426844014510278, - .group_x = 0.0004701457451810, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.8751322751322751, + .relative_x = 0.0004701457451810, + .relative_y = 0.0624338624338624, }, 0xf078, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1434320241691844, - .group_height = 2.0026841590612778, - .group_y = 0.1879786499051550, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8745600777856455, + .relative_height = 0.4993298596163721, + .relative_y = 0.1879786499051550, }, 0xf07b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.2285368802902055, - .group_y = 0.0930118110236220, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.8139763779527559, + .relative_y = 0.0930118110236220, }, 0xf081, 0xf092, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1441233373639663, - .group_height = 1.1430071621244535, - .group_y = 0.0626172338785870, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8740316426933279, + .relative_height = 0.8748851565736010, + .relative_y = 0.0626172338785870, }, 0xf08c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2859733978234582, - .group_height = 1.1426844014510278, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7776210625293841, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, }, 0xf09f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.7489690176588770, - .group_x = 0.0006952841596131, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5717654171704958, + .relative_x = 0.0006952841596131, }, 0xf0a1, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.0749103295228757, - .group_y = 0.0349409448818898, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.9303101594008066, + .relative_y = 0.0349409448818898, }, 0xf0a2, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1429529187840552, - .group_height = 1.0002824582824583, - .group_x = 0.0001253913778381, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8749266777006549, + .relative_height = 0.9997176214776941, + .relative_x = 0.0001253913778381, + .relative_y = 0.0002010014265405, }, 0xf0a3, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0005921977940631, - .group_height = 1.0001448722153810, - .group_x = 0.0005918473033957, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9994081526966043, + .relative_height = 0.9998551487695376, + .relative_x = 0.0005918473033957, }, 0xf0a4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.3332396658348704, - .group_y = 0.1250334663306335, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987423244802840, + .relative_height = 0.7500526916695081, + .relative_y = 0.1250334663306335, }, 0xf0ca, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0335226652102676, - .group_height = 1.2308163060897437, - .group_y = 0.0938253501046103, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9675646540335450, + .relative_height = 0.8124689241215546, + .relative_y = 0.0938253501046103, }, 0xf0d6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.4330042313117066, - .group_y = 0.1510826771653543, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6978346456692913, + .relative_y = 0.1510826771653543, }, 0xf0de, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3984670905653893, - .group_height = 2.6619718309859155, - .group_x = 0.0004030632809351, - .group_y = 0.5708994708994709, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7150686682199350, + .relative_height = 0.3756613756613756, + .relative_x = 0.0004030632809351, + .relative_y = 0.5708994708994709, }, 0xf0e7, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3348918927786344, - .group_height = 1.0001196386424678, - .group_x = 0.0006021702214782, - .group_y = 0.0001196243307751, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7491243338952770, + .relative_height = 0.9998803756692248, + .relative_x = 0.0006021702214782, + .relative_y = 0.0001196243307751, }, 0xf296, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0005202277820979, - .group_height = 1.0386597451628128, - .group_x = 0.0001795653226322, - .group_y = 0.0187142907131644, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9994800427141276, + .relative_height = 0.9627792014248586, + .relative_x = 0.0001795653226322, + .relative_y = 0.0187142907131644, }, 0xf2c4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3292088488938882, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7523272214386461, }, 0xf2c5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0118264574212998, - .group_height = 1.1664315937940761, - .group_x = 0.0004377219006858, - .group_y = 0.0713422007255139, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9883117728988424, + .relative_height = 0.8573155985489722, + .relative_x = 0.0004377219006858, + .relative_y = 0.0713422007255139, }, 0xf2f0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.0342088873926949, - .group_y = 0.0165984862232646, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987423244802840, + .relative_height = 0.9669226518842459, + .relative_y = 0.0165984862232646, }, 0xf306, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3001222493887530, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7691584391161260, }, else => null, }; diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index a103a30ac..4965dabe4 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -50,10 +50,10 @@ class PatchSetAttributeEntry(TypedDict): stretch: str params: dict[str, float | bool] - group_x: float - group_y: float - group_width: float - group_height: float + relative_x: float + relative_y: float + relative_width: float + relative_height: float class PatchSet(TypedDict): @@ -143,7 +143,7 @@ def parse_alignment(val: str) -> str | None: return { "l": ".start", "r": ".end", - "c": ".center", + "c": ".center1", # font-patcher specific centering rule, see face.zig "": None, }.get(val, ".none") @@ -158,10 +158,10 @@ def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash: float(params.get("overlap", 0.0)), float(params.get("xy-ratio", -1.0)), float(params.get("ypadding", 0.0)), - float(attr.get("group_x", 0.0)), - float(attr.get("group_y", 0.0)), - float(attr.get("group_width", 1.0)), - float(attr.get("group_height", 1.0)), + float(attr.get("relative_x", 0.0)), + float(attr.get("relative_y", 0.0)), + float(attr.get("relative_width", 1.0)), + float(attr.get("relative_height", 1.0)), ) @@ -187,10 +187,10 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) stretch = attr.get("stretch", "") params = attr.get("params", {}) - group_x = attr.get("group_x", 0.0) - group_y = attr.get("group_y", 0.0) - group_width = attr.get("group_width", 1.0) - group_height = attr.get("group_height", 1.0) + relative_x = attr.get("relative_x", 0.0) + relative_y = attr.get("relative_y", 0.0) + relative_width = attr.get("relative_width", 1.0) + relative_height = attr.get("relative_height", 1.0) overlap = params.get("overlap", 0.0) xy_ratio = params.get("xy-ratio", -1.0) @@ -204,28 +204,30 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) s = f"{keys}\n => .{{\n" - # These translations don't quite capture the way - # the actual patcher does scaling, but they're a - # good enough compromise. - if "xy" in stretch: - s += " .size_horizontal = .stretch,\n" - s += " .size_vertical = .stretch,\n" - elif "!" in stretch or "^" in stretch: - s += " .size_horizontal = .cover,\n" - s += " .size_vertical = .fit,\n" + # This maps the font_patcher stretch rules to a Constrain instance + # NOTE: some comments in font_patcher indicate that only x or y + # would also be a valid spec, but no icons use it, so we won't + # support it until we have to. + if "pa" in stretch: + if "!" in stretch or overlap: + s += " .size = .cover,\n" + else: + s += " .size = .fit_cover1,\n" + elif "xy" in stretch: + s += " .size = .stretch,\n" else: - s += " .size_horizontal = .fit,\n" - s += " .size_vertical = .fit,\n" + print(f"Warning: Unknown stretch rule {stretch}") - # `^` indicates that scaling should fill - # the whole cell, not just the icon height. + # `^` indicates that scaling should use the + # full cell height, not just the icon height, + # even when the constraint width is 1 if "^" not in stretch: s += " .height = .icon,\n" # There are two cases where we want to limit the constraint width to 1: # - If there's a `1` in the stretch mode string. - # - If the stretch mode is `xy` and there's not an explicit `2`. - if "1" in stretch or ("xy" in stretch and "2" not in stretch): + # - If the stretch mode is not `pa` and there's not an explicit `2`. + if "1" in stretch or ("pa" not in stretch and "2" not in stretch): s += " .max_constraint_width = 1,\n" if align is not None: @@ -233,14 +235,14 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if valign is not None: s += f" .align_vertical = {valign},\n" - if group_width != 1.0: - s += f" .group_width = {group_width:.16f},\n" - if group_height != 1.0: - s += f" .group_height = {group_height:.16f},\n" - if group_x != 0.0: - s += f" .group_x = {group_x:.16f},\n" - if group_y != 0.0: - s += f" .group_y = {group_y:.16f},\n" + if relative_width != 1.0: + s += f" .relative_width = {relative_width:.16f},\n" + if relative_height != 1.0: + s += f" .relative_height = {relative_height:.16f},\n" + if relative_x != 0.0: + s += f" .relative_x = {relative_x:.16f},\n" + if relative_y != 0.0: + s += f" .relative_y = {relative_y:.16f},\n" # `overlap` and `ypadding` are mutually exclusive, # this is asserted in the nerd fonts patcher itself. @@ -286,7 +288,7 @@ def generate_zig_switch_arms( yMin = math.inf xMax = -math.inf yMax = -math.inf - individual_bounds: dict[int, tuple[int, int, int ,int]] = {} + individual_bounds: dict[int, tuple[int, int, int, int]] = {} for cp in group: if cp not in cmap: continue @@ -306,10 +308,10 @@ def generate_zig_switch_arms( this_bounds = individual_bounds[cp] this_width = this_bounds[2] - this_bounds[0] this_height = this_bounds[3] - this_bounds[1] - entries[cp]["group_width"] = group_width / this_width - entries[cp]["group_height"] = group_height / this_height - entries[cp]["group_x"] = (this_bounds[0] - xMin) / group_width - entries[cp]["group_y"] = (this_bounds[1] - yMin) / group_height + entries[cp]["relative_width"] = this_width / group_width + entries[cp]["relative_height"] = this_height / group_height + entries[cp]["relative_x"] = (this_bounds[0] - xMin) / group_width + entries[cp]["relative_y"] = (this_bounds[1] - yMin) / group_height del entries[0] @@ -350,7 +352,7 @@ if __name__ == "__main__": const Constraint = @import("face.zig").RenderOptions.Constraint; -/// Get the a constraints for the provided codepoint. +/// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { """) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fbc8cab99..802c769a6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3093,8 +3093,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // its cell(s), we don't modify the alignment at all. .constraint = getConstraint(cp) orelse if (cellpkg.isSymbol(cp)) .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit, } else .none, .constraint_width = constraintWidth(cell_pin), }, From b643d30d60e2540de6aea775b077b7208a20a7d2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Sep 2025 12:24:03 -0700 Subject: [PATCH 095/319] move test out of terminal to avoid lib-vt catch --- src/renderer/cell.zig | 94 ++++++++++++++++++++++++++++++++++++++++ src/terminal/Screen.zig | 95 ----------------------------------------- 2 files changed, 94 insertions(+), 95 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index d54e98811..46e660bfd 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -513,3 +513,97 @@ test "Contents with zero-sized screen" { c.setCursor(null, null); try testing.expect(c.getCursorGlyph() == null); } + +test "Screen cell constraint widths" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try terminal.Screen.init(alloc, 4, 1, 0); + defer s.deinit(); + + // for each case, the numbers in the comment denote expected + // constraint widths for the symbol-containing cells + + // symbol->nothing: 2 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + + // symbol->character: 1 + { + try s.testWriteString("z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // symbol->space: 2 + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + // symbol->no-break space: 1 + { + try s.testWriteString("\u{00a0}z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // symbol->end of row: 1 + { + try s.testWriteString(" "); + const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p3)); + s.reset(); + } + + // character->symbol: 2 + { + try s.testWriteString("z"); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p1)); + s.reset(); + } + + // symbol->symbol: 1,1 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + try testing.expectEqual(1, constraintWidth(p1)); + s.reset(); + } + + // symbol->space->symbol: 2,2 + { + try s.testWriteString(" "); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + try testing.expectEqual(2, constraintWidth(p2)); + s.reset(); + } + + // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p1)); + s.reset(); + } +} diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0a5c8e7b0..7be4d7c12 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -14,7 +14,6 @@ const Selection = @import("Selection.zig"); const PageList = @import("PageList.zig"); const StringMap = @import("StringMap.zig"); const pagepkg = @import("page.zig"); -const cellpkg = @import("../renderer/cell.zig"); const point = @import("point.zig"); const size = @import("size.zig"); const style = @import("style.zig"); @@ -9098,97 +9097,3 @@ test "Screen UTF8 cell map with blank prefix" { .y = 1, }, cell_map.items[3]); } - -test "Screen cell constraint widths" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, 4, 1, 0); - defer s.deinit(); - - // for each case, the numbers in the comment denote expected - // constraint widths for the symbol-containing cells - - // symbol->nothing: 2 - { - try s.testWriteString(""); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - try testing.expectEqual(2, cellpkg.constraintWidth(p0)); - s.reset(); - } - - // symbol->character: 1 - { - try s.testWriteString("z"); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - try testing.expectEqual(1, cellpkg.constraintWidth(p0)); - s.reset(); - } - - // symbol->space: 2 - { - try s.testWriteString(" z"); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - try testing.expectEqual(2, cellpkg.constraintWidth(p0)); - s.reset(); - } - // symbol->no-break space: 1 - { - try s.testWriteString("\u{00a0}z"); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - try testing.expectEqual(1, cellpkg.constraintWidth(p0)); - s.reset(); - } - - // symbol->end of row: 1 - { - try s.testWriteString(" "); - const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; - try testing.expectEqual(1, cellpkg.constraintWidth(p3)); - s.reset(); - } - - // character->symbol: 2 - { - try s.testWriteString("z"); - const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; - try testing.expectEqual(2, cellpkg.constraintWidth(p1)); - s.reset(); - } - - // symbol->symbol: 1,1 - { - try s.testWriteString(""); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; - try testing.expectEqual(1, cellpkg.constraintWidth(p0)); - try testing.expectEqual(1, cellpkg.constraintWidth(p1)); - s.reset(); - } - - // symbol->space->symbol: 2,2 - { - try s.testWriteString(" "); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; - try testing.expectEqual(2, cellpkg.constraintWidth(p0)); - try testing.expectEqual(2, cellpkg.constraintWidth(p2)); - s.reset(); - } - - // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) - { - try s.testWriteString(""); - const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; - try testing.expectEqual(1, cellpkg.constraintWidth(p0)); - s.reset(); - } - - // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) - { - try s.testWriteString(""); - const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; - try testing.expectEqual(2, cellpkg.constraintWidth(p1)); - s.reset(); - } -} From bc3d0b7cbc2ff4343243eaed55656eb1a1555ea5 Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 05:21:56 +0900 Subject: [PATCH 096/319] fix: the renderer's cursor remains in an unfocused state (block_hollow) --- .../Sources/Features/Terminal/BaseTerminalController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index c1c350f9d..4298dd1e6 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -240,7 +240,12 @@ class BaseTerminalController: NSWindowController, // Move focus to the target surface and activate the window/app DispatchQueue.main.async { - Ghostty.moveFocus(to: view, from: self.focusedSurface) + // We suppress the spurious unfocus signal by passing nil for `from` + // when the surface is already the logically focused one. + Ghostty.moveFocus( + to: view, + from: (self.focusedSurface == view) ? nil : self.focusedSurface + ) view.window?.makeKeyAndOrderFront(nil) if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) From b3d0b6a965575da7efecea02f6478136630dee6e Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 05:58:21 +0900 Subject: [PATCH 097/319] refactor: no need to set from for moveFocus probably --- .../Features/QuickTerminal/QuickTerminalController.swift | 2 +- .../Sources/Features/Terminal/BaseTerminalController.swift | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 17650b5c6..eaefbf55b 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -257,7 +257,7 @@ class QuickTerminalController: BaseTerminalController { guard surfaceTree.contains(view) else { return } // Set the target surface as focused before animation DispatchQueue.main.async { - Ghostty.moveFocus(to: view, from: self.focusedSurface) + Ghostty.moveFocus(to: view) } // Animation completion handler will handle window/app activation animateIn() diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 4298dd1e6..a34be4125 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -240,12 +240,7 @@ class BaseTerminalController: NSWindowController, // Move focus to the target surface and activate the window/app DispatchQueue.main.async { - // We suppress the spurious unfocus signal by passing nil for `from` - // when the surface is already the logically focused one. - Ghostty.moveFocus( - to: view, - from: (self.focusedSurface == view) ? nil : self.focusedSurface - ) + Ghostty.moveFocus(to: view) view.window?.makeKeyAndOrderFront(nil) if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) From 373be614828b146d468cf46e8f3958e76f93934d Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 06:36:29 +0900 Subject: [PATCH 098/319] docs --- .../Features/QuickTerminal/QuickTerminalController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index eaefbf55b..fcc8c6505 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -255,7 +255,7 @@ class QuickTerminalController: BaseTerminalController { } // Check if target surface belongs to this quick terminal guard surfaceTree.contains(view) else { return } - // Set the target surface as focused before animation + // Set the target surface as focused DispatchQueue.main.async { Ghostty.moveFocus(to: view) } From c58a8b27b6032b4eb20555ddae244c684f71888e Mon Sep 17 00:00:00 2001 From: himura467 Date: Tue, 30 Sep 2025 07:14:09 +0900 Subject: [PATCH 099/319] chore: update iOS membership exceptions --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cd9e56186..e53f6d468 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ "Features/App Intents/CommandPaletteIntent.swift", "Features/App Intents/Entities/CommandEntity.swift", "Features/App Intents/Entities/TerminalEntity.swift", + "Features/App Intents/FocusTerminalIntent.swift", "Features/App Intents/GetTerminalDetailsIntent.swift", "Features/App Intents/GhosttyIntentError.swift", "Features/App Intents/InputIntent.swift", From bdf07727ad91477b8d129b6e5d0498d4ec2b5d9b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 29 Sep 2025 19:06:11 -0500 Subject: [PATCH 100/319] gtk: some bell features need to happen on receipt of every BEL Some bell features should be triggered on the receipt of every BEL character, namely `audio` and `system`. However, Ghostty was setting a boolean to `true` upon the receipt of the first BEL. Subsequent BEL characters would be ignored until that boolean was reset to `false`, usually by keyboard/mouse activity. This PR fixes the problem by ensuring that the `audio` and `system` features are triggered every time a BEL is received. Other features continue to be triggered only when the `bell-ringing` boolean state changes. Fixes #8957 --- src/apprt/gtk/class/surface.zig | 52 +++++++++++++++++++++++++++----- src/apprt/gtk/ui/1.2/surface.blp | 1 - 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index fb933073c..344bf8f21 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -51,6 +51,13 @@ pub const Surface = extern struct { pub const Tree = datastruct.SplitTree(Self); pub const properties = struct { + /// This property is set to true when the bell is ringing. Note that + /// this property will only emit a changed signal when there is a + /// full state change. If a bell is ringing and another bell event + /// comes through, the change notification will NOT be emitted. + /// + /// If you need to know every scenario the bell is triggered, + /// listen to the `bell` signal instead. pub const @"bell-ringing" = struct { pub const name = "bell-ringing"; const impl = gobject.ext.defineProperty( @@ -296,6 +303,19 @@ pub const Surface = extern struct { }; pub const signals = struct { + /// Emitted whenever the bell event is received. Unlike the + /// `bell-ringing` property, this is emitted every time the event + /// is received and not just on state changes. + pub const bell = struct { + pub const name = "bell"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; /// Emitted whenever the surface would like to be closed for any /// reason. /// @@ -1674,6 +1694,16 @@ pub const Surface = extern struct { } pub fn setBellRinging(self: *Self, ringing: bool) void { + // Prevent duplicate change notifications if the signals we emit + // in this function cause this state to change again. + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + + // Logic around bell reaction happens on every event even if we're + // already in the ringing state. + if (ringing) self.ringBell(); + + // Property change only happens on actual state change const priv = self.private(); if (priv.bell_ringing == ringing) return; priv.bell_ringing = ringing; @@ -1858,20 +1888,26 @@ pub const Surface = extern struct { self.as(gtk.Widget).setCursorFromName(name.ptr); } - fn propBellRinging( - self: *Self, - _: *gobject.ParamSpec, - _: ?*anyopaque, - ) callconv(.c) void { + /// Handle bell features that need to happen every time a BEL is received + /// Currently this is audio and system but this could change in the future. + fn ringBell(self: *Self) void { const priv = self.private(); - if (!priv.bell_ringing) return; + + // Emit the signal + signals.bell.impl.emit( + self, + null, + .{}, + null, + ); // Activate actions if they exist _ = self.as(gtk.Widget).activateAction("tab.ring-bell", null); _ = self.as(gtk.Widget).activateAction("win.ring-bell", null); - // Do our sound const config = if (priv.config) |c| c.get() else return; + + // Do our sound if (config.@"bell-features".audio) audio: { const config_path = config.@"bell-audio-path" orelse break :audio; const path, const required = switch (config_path) { @@ -2859,7 +2895,6 @@ pub const Surface = extern struct { class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); - class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown); @@ -2884,6 +2919,7 @@ pub const Surface = extern struct { }); // Signals + signals.bell.impl.register(.{}); signals.@"close-request".impl.register(.{}); signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index ad971e991..7ed78ecb3 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -169,7 +169,6 @@ template $GhosttySurface: Adw.Bin { "surface", ] - notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url(); From 45e61b1a17973825de15d34061f6a5331de29ca2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Sep 2025 06:55:12 -0700 Subject: [PATCH 101/319] update deps files --- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build.zig.zon.json b/build.zig.zon.json index ac4098f96..6e8ea3acc 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -109,10 +109,10 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "uucode-0.0.0-ZZjBPk0GQACuYIoFqT_Vzkvn8Ur_M3dE7o4DNUE65Z7v": { + "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz", - "hash": "sha256-j1ZNumH19olw0DHTEb6sChnCZpYhK9+1Q/rr6nxPRcQ=" + "url": "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", + "hash": "sha256-iq9Oyns5e5Tnz2BKPPPTuyJ03BN4bK0dsmSPE1s0wig=" }, "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { "name": "vaxis", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 24bb3b258..4e0d2bede 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -259,11 +259,11 @@ in }; } { - name = "uucode-0.0.0-ZZjBPk0GQACuYIoFqT_Vzkvn8Ur_M3dE7o4DNUE65Z7v"; + name = "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz"; - hash = "sha256-j1ZNumH19olw0DHTEb6sChnCZpYhK9+1Q/rr6nxPRcQ="; + url = "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz"; + hash = "sha256-iq9Oyns5e5Tnz2BKPPPTuyJ03BN4bK0dsmSPE1s0wig="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 5b5e542dd..4d7e350a0 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst -https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz +https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 824f76adc..a6eaf846c 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -133,9 +133,9 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz", - "dest": "vendor/p/uucode-0.0.0-ZZjBPk0GQACuYIoFqT_Vzkvn8Ur_M3dE7o4DNUE65Z7v", - "sha256": "8f564dba61f5f68970d031d311beac0a19c26696212bdfb543faebea7c4f45c4" + "url": "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", + "dest": "vendor/p/uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE", + "sha256": "8aaf4eca7b397b94e7cf604a3cf3d3bb2274dc13786cad1db2648f135b34c228" }, { "type": "git", From 837ac9be774472132ec8302af8d2da0b56818c91 Mon Sep 17 00:00:00 2001 From: azhn Date: Sun, 28 Sep 2025 17:46:24 +1000 Subject: [PATCH 102/319] lib-vt: Add SemanticVersion to module - Provide SONAME versioned shared libraries with major version numbers to separate ABI breaks --- src/build/GhosttyLibVt.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 80f2bf9ad..9eb945293 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -27,6 +27,7 @@ pub fn initShared( const lib = b.addSharedLibrary(.{ .name = "ghostty-vt", .root_module = zig.vt_c, + .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, }); lib.installHeader( b.path("include/ghostty/vt.h"), From fcea09e413a55c677dca377f716aa9bc6465306b Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 28 Sep 2025 18:44:08 -0600 Subject: [PATCH 103/319] renderer: slightly optimize screen copy Changes it so that the renderer retains its own MemoryPool for PageList pages so that new pages rarely need to be allocated when cloning the screen. Also switches to using an arena allocator in `updateFrame` to avoid having to deinit the cloned screen since instead we can just throw out the memory. --- src/renderer/generic.zig | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 802c769a6..b5d3b5661 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -95,6 +95,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Allocator that can be used alloc: std.mem.Allocator, + /// MemoryPool for PageList pages which we use when cloning the screen. + page_pool: terminal.PageList.MemoryPool, + /// This mutex must be held whenever any state used in `drawFrame` is /// being modified, and also when it's being accessed in `drawFrame`. draw_mutex: std.Thread.Mutex = .{}, @@ -676,8 +679,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; errdefer if (display_link) |v| v.release(); + // We preheat the page pool with 4 pages- this is an arbitrary + // choice based on what seems reasonable for the number of pages + // used by the viewport area. + var page_pool: terminal.PageList.MemoryPool = try .init( + alloc, + std.heap.page_allocator, + 4, + ); + errdefer page_pool.deinit(); + var result: Self = .{ .alloc = alloc, + .page_pool = page_pool, .config = options.config, .surface_mailbox = options.surface_mailbox, .grid_metrics = font_critical.metrics, @@ -760,6 +774,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } pub fn deinit(self: *Self) void { + self.page_pool.deinit(); + self.swap_chain.deinit(); if (DisplayLink != void) { @@ -1092,6 +1108,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { full_rebuild: bool, }; + // Empty our page pool, but retain capacity. + self.page_pool.reset(.retain_capacity); + + var arena: std.heap.ArenaAllocator = .init(self.alloc); + defer arena.deinit(); + const alloc = arena.allocator(); + // Update all our data as tightly as possible within the mutex. var critical: Critical = critical: { // const start = try std.time.Instant.now(); @@ -1148,12 +1171,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // We used to share terminal state, but we've since learned through // analysis that it is faster to copy the terminal state than to // hold the lock while rebuilding GPU cells. - var screen_copy = try state.terminal.screen.clone( - self.alloc, + const screen_copy = try state.terminal.screen.clonePool( + alloc, + &self.page_pool, .{ .viewport = .{} }, null, ); - errdefer screen_copy.deinit(); // Whether to draw our cursor or not. const cursor_style = if (state.terminal.flags.password_input) @@ -1169,9 +1192,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const preedit: ?renderer.State.Preedit = preedit: { if (cursor_style == null) break :preedit null; const p = state.preedit orelse break :preedit null; - break :preedit try p.clone(self.alloc); + break :preedit try p.clone(alloc); }; - errdefer if (preedit) |p| p.deinit(self.alloc); // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if @@ -1241,10 +1263,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .full_rebuild = full_rebuild, }; }; - defer { - critical.screen.deinit(); - if (critical.preedit) |p| p.deinit(self.alloc); - } // Build our GPU cells try self.rebuildCells( From 4136c469fad7a85260c2e23b8aca7a1a31aff686 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 28 Sep 2025 21:23:01 -0600 Subject: [PATCH 104/319] datastruct: make trivial linked list ops inline Supported by benchmarks (vtebench on Apple M3 Max) --- src/datastruct/intrusive_linked_list.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/datastruct/intrusive_linked_list.zig b/src/datastruct/intrusive_linked_list.zig index 61bf8157c..734b82fff 100644 --- a/src/datastruct/intrusive_linked_list.zig +++ b/src/datastruct/intrusive_linked_list.zig @@ -23,7 +23,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 insertAfter(list: *Self, node: *Node, new_node: *Node) void { + pub inline fn insertAfter(list: *Self, node: *Node, new_node: *Node) void { new_node.prev = node; if (node.next) |next_node| { // Intermediate node. @@ -42,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: *Node, new_node: *Node) void { + pub inline fn insertBefore(list: *Self, node: *Node, new_node: *Node) void { new_node.next = node; if (node.prev) |prev_node| { // Intermediate node. @@ -60,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: *Node) void { + pub inline fn append(list: *Self, new_node: *Node) void { if (list.last) |last| { // Insert after last. list.insertAfter(last, new_node); @@ -74,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: *Node) void { + pub inline fn prepend(list: *Self, new_node: *Node) void { if (list.first) |first| { // Insert before first. list.insertBefore(first, new_node); @@ -91,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: *Node) void { + pub inline fn remove(list: *Self, node: *Node) void { if (node.prev) |prev_node| { // Intermediate node. prev_node.next = node.next; @@ -113,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) ?*Node { + pub inline fn pop(list: *Self) ?*Node { const last = list.last orelse return null; list.remove(last); return last; @@ -123,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) ?*Node { + pub inline fn popFirst(list: *Self) ?*Node { const first = list.first orelse return null; list.remove(first); return first; From 43dd7120531e10a990fd87e52eecbf40804fdba3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 28 Sep 2025 21:23:44 -0600 Subject: [PATCH 105/319] termio: make trivial stream handler callbacks inline Supported by benchmarks (vtebench on Apple M3 Max) --- src/termio/stream_handler.zig | 110 +++++++++++++++++----------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index f9bc03500..2d90831f2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -186,19 +186,19 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); } - pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void { + pub inline fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void { var cmd = self.dcs.hook(self.alloc, dcs) orelse return; defer cmd.deinit(); try self.dcsCommand(&cmd); } - pub fn dcsPut(self: *StreamHandler, byte: u8) !void { + pub inline fn dcsPut(self: *StreamHandler, byte: u8) !void { var cmd = self.dcs.put(byte) orelse return; defer cmd.deinit(); try self.dcsCommand(&cmd); } - pub fn dcsUnhook(self: *StreamHandler) !void { + pub inline fn dcsUnhook(self: *StreamHandler) !void { var cmd = self.dcs.unhook() orelse return; defer cmd.deinit(); try self.dcsCommand(&cmd); @@ -293,11 +293,11 @@ pub const StreamHandler = struct { } } - pub fn apcStart(self: *StreamHandler) !void { + pub inline fn apcStart(self: *StreamHandler) !void { self.apc.start(); } - pub fn apcPut(self: *StreamHandler, byte: u8) !void { + pub inline fn apcPut(self: *StreamHandler, byte: u8) !void { self.apc.feed(self.alloc, byte); } @@ -322,23 +322,23 @@ pub const StreamHandler = struct { } } - pub fn print(self: *StreamHandler, ch: u21) !void { + pub inline fn print(self: *StreamHandler, ch: u21) !void { try self.terminal.print(ch); } - pub fn printRepeat(self: *StreamHandler, count: usize) !void { + pub inline fn printRepeat(self: *StreamHandler, count: usize) !void { try self.terminal.printRepeat(count); } - pub fn bell(self: *StreamHandler) !void { + pub inline fn bell(self: *StreamHandler) !void { self.surfaceMessageWriter(.ring_bell); } - pub fn backspace(self: *StreamHandler) !void { + pub inline fn backspace(self: *StreamHandler) !void { self.terminal.backspace(); } - pub fn horizontalTab(self: *StreamHandler, count: u16) !void { + pub inline fn horizontalTab(self: *StreamHandler, count: u16) !void { for (0..count) |_| { const x = self.terminal.screen.cursor.x; try self.terminal.horizontalTab(); @@ -346,7 +346,7 @@ pub const StreamHandler = struct { } } - pub fn horizontalTabBack(self: *StreamHandler, count: u16) !void { + pub inline fn horizontalTabBack(self: *StreamHandler, count: u16) !void { for (0..count) |_| { const x = self.terminal.screen.cursor.x; try self.terminal.horizontalTabBack(); @@ -354,61 +354,61 @@ pub const StreamHandler = struct { } } - pub fn linefeed(self: *StreamHandler) !void { + pub inline fn linefeed(self: *StreamHandler) !void { // Small optimization: call index instead of linefeed because they're // identical and this avoids one layer of function call overhead. try self.terminal.index(); } - pub fn carriageReturn(self: *StreamHandler) !void { + pub inline fn carriageReturn(self: *StreamHandler) !void { self.terminal.carriageReturn(); } - pub fn setCursorLeft(self: *StreamHandler, amount: u16) !void { + pub inline fn setCursorLeft(self: *StreamHandler, amount: u16) !void { self.terminal.cursorLeft(amount); } - pub fn setCursorRight(self: *StreamHandler, amount: u16) !void { + pub inline fn setCursorRight(self: *StreamHandler, amount: u16) !void { self.terminal.cursorRight(amount); } - pub fn setCursorDown(self: *StreamHandler, amount: u16, carriage: bool) !void { + pub inline fn setCursorDown(self: *StreamHandler, amount: u16, carriage: bool) !void { self.terminal.cursorDown(amount); if (carriage) self.terminal.carriageReturn(); } - pub fn setCursorUp(self: *StreamHandler, amount: u16, carriage: bool) !void { + pub inline fn setCursorUp(self: *StreamHandler, amount: u16, carriage: bool) !void { self.terminal.cursorUp(amount); if (carriage) self.terminal.carriageReturn(); } - pub fn setCursorCol(self: *StreamHandler, col: u16) !void { + pub inline fn setCursorCol(self: *StreamHandler, col: u16) !void { self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, col); } - pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void { + pub inline fn setCursorColRelative(self: *StreamHandler, offset: u16) !void { self.terminal.setCursorPos( self.terminal.screen.cursor.y + 1, self.terminal.screen.cursor.x + 1 +| offset, ); } - pub fn setCursorRow(self: *StreamHandler, row: u16) !void { + pub inline fn setCursorRow(self: *StreamHandler, row: u16) !void { self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1); } - pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void { + pub inline fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void { self.terminal.setCursorPos( self.terminal.screen.cursor.y + 1 +| offset, self.terminal.screen.cursor.x + 1, ); } - pub fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { + pub inline fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { self.terminal.setCursorPos(row, col); } - pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { + pub inline fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { if (mode == .complete) { // Whenever we erase the full display, scroll to bottom. try self.terminal.scrollViewport(.{ .bottom = {} }); @@ -418,48 +418,48 @@ pub const StreamHandler = struct { self.terminal.eraseDisplay(mode, protected); } - pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { + pub inline fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { self.terminal.eraseLine(mode, protected); } - pub fn deleteChars(self: *StreamHandler, count: usize) !void { + pub inline fn deleteChars(self: *StreamHandler, count: usize) !void { self.terminal.deleteChars(count); } - pub fn eraseChars(self: *StreamHandler, count: usize) !void { + pub inline fn eraseChars(self: *StreamHandler, count: usize) !void { self.terminal.eraseChars(count); } - pub fn insertLines(self: *StreamHandler, count: usize) !void { + pub inline fn insertLines(self: *StreamHandler, count: usize) !void { self.terminal.insertLines(count); } - pub fn insertBlanks(self: *StreamHandler, count: usize) !void { + pub inline fn insertBlanks(self: *StreamHandler, count: usize) !void { self.terminal.insertBlanks(count); } - pub fn deleteLines(self: *StreamHandler, count: usize) !void { + pub inline fn deleteLines(self: *StreamHandler, count: usize) !void { self.terminal.deleteLines(count); } - pub fn reverseIndex(self: *StreamHandler) !void { + pub inline fn reverseIndex(self: *StreamHandler) !void { self.terminal.reverseIndex(); } - pub fn index(self: *StreamHandler) !void { + pub inline fn index(self: *StreamHandler) !void { try self.terminal.index(); } - pub fn nextLine(self: *StreamHandler) !void { + pub inline fn nextLine(self: *StreamHandler) !void { try self.terminal.index(); self.terminal.carriageReturn(); } - pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { + pub inline fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { self.terminal.setTopAndBottomMargin(top, bot); } - pub fn setLeftAndRightMarginAmbiguous(self: *StreamHandler) !void { + pub inline fn setLeftAndRightMarginAmbiguous(self: *StreamHandler) !void { if (self.terminal.modes.get(.enable_left_and_right_margin)) { try self.setLeftAndRightMargin(0, 0); } else { @@ -467,7 +467,7 @@ pub const StreamHandler = struct { } } - pub fn setLeftAndRightMargin(self: *StreamHandler, left: u16, right: u16) !void { + pub inline fn setLeftAndRightMargin(self: *StreamHandler, left: u16, right: u16) !void { self.terminal.setLeftAndRightMargin(left, right); } @@ -504,12 +504,12 @@ pub const StreamHandler = struct { self.messageWriter(msg); } - pub fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void { + pub inline fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void { // log.debug("save mode={}", .{mode}); self.terminal.modes.save(mode); } - pub fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void { + pub inline fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void { // For restore mode we have to restore but if we set it, we // always have to call setMode because setting some modes have // side effects and we want to make sure we process those. @@ -696,11 +696,11 @@ pub const StreamHandler = struct { } } - pub fn setMouseShiftCapture(self: *StreamHandler, v: bool) !void { + pub inline fn setMouseShiftCapture(self: *StreamHandler, v: bool) !void { self.terminal.flags.mouse_shift_capture = if (v) .true else .false; } - pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { + pub inline fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { switch (attr) { .unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}), @@ -709,11 +709,11 @@ pub const StreamHandler = struct { } } - pub fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { + pub inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { try self.terminal.screen.startHyperlink(uri, id); } - pub fn endHyperlink(self: *StreamHandler) !void { + pub inline fn endHyperlink(self: *StreamHandler) !void { self.terminal.screen.endHyperlink(); } @@ -832,31 +832,31 @@ pub const StreamHandler = struct { } } - pub fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void { + pub inline fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void { self.terminal.setProtectedMode(mode); } - pub fn decaln(self: *StreamHandler) !void { + pub inline fn decaln(self: *StreamHandler) !void { try self.terminal.decaln(); } - pub fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { + pub inline fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { self.terminal.tabClear(cmd); } - pub fn tabSet(self: *StreamHandler) !void { + pub inline fn tabSet(self: *StreamHandler) !void { self.terminal.tabSet(); } - pub fn tabReset(self: *StreamHandler) !void { + pub inline fn tabReset(self: *StreamHandler) !void { self.terminal.tabReset(); } - pub fn saveCursor(self: *StreamHandler) !void { + pub inline fn saveCursor(self: *StreamHandler) !void { self.terminal.saveCursor(); } - pub fn restoreCursor(self: *StreamHandler) !void { + pub inline fn restoreCursor(self: *StreamHandler) !void { try self.terminal.restoreCursor(); } @@ -865,11 +865,11 @@ pub const StreamHandler = struct { self.messageWriter(try termio.Message.writeReq(self.alloc, self.enquiry_response)); } - pub fn scrollDown(self: *StreamHandler, count: usize) !void { + pub inline fn scrollDown(self: *StreamHandler, count: usize) !void { self.terminal.scrollDown(count); } - pub fn scrollUp(self: *StreamHandler, count: usize) !void { + pub inline fn scrollUp(self: *StreamHandler, count: usize) !void { self.terminal.scrollUp(count); } @@ -995,7 +995,7 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.{ .set_title = buf }); } - pub fn setMouseShape( + pub inline fn setMouseShape( self: *StreamHandler, shape: terminal.MouseShape, ) !void { @@ -1037,22 +1037,22 @@ pub const StreamHandler = struct { }); } - pub fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void { + pub inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void { _ = aid; self.terminal.markSemanticPrompt(.prompt); self.terminal.flags.shell_redraws_prompt = redraw; } - pub fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void { + pub inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void { _ = aid; self.terminal.markSemanticPrompt(.prompt_continuation); } - pub fn promptEnd(self: *StreamHandler) !void { + pub inline fn promptEnd(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.input); } - pub fn endOfInput(self: *StreamHandler) !void { + pub inline fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); } From 0388a2b39618318032a6f88e707f0dd39b02c741 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 28 Sep 2025 21:26:12 -0600 Subject: [PATCH 106/319] terminal: inline all the things A whole bunch of inline annotations, some of these were tracked down with Instruments.app, others are guesses / just seemed right because they were trivial wrapper functions. Regardless, these changes are ultimately supported by improved vtebench results on my machine (Apple M3 Max). --- src/terminal/PageList.zig | 38 ++++++++++++------------ src/terminal/Parser.zig | 8 ++--- src/terminal/Screen.zig | 24 +++++++-------- src/terminal/page.zig | 62 +++++++++++++++++++-------------------- src/terminal/point.zig | 2 +- src/terminal/size.zig | 2 +- src/terminal/stream.zig | 24 +++++++-------- 7 files changed, 80 insertions(+), 80 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index b8e16dbf7..8aeb6f6dc 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1861,7 +1861,7 @@ pub fn maxSize(self: *const PageList) usize { } /// Returns true if we need to grow into our active area. -fn growRequiredForActive(self: *const PageList) bool { +inline fn growRequiredForActive(self: *const PageList) bool { var rows: usize = 0; var page = self.pages.last; while (page) |p| : (page = p.prev) { @@ -2047,7 +2047,7 @@ pub fn adjustCapacity( /// Create a new page node. This does not add it to the list and this /// does not do any memory size accounting with max_size/page_size. -fn createPage( +inline fn createPage( self: *PageList, cap: Capacity, ) Allocator.Error!*List.Node { @@ -2055,7 +2055,7 @@ fn createPage( return try createPageExt(&self.pool, cap, &self.page_size); } -fn createPageExt( +inline fn createPageExt( pool: *MemoryPool, cap: Capacity, total_size: ?*usize, @@ -3394,7 +3394,7 @@ pub const Pin = struct { y: size.CellCountInt = 0, x: size.CellCountInt = 0, - pub fn rowAndCell(self: Pin) struct { + pub inline fn rowAndCell(self: Pin) struct { row: *pagepkg.Row, cell: *pagepkg.Cell, } { @@ -3407,7 +3407,7 @@ pub const Pin = struct { /// Returns the cells for the row that this pin is on. The subset determines /// what subset of the cells are returned. The "left/right" subsets are /// inclusive of the x coordinate of the pin. - pub fn cells(self: Pin, subset: CellSubset) []pagepkg.Cell { + pub inline fn cells(self: Pin, subset: CellSubset) []pagepkg.Cell { const rac = self.rowAndCell(); const all = self.node.data.getCells(rac.row); return switch (subset) { @@ -3419,12 +3419,12 @@ 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 { + pub inline fn grapheme(self: Pin, cell: *const pagepkg.Cell) ?[]u21 { 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 { + pub inline fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style { if (cell.style_id == stylepkg.default_id) return .{}; return self.node.data.styles.get( self.node.data.memory, @@ -3433,12 +3433,12 @@ pub const Pin = struct { } /// Check if this pin is dirty. - pub fn isDirty(self: Pin) bool { + pub inline fn isDirty(self: Pin) bool { return self.node.data.isRowDirty(self.y); } /// Mark this pin location as dirty. - pub fn markDirty(self: Pin) void { + pub inline fn markDirty(self: Pin) void { var set = self.node.data.dirtyBitSet(); set.set(self.y); } @@ -3507,7 +3507,7 @@ pub const Pin = struct { /// pointFromPin and building up the iterator from points. /// /// The limit pin is inclusive. - pub fn pageIterator( + pub inline fn pageIterator( self: Pin, direction: Direction, limit: ?Pin, @@ -3529,7 +3529,7 @@ pub const Pin = struct { }; } - pub fn rowIterator( + pub inline fn rowIterator( self: Pin, direction: Direction, limit: ?Pin, @@ -3546,7 +3546,7 @@ pub const Pin = struct { }; } - pub fn cellIterator( + pub inline fn cellIterator( self: Pin, direction: Direction, limit: ?Pin, @@ -3647,14 +3647,14 @@ pub const Pin = struct { return false; } - pub fn eql(self: Pin, other: Pin) bool { + pub inline fn eql(self: Pin, other: Pin) bool { return self.node == other.node and self.y == other.y and self.x == other.x; } /// Move the pin left n columns. n must fit within the size. - pub fn left(self: Pin, n: usize) Pin { + pub inline fn left(self: Pin, n: usize) Pin { assert(n <= self.x); var result = self; result.x -= std.math.cast(size.CellCountInt, n) orelse result.x; @@ -3662,7 +3662,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 { + pub inline fn right(self: Pin, n: usize) Pin { assert(self.x + n < self.node.data.size.cols); var result = self; result.x +|= std.math.cast(size.CellCountInt, n) orelse @@ -3671,14 +3671,14 @@ pub const Pin = struct { } /// Move the pin left n columns, stopping at the start of the row. - pub fn leftClamp(self: Pin, n: size.CellCountInt) Pin { + pub inline fn leftClamp(self: Pin, n: size.CellCountInt) Pin { var result = self; result.x -|= n; return result; } /// Move the pin right n columns, stopping at the end of the row. - pub fn rightClamp(self: Pin, n: size.CellCountInt) Pin { + pub inline fn rightClamp(self: Pin, n: size.CellCountInt) Pin { var result = self; result.x = @min(self.x +| n, self.node.data.size.cols - 1); return result; @@ -3740,7 +3740,7 @@ pub const Pin = struct { /// Move the pin down a certain number of rows, or return null if /// the pin goes beyond the end of the screen. - pub fn down(self: Pin, n: usize) ?Pin { + pub inline fn down(self: Pin, n: usize) ?Pin { return switch (self.downOverflow(n)) { .offset => |v| v, .overflow => null, @@ -3749,7 +3749,7 @@ pub const Pin = struct { /// Move the pin up a certain number of rows, or return null if /// the pin goes beyond the start of the screen. - pub fn up(self: Pin, n: usize) ?Pin { + pub inline fn up(self: Pin, n: usize) ?Pin { return switch (self.upOverflow(n)) { .offset => |v| v, .overflow => null, diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 6deb03da5..61ac4e312 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -254,7 +254,7 @@ pub fn deinit(self: *Parser) void { /// Next consumes the next character c and returns the actions to execute. /// Up to 3 actions may need to be executed -- in order -- representing /// the state exit, transition, and entry actions. -pub fn next(self: *Parser, c: u8) [3]?Action { +pub inline fn next(self: *Parser, c: u8) [3]?Action { const effect = table[c][@intFromEnum(self.state)]; // log.info("next: {x}", .{c}); @@ -314,7 +314,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { }; } -pub fn collect(self: *Parser, c: u8) void { +pub inline fn collect(self: *Parser, c: u8) void { if (self.intermediates_idx >= MAX_INTERMEDIATE) { log.warn("invalid intermediates count", .{}); return; @@ -324,7 +324,7 @@ pub fn collect(self: *Parser, c: u8) void { self.intermediates_idx += 1; } -fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { +inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { return switch (action) { .none, .ignore => null, .print => Action{ .print = c }, @@ -410,7 +410,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { }; } -pub fn clear(self: *Parser) void { +pub inline fn clear(self: *Parser) void { self.intermediates_idx = 0; self.params_idx = 0; self.params_sep = .initEmpty(); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 7be4d7c12..0c60dcec8 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -533,13 +533,13 @@ pub fn adjustCapacity( return new_node; } -pub fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { +pub inline fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { assert(self.cursor.x + n < self.pages.cols); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); return @ptrCast(cell + n); } -pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { +pub inline fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { assert(self.cursor.x >= n); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); return @ptrCast(cell - n); @@ -959,7 +959,7 @@ fn cursorScrollAboveRotate(self: *Screen) !void { /// Move the cursor down if we're not at the bottom of the screen. Otherwise /// scroll. Currently only used for testing. -fn cursorDownOrScroll(self: *Screen) !void { +inline fn cursorDownOrScroll(self: *Screen) !void { if (self.cursor.y + 1 < self.pages.rows) { self.cursorDown(1); } else { @@ -1034,7 +1034,7 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct { /// page than the old AND we have a style or hyperlink set. In that case, /// we must release our old one and insert the new one, since styles are /// stored per-page. -fn cursorChangePin(self: *Screen, new: Pin) void { +inline fn cursorChangePin(self: *Screen, new: Pin) void { // Moving the cursor affects text run splitting (ligatures) so // we must mark the old and new page dirty. We do this as long // as the pins are not equal @@ -1108,7 +1108,7 @@ fn cursorChangePin(self: *Screen, new: Pin) void { /// Mark the cursor position as dirty. /// TODO: test -pub fn cursorMarkDirty(self: *Screen) void { +pub inline fn cursorMarkDirty(self: *Screen) void { self.cursor.page_pin.markDirty(); } @@ -1160,7 +1160,7 @@ pub const Scroll = union(enum) { }; /// Scroll the viewport of the terminal grid. -pub fn scroll(self: *Screen, behavior: Scroll) void { +pub inline fn scroll(self: *Screen, behavior: Scroll) void { defer self.assertIntegrity(); if (comptime build_options.kitty_graphics) { @@ -1181,7 +1181,7 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { /// See PageList.scrollClear. In addition to that, we reset the cursor /// to be on top. -pub fn scrollClear(self: *Screen) !void { +pub inline fn scrollClear(self: *Screen) !void { defer self.assertIntegrity(); try self.pages.scrollClear(); @@ -1196,14 +1196,14 @@ pub fn scrollClear(self: *Screen) !void { } /// Returns true if the viewport is scrolled to the bottom of the screen. -pub fn viewportIsBottom(self: Screen) bool { +pub inline fn viewportIsBottom(self: Screen) bool { return self.pages.viewport == .active; } /// Erase the region specified by tl and br, inclusive. This will physically /// erase the rows meaning the memory will be reclaimed (if the underlying /// page is empty) and other rows will be shifted up. -pub fn eraseRows( +pub inline fn eraseRows( self: *Screen, tl: point.Point, bl: ?point.Point, @@ -1539,7 +1539,7 @@ pub fn splitCellBoundary( /// Returns the blank cell to use when doing terminal operations that /// require preserving the bg color. -pub fn blankCell(self: *const Screen) Cell { +pub inline fn blankCell(self: *const Screen) Cell { if (self.cursor.style_id == style.default_id) return .{}; return self.cursor.style.bgCell() orelse .{}; } @@ -1557,7 +1557,7 @@ pub fn blankCell(self: *const Screen) Cell { /// probably means the system is in trouble anyways. I'd like to improve this /// in the future but it is not a priority particularly because this scenario /// (resize) is difficult. -pub fn resize( +pub inline fn resize( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt, @@ -1568,7 +1568,7 @@ pub fn resize( /// Resize the screen without any reflow. In this mode, columns/rows will /// be truncated as they are shrunk. If they are grown, the new space is filled /// with zeros. -pub fn resizeWithoutReflow( +pub inline fn resizeWithoutReflow( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt, diff --git a/src/terminal/page.zig b/src/terminal/page.zig index b1a24e9a9..b2fe993d2 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -191,7 +191,7 @@ pub const Page = struct { /// The backing memory is always allocated using mmap directly. /// You cannot use custom allocators with this structure because /// it is critical to performance that we use mmap. - pub fn init(cap: Capacity) !Page { + pub inline fn init(cap: Capacity) !Page { const l = layout(cap); // We use mmap directly to avoid Zig allocator overhead @@ -215,7 +215,7 @@ pub const Page = struct { /// Initialize a new page using the given backing memory. /// It is up to the caller to not call deinit on these pages. - pub fn initBuf(buf: OffsetBuf, l: Layout) Page { + pub inline fn initBuf(buf: OffsetBuf, l: Layout) Page { const cap = l.capacity; const rows = buf.member(Row, l.rows_start); const cells = buf.member(Cell, l.cells_start); @@ -270,13 +270,13 @@ pub const Page = struct { /// Deinitialize the page, freeing any backing memory. Do NOT call /// this if you allocated the backing memory yourself (i.e. you used /// initBuf). - pub fn deinit(self: *Page) void { + pub inline fn deinit(self: *Page) void { posix.munmap(self.memory); self.* = undefined; } /// Reinitialize the page with the same capacity. - pub fn reinit(self: *Page) void { + pub inline fn reinit(self: *Page) void { // We zero the page memory as u64 instead of u8 because // we can and it's empirically quite a bit faster. @memset(@as([*]u64, @ptrCast(self.memory))[0 .. self.memory.len / 8], 0); @@ -306,7 +306,7 @@ pub const Page = struct { /// Temporarily pause integrity checks. This is useful when you are /// doing a lot of operations that would trigger integrity check /// violations but you know the page will end up in a consistent state. - pub fn pauseIntegrityChecks(self: *Page, v: bool) void { + pub inline fn pauseIntegrityChecks(self: *Page, v: bool) void { if (build_options.slow_runtime_safety) { if (v) { self.pause_integrity_checks += 1; @@ -319,7 +319,7 @@ pub const Page = struct { /// A helper that can be used to assert the integrity of the page /// when runtime safety is enabled. This is a no-op when runtime /// safety is disabled. This uses the libc allocator. - pub fn assertIntegrity(self: *const Page) void { + pub inline fn assertIntegrity(self: *const Page) void { if (comptime build_options.slow_runtime_safety) { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; defer _ = debug_allocator.deinit(); @@ -603,7 +603,7 @@ pub const Page = struct { /// Clone the contents of this page. This will allocate new memory /// using the page allocator. If you want to manage memory manually, /// use cloneBuf. - pub fn clone(self: *const Page) !Page { + pub inline fn clone(self: *const Page) !Page { const backing = try posix.mmap( null, self.memory.len, @@ -619,7 +619,7 @@ pub const Page = struct { /// Clone the entire contents of this page. /// /// The buffer must be at least the size of self.memory. - pub fn cloneBuf(self: *const Page, buf: []align(std.heap.page_size_min) u8) Page { + pub inline fn cloneBuf(self: *const Page, buf: []align(std.heap.page_size_min) u8) Page { assert(buf.len >= self.memory.len); // The entire concept behind a page is that everything is stored @@ -671,7 +671,7 @@ pub const Page = struct { /// If the other page has more columns, the extra columns will be /// truncated. If the other page has fewer columns, the extra columns /// will be zeroed. - pub fn cloneFrom( + pub inline fn cloneFrom( self: *Page, other: *const Page, y_start: usize, @@ -695,7 +695,7 @@ pub const Page = struct { } /// Clone a single row from another page into this page. - pub fn cloneRowFrom( + pub inline fn cloneRowFrom( self: *Page, other: *const Page, dst_row: *Row, @@ -912,13 +912,13 @@ pub const Page = struct { } /// Get a single row. y must be valid. - pub fn getRow(self: *const Page, y: usize) *Row { + pub inline fn getRow(self: *const Page, y: usize) *Row { assert(y < self.size.rows); return &self.rows.ptr(self.memory)[y]; } /// Get the cells for a row. - pub fn getCells(self: *const Page, row: *Row) []Cell { + pub inline fn getCells(self: *const Page, row: *Row) []Cell { if (build_options.slow_runtime_safety) { const rows = self.rows.ptr(self.memory); const cells = self.cells.ptr(self.memory); @@ -931,7 +931,7 @@ pub const Page = struct { } /// Get the row and cell for the given X/Y within this page. - pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct { + pub inline fn getRowAndCell(self: *const Page, x: usize, y: usize) struct { row: *Row, cell: *Cell, } { @@ -1016,7 +1016,7 @@ pub const Page = struct { } /// Swap two cells within the same row as quickly as possible. - pub fn swapCells( + pub inline fn swapCells( self: *Page, src: *Cell, dst: *Cell, @@ -1077,7 +1077,7 @@ pub const Page = struct { /// active, Page cannot know this and it will still be ref counted down. /// The best solution for this is to artificially increment the ref count /// prior to calling this function. - pub fn clearCells( + pub inline fn clearCells( self: *Page, row: *Row, left: usize, @@ -1127,14 +1127,14 @@ pub const Page = struct { } /// Returns the hyperlink ID for the given cell. - pub fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id { + pub inline fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id { const cell_offset = getOffset(Cell, self.memory, cell); const map = self.hyperlink_map.map(self.memory); return map.get(cell_offset); } /// Clear the hyperlink from the given cell. - pub fn clearHyperlink(self: *Page, row: *Row, cell: *Cell) void { + pub inline fn clearHyperlink(self: *Page, row: *Row, cell: *Cell) void { defer self.assertIntegrity(); // Get our ID @@ -1258,7 +1258,7 @@ pub const Page = struct { /// Caller is responsible for updating the refcount in the hyperlink /// set as necessary by calling `use` if the id was not acquired with /// `add`. - pub fn setHyperlink(self: *Page, row: *Row, cell: *Cell, id: hyperlink.Id) error{HyperlinkMapOutOfMemory}!void { + pub inline fn setHyperlink(self: *Page, row: *Row, cell: *Cell, id: hyperlink.Id) error{HyperlinkMapOutOfMemory}!void { defer self.assertIntegrity(); const cell_offset = getOffset(Cell, self.memory, cell); @@ -1300,7 +1300,7 @@ pub const Page = struct { /// Move the hyperlink from one cell to another. This can't fail /// because we avoid any allocations since we're just moving data. /// Destination must NOT have a hyperlink. - fn moveHyperlink(self: *Page, src: *Cell, dst: *Cell) void { + inline fn moveHyperlink(self: *Page, src: *Cell, dst: *Cell) void { assert(src.hyperlink); assert(!dst.hyperlink); @@ -1320,19 +1320,19 @@ pub const Page = struct { /// Returns the number of hyperlinks in the page. This isn't the byte /// size but the total number of unique cells that have hyperlink data. - pub fn hyperlinkCount(self: *const Page) usize { + pub inline fn hyperlinkCount(self: *const Page) usize { return self.hyperlink_map.map(self.memory).count(); } /// Returns the hyperlink capacity for the page. This isn't the byte /// size but the number of unique cells that can have hyperlink data. - pub fn hyperlinkCapacity(self: *const Page) usize { + pub inline fn hyperlinkCapacity(self: *const Page) usize { return self.hyperlink_map.map(self.memory).capacity(); } /// Set the graphemes for the given cell. This asserts that the cell /// has no graphemes set, and only contains a single codepoint. - pub fn setGraphemes( + pub inline fn setGraphemes( self: *Page, row: *Row, cell: *Cell, @@ -1433,7 +1433,7 @@ pub const Page = struct { /// Returns the codepoints for the given cell. These are the codepoints /// in addition to the first codepoint. The first codepoint is NOT /// included since it is on the cell itself. - pub fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 { + pub inline fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 { const cell_offset = getOffset(Cell, self.memory, cell); const map = self.grapheme_map.map(self.memory); const slice = map.get(cell_offset) orelse return null; @@ -1446,7 +1446,7 @@ pub const Page = struct { /// WARNING: This will NOT change the content_tag on the cells because /// there are scenarios where we want to move graphemes without changing /// the content tag. Callers beware but assertIntegrity should catch this. - fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void { + inline fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void { if (build_options.slow_runtime_safety) { assert(src.hasGrapheme()); assert(!dst.hasGrapheme()); @@ -1462,7 +1462,7 @@ pub const Page = struct { } /// Clear the graphemes for a given cell. - pub fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void { + pub inline fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void { defer self.assertIntegrity(); if (build_options.slow_runtime_safety) assert(cell.hasGrapheme()); @@ -1488,13 +1488,13 @@ pub const Page = struct { /// Returns the number of graphemes in the page. This isn't the byte /// size but the total number of unique cells that have grapheme data. - pub fn graphemeCount(self: *const Page) usize { + pub inline fn graphemeCount(self: *const Page) usize { return self.grapheme_map.map(self.memory).count(); } /// Returns the grapheme capacity for the page. This isn't the byte /// size but the number of unique cells that can have grapheme data. - pub fn graphemeCapacity(self: *const Page) usize { + pub inline fn graphemeCapacity(self: *const Page) usize { return self.grapheme_map.map(self.memory).capacity(); } @@ -1676,7 +1676,7 @@ pub const Page = struct { /// The returned value is a DynamicBitSetUnmanaged but it is NOT /// actually dynamic; do NOT call resize on this. It is safe to /// read and write but do not resize it. - pub fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged { + pub inline fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged { return .{ .bit_length = self.capacity.rows, .masks = self.dirty.ptr(self.memory), @@ -1686,14 +1686,14 @@ pub const Page = struct { /// Returns true if the given row is dirty. This is NOT very /// efficient if you're checking many rows and you should use /// dirtyBitSet directly instead. - pub fn isRowDirty(self: *const Page, y: usize) bool { + pub inline fn isRowDirty(self: *const Page, y: usize) bool { return self.dirtyBitSet().isSet(y); } /// Returns true if this page is dirty at all. If you plan on /// checking any additional rows, you should use dirtyBitSet and /// check this on your own so you have the set available. - pub fn isDirty(self: *const Page) bool { + pub inline fn isDirty(self: *const Page) bool { return self.dirtyBitSet().findFirstSet() != null; } @@ -1722,7 +1722,7 @@ pub const Page = struct { /// The memory layout for a page given a desired minimum cols /// and rows size. - pub fn layout(cap: Capacity) Layout { + pub inline fn layout(cap: Capacity) Layout { const rows_count: usize = @intCast(cap.rows); const rows_start = 0; const rows_end: usize = rows_start + (rows_count * @sizeOf(Row)); diff --git a/src/terminal/point.zig b/src/terminal/point.zig index f2544f90c..e7e2a8840 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -56,7 +56,7 @@ pub const Point = union(Tag) { screen: Coordinate, history: Coordinate, - pub fn coord(self: Point) Coordinate { + pub inline fn coord(self: Point) Coordinate { return switch (self) { .active, .viewport, diff --git a/src/terminal/size.zig b/src/terminal/size.zig index 6cedfdf6d..8322ddb41 100644 --- a/src/terminal/size.zig +++ b/src/terminal/size.zig @@ -31,7 +31,7 @@ pub fn Offset(comptime T: type) type { }; /// Returns a pointer to the start of the data, properly typed. - pub fn ptr(self: Self, base: anytype) [*]T { + pub inline fn ptr(self: Self, base: anytype) [*]T { // The offset must be properly aligned for the type since // our return type is naturally aligned. We COULD modify this // to return arbitrary alignment, but its not something we need. diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a58e01576..539807b44 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -64,7 +64,7 @@ pub fn Stream(comptime Handler: type) type { } /// Process a string of characters. - pub fn nextSlice(self: *Self, input: []const u8) !void { + pub inline fn nextSlice(self: *Self, input: []const u8) !void { // Disable SIMD optimizations if build requests it or if our // manual debug mode is on. if (comptime debug or !build_options.simd) { @@ -87,7 +87,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn nextSliceCapped(self: *Self, input: []const u8, cp_buf: []u32) !void { + inline fn nextSliceCapped(self: *Self, input: []const u8, cp_buf: []u32) !void { assert(input.len <= cp_buf.len); var offset: usize = 0; @@ -144,7 +144,7 @@ pub fn Stream(comptime Handler: type) type { /// /// Expects input to start with 0x1B, use consumeUntilGround first /// if the stream may be in the middle of an escape sequence. - fn consumeAllEscapes(self: *Self, input: []const u8) !usize { + inline fn consumeAllEscapes(self: *Self, input: []const u8) !usize { var offset: usize = 0; while (input[offset] == 0x1B) { self.parser.state = .escape; @@ -158,7 +158,7 @@ pub fn Stream(comptime Handler: type) type { /// Parses escape sequences until the parser reaches the ground state. /// Returns the number of bytes consumed from the provided input. - fn consumeUntilGround(self: *Self, input: []const u8) !usize { + inline fn consumeUntilGround(self: *Self, input: []const u8) !usize { var offset: usize = 0; while (self.parser.state != .ground) { if (offset >= input.len) return input.len; @@ -171,7 +171,7 @@ pub fn Stream(comptime Handler: type) type { /// Like nextSlice but takes one byte and is necessarily a scalar /// operation that can't use SIMD. Prefer nextSlice if you can and /// try to get multiple bytes at once. - pub fn next(self: *Self, c: u8) !void { + pub inline fn next(self: *Self, c: u8) !void { // The scalar path can be responsible for decoding UTF-8. if (self.parser.state == .ground) { try self.nextUtf8(c); @@ -185,7 +185,7 @@ pub fn Stream(comptime Handler: type) type { /// /// This assumes we're in the UTF-8 decoding state. If we may not /// be in the UTF-8 decoding state call nextSlice or next. - fn nextUtf8(self: *Self, c: u8) !void { + inline fn nextUtf8(self: *Self, c: u8) !void { assert(self.parser.state == .ground); const res = self.utf8decoder.next(c); @@ -326,13 +326,13 @@ pub fn Stream(comptime Handler: type) type { } } - pub fn print(self: *Self, c: u21) !void { + pub inline fn print(self: *Self, c: u21) !void { if (@hasDecl(T, "print")) { try self.handler.print(c); } } - pub fn execute(self: *Self, c: u8) !void { + pub inline fn execute(self: *Self, c: u8) !void { const c0: ansi.C0 = @enumFromInt(c); if (comptime debug) log.info("execute: {}", .{c0}); switch (c0) { @@ -383,7 +383,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void { + inline fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void { switch (input.final) { // CUU - Cursor Up 'A', 'k' => switch (input.intermediates.len) { @@ -1490,7 +1490,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn oscDispatch(self: *Self, cmd: osc.Command) !void { + inline fn oscDispatch(self: *Self, cmd: osc.Command) !void { switch (cmd) { .change_window_title => |title| { if (@hasDecl(T, "changeWindowTitle")) { @@ -1635,7 +1635,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn configureCharset( + inline fn configureCharset( self: *Self, intermediates: []const u8, set: charsets.Charset, @@ -1669,7 +1669,7 @@ pub fn Stream(comptime Handler: type) type { }); } - fn escDispatch( + inline fn escDispatch( self: *Self, action: Parser.Action.ESC, ) !void { From c57c205672f5338a4c76c5d0d7bfd3120b54b79c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 29 Sep 2025 11:17:29 -0600 Subject: [PATCH 107/319] fix test failures Very weird failures, not 100% sure of the cause; regardless, this fixes them. --- src/terminal/Parser.zig | 2 +- src/terminal/stream.zig | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 61ac4e312..05cbe7957 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -254,7 +254,7 @@ pub fn deinit(self: *Parser) void { /// Next consumes the next character c and returns the actions to execute. /// Up to 3 actions may need to be executed -- in order -- representing /// the state exit, transition, and entry actions. -pub inline fn next(self: *Parser, c: u8) [3]?Action { +pub fn next(self: *Parser, c: u8) [3]?Action { const effect = table[c][@intFromEnum(self.state)]; // log.info("next: {x}", .{c}); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 539807b44..db43aae47 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -278,7 +278,14 @@ pub fn Stream(comptime Handler: type) type { return; } - const actions = self.parser.next(c); + // We explicitly inline this call here for performance reasons. + // + // We do this rather than mark Parser.next as inline because doing + // that causes weird behavior in some tests- I'm not sure if they + // miscompile or it's just very counter-intuitive comptime stuff, + // but regardless, this is the easy solution. + const actions = @call(.always_inline, Parser.next, .{ &self.parser, c }); + for (actions) |action_opt| { const action = action_opt orelse continue; if (comptime debug) log.info("action: {}", .{action}); From 86fb03677ae0904422a05848e7a9cb76dc09586e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 30 Sep 2025 11:07:25 -0500 Subject: [PATCH 108/319] Revert "renderer: slightly optimize screen copy" This reverts commit fcea09e413a55c677dca377f716aa9bc6465306b. --- src/renderer/generic.zig | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index b5d3b5661..802c769a6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -95,9 +95,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Allocator that can be used alloc: std.mem.Allocator, - /// MemoryPool for PageList pages which we use when cloning the screen. - page_pool: terminal.PageList.MemoryPool, - /// This mutex must be held whenever any state used in `drawFrame` is /// being modified, and also when it's being accessed in `drawFrame`. draw_mutex: std.Thread.Mutex = .{}, @@ -679,19 +676,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; errdefer if (display_link) |v| v.release(); - // We preheat the page pool with 4 pages- this is an arbitrary - // choice based on what seems reasonable for the number of pages - // used by the viewport area. - var page_pool: terminal.PageList.MemoryPool = try .init( - alloc, - std.heap.page_allocator, - 4, - ); - errdefer page_pool.deinit(); - var result: Self = .{ .alloc = alloc, - .page_pool = page_pool, .config = options.config, .surface_mailbox = options.surface_mailbox, .grid_metrics = font_critical.metrics, @@ -774,8 +760,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } pub fn deinit(self: *Self) void { - self.page_pool.deinit(); - self.swap_chain.deinit(); if (DisplayLink != void) { @@ -1108,13 +1092,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { full_rebuild: bool, }; - // Empty our page pool, but retain capacity. - self.page_pool.reset(.retain_capacity); - - var arena: std.heap.ArenaAllocator = .init(self.alloc); - defer arena.deinit(); - const alloc = arena.allocator(); - // Update all our data as tightly as possible within the mutex. var critical: Critical = critical: { // const start = try std.time.Instant.now(); @@ -1171,12 +1148,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // We used to share terminal state, but we've since learned through // analysis that it is faster to copy the terminal state than to // hold the lock while rebuilding GPU cells. - const screen_copy = try state.terminal.screen.clonePool( - alloc, - &self.page_pool, + var screen_copy = try state.terminal.screen.clone( + self.alloc, .{ .viewport = .{} }, null, ); + errdefer screen_copy.deinit(); // Whether to draw our cursor or not. const cursor_style = if (state.terminal.flags.password_input) @@ -1192,8 +1169,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const preedit: ?renderer.State.Preedit = preedit: { if (cursor_style == null) break :preedit null; const p = state.preedit orelse break :preedit null; - break :preedit try p.clone(alloc); + break :preedit try p.clone(self.alloc); }; + errdefer if (preedit) |p| p.deinit(self.alloc); // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if @@ -1263,6 +1241,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .full_rebuild = full_rebuild, }; }; + defer { + critical.screen.deinit(); + if (critical.preedit) |p| p.deinit(self.alloc); + } // Build our GPU cells try self.rebuildCells( From 4cc663fc6003aa6626d4a9c73f1763b862dc569f Mon Sep 17 00:00:00 2001 From: Matthew Hrehirchuk Date: Tue, 30 Sep 2025 11:23:02 -0600 Subject: [PATCH 109/319] feat: add GHOSTTY_BIN_DIR to path via shell integration --- src/config/Config.zig | 6 ++++++ src/shell-integration/bash/ghostty.bash | 8 ++++++++ .../elvish/lib/ghostty-integration.elv | 13 +++++++++++++ .../vendor_conf.d/ghostty-shell-integration.fish | 8 ++++++++ src/shell-integration/zsh/ghostty-integration | 8 ++++++++ src/termio/shell_integration.zig | 8 ++++---- 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 46eb03fe2..b7a9699c8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2359,6 +2359,11 @@ keybind: Keybinds = .{}, /// cache manually using various arguments. /// (Available since: 1.2.0) /// +/// * `path` - Add Ghostty's binary directory to PATH. This ensures the `ghostty` +/// command is available in the shell even if shell init scripts reset PATH. +/// This is particularly useful on macOS where PATH is often overridden by +/// system scripts. The directory is only added if not already present. +/// /// SSH features work independently and can be combined for optimal experience: /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its /// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to @@ -6994,6 +6999,7 @@ pub const ShellIntegrationFeatures = packed struct { title: bool = true, @"ssh-env": bool = false, @"ssh-terminfo": bool = false, + path: bool = true, }; pub const RepeatableCommand = struct { diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 2cf9d388f..cdaddea5c 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -73,6 +73,14 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then builtin unset GHOSTTY_BASH_RCFILE fi +# Add Ghostty binary to PATH if the path feature is enabled +if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* && -n "$GHOSTTY_BIN_DIR" ]]; then + # Check if the directory is already in PATH + if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then + export PATH="$PATH:$GHOSTTY_BIN_DIR" + fi +fi + # Sudo if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved. diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 6d0d19f4f..e570e3aec 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -196,6 +196,19 @@ set edit:before-readline = (conj $edit:before-readline $beam~) set edit:after-readline = (conj $edit:after-readline {|_| block }) } + if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) { + # Check if the directory is already in PATH + var path-contains-ghostty = $false + for p $paths { + if (eq $p $E:GHOSTTY_BIN_DIR) { + set path-contains-ghostty = $true + break + } + } + if (not $path-contains-ghostty) { + set paths = [$@paths $E:GHOSTTY_BIN_DIR] + } + } if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { edit:add-var sudo~ $sudo-with-terminfo~ } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index daa4f1d4f..fcba767a2 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -61,6 +61,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end + # Add Ghostty binary to PATH if the path feature is enabled + if contains path $features; and test -n "$GHOSTTY_BIN_DIR" + # Check if the directory is already in PATH + if not contains -- "$GHOSTTY_BIN_DIR" $PATH + set --global --export PATH $PATH "$GHOSTTY_BIN_DIR" + end + end + # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 8607664a2..e76aace4a 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -220,6 +220,14 @@ _ghostty_deferred_init() { builtin print -rnu $_ghostty_fd \$'\\e[0 q'" fi + # Add Ghostty binary to PATH if the path feature is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then + # Check if the directory is already in PATH + if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then + builtin export PATH="$PATH:$GHOSTTY_BIN_DIR" + fi + fi + # Sudo if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 30519b6e2..90a697409 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -219,8 +219,8 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true }); - try testing.expectEqualStrings("cursor,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); + try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true, .path = true }); + try testing.expectEqualStrings("cursor,path,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); } // Test: all features disabled @@ -228,7 +228,7 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false }); + try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false, .path = false }); try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } @@ -237,7 +237,7 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false }); + try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false, .path = false }); try testing.expectEqualStrings("ssh-env,sudo", env.get("GHOSTTY_SHELL_FEATURES").?); } } From 16deea2761b305293b351c7fbe85d3a6a0f8ad57 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Sep 2025 12:13:43 -0700 Subject: [PATCH 110/319] nuke ziglyph from orbit Since we now use uucode, we don't need ziglyph anymore. Ziglyph was kept around as a test-only dep so we can verify matching but this is complicating our Zig 0.15 upgrade because ziglyph doesn't support Zig 0.15. Let's just drop it. --- build.zig.zon | 5 --- build.zig.zon.json | 5 --- build.zig.zon.nix | 8 ----- build.zig.zon.txt | 1 - flatpak/zig-packages.json | 6 ---- src/build/SharedDeps.zig | 8 ----- src/unicode/props_uucode.zig | 57 ---------------------------------- src/unicode/symbols_uucode.zig | 26 ---------------- 8 files changed, 116 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 0e5fdfb1f..992284bf7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -37,11 +37,6 @@ .hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", .lazy = true, }, - .ziglyph = .{ - .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - .hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", - .lazy = true, - }, .uucode = .{ .url = "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", .hash = "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE", diff --git a/build.zig.zon.json b/build.zig.zon.json index 6e8ea3acc..83625f765 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -169,11 +169,6 @@ "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d", "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=" }, - "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": { - "name": "ziglyph", - "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - "hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=" - }, "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": { "name": "zlib", "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 4e0d2bede..abd5a37c5 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -354,14 +354,6 @@ in hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; }; } - { - name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; - path = fetchZigArtifact { - name = "ziglyph"; - url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; - hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; - }; - } { name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; path = fetchZigArtifact { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 4d7e350a0..453a12347 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -25,7 +25,6 @@ https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d. https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz -https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index a6eaf846c..beea0dc04 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -203,12 +203,6 @@ "commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d", "dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj" }, - { - "type": "archive", - "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - "dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", - "sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9" - }, { "type": "archive", "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 7c0619b5e..9461d48b7 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -403,14 +403,6 @@ pub fn add( })) |dep| { step.root_module.addImport("z2d", dep.module("z2d")); } - if (step.kind == .@"test") { - if (b.lazyDependency("ziglyph", .{ - .target = step.root_module.resolved_target.?, - .optimize = step.root_module.optimize.?, - })) |dep| { - step.root_module.addImport("ziglyph", dep.module("ziglyph")); - } - } if (b.lazyDependency("uucode", .{ .target = target, .optimize = optimize, diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index 71edac4fb..ba0511ea4 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -114,60 +114,3 @@ test "unicode props: tables match uucode" { } } } - -test "unicode props: tables match ziglyph" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const testing = std.testing; - const table = @import("props_table.zig").table; - const ziglyph = @import("ziglyph"); - - const min = 0xFF + 1; // start outside ascii - const max = std.math.maxInt(u21) + 1; - for (min..max) |cp| { - const t = table.get(@intCast(cp)); - const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half))); - if (t.width != zg) { - - // Known exceptions - if (cp == 0x0897) continue; // non-spacing mark (t = 0) - if (cp == 0x2065) continue; // unassigned (t = 1) - if (cp >= 0x2630 and cp <= 0x2637) continue; // east asian width is wide (t = 2) - if (cp >= 0x268A and cp <= 0x268F) continue; // east asian width is wide (t = 2) - if (cp >= 0x2FFC and cp <= 0x2FFF) continue; // east asian width is wide (t = 2) - if (cp == 0x31E4 or cp == 0x31E5) continue; // east asian width is wide (t = 2) - if (cp == 0x31EF) continue; // east asian width is wide (t = 2) - if (cp >= 0x4DC0 and cp <= 0x4DFF) continue; // east asian width is wide (t = 2) - if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) - if (cp >= 0xFFF0 and cp <= 0xFFF8) continue; // unassigned (t = 1) - if (cp >= 0x10D69 and cp <= 0x10D6D) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0x10EFC and cp <= 0x10EFF) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0x113BB and cp <= 0x113C0) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113CE) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113D0) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113D2) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113E1) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x113E2) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1171E) continue; // mark spacing combining (t = 1) - if (cp == 0x11F5A) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1611E) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1611F) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0x16120 and cp <= 0x1612F) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp >= 0xE0000 and cp <= 0xE0FFF) continue; // ziglyph ignores these with 0, but many are unassigned (t = 1) - if (cp == 0x18CFF) continue; // east asian width is wide (t = 2) - if (cp >= 0x1D300 and cp <= 0x1D376) continue; // east asian width is wide (t = 2) - if (cp == 0x1E5EE) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1E5EF) continue; // non-spacing mark, despite being east asian width normal (t = 0) - if (cp == 0x1FA89) continue; // east asian width is wide (t = 2) - if (cp == 0x1FA8F) continue; // east asian width is wide (t = 2) - if (cp == 0x1FABE) continue; // east asian width is wide (t = 2) - if (cp == 0x1FAC6) continue; // east asian width is wide (t = 2) - if (cp == 0x1FADC) continue; // east asian width is wide (t = 2) - if (cp == 0x1FADF) continue; // east asian width is wide (t = 2) - if (cp == 0x1FAE9) continue; // east asian width is wide (t = 2) - - std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t.width, zg }); - try testing.expect(false); - } - } -} diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig index 985ed1380..3da019e81 100644 --- a/src/unicode/symbols_uucode.zig +++ b/src/unicode/symbols_uucode.zig @@ -59,29 +59,3 @@ test "unicode symbols: tables match uucode" { } } } - -test "unicode symbols: tables match ziglyph" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const testing = std.testing; - const table = @import("symbols_table.zig").table; - const ziglyph = @import("ziglyph"); - - for (0..std.math.maxInt(u21)) |cp_usize| { - const cp: u21 = @intCast(cp_usize); - const t = table.get(cp); - const zg = ziglyph.general_category.isPrivateUse(cp) or - ziglyph.blocks.isDingbats(cp) or - ziglyph.blocks.isEmoticons(cp) or - ziglyph.blocks.isMiscellaneousSymbols(cp) or - ziglyph.blocks.isEnclosedAlphanumerics(cp) or - ziglyph.blocks.isEnclosedAlphanumericSupplement(cp) or - ziglyph.blocks.isMiscellaneousSymbolsAndPictographs(cp) or - ziglyph.blocks.isTransportAndMapSymbols(cp); - - if (t != zg) { - std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); - try testing.expect(false); - } - } -} From 26b70e3125d04f48ce76a49b6b22b08d06619404 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 30 Sep 2025 12:28:33 -0700 Subject: [PATCH 111/319] Implement and use generic approx equality tester --- src/datastruct/comparison.zig | 145 ++++++++++++++++++++++++++++++++++ src/font/Collection.zig | 33 ++------ 2 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 src/datastruct/comparison.zig diff --git a/src/datastruct/comparison.zig b/src/datastruct/comparison.zig new file mode 100644 index 000000000..61d540353 --- /dev/null +++ b/src/datastruct/comparison.zig @@ -0,0 +1,145 @@ +const std = @import("std"); + +/// Generic, recursive equality testing utility using approximate comparison for +/// floats and equality for everything else +/// +/// Based on the source code of `std.testing.expectEqual`, +/// `std.testing.expectEqualSlices`, and `std.meta.eql`, as of Zig 0.15.1. +/// +/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. +pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { + const T = @TypeOf(expected, actual); + return expectApproxEqualInner(T, expected, actual); +} + +fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { + switch (@typeInfo(T)) { + // check approximate equality for floats + .float => { + const sqrt_eps = comptime std.math.sqrt(std.math.floatEps(T)); + if (!std.math.approxEqRel(T, expected, actual, sqrt_eps)) { + print("expected approximately {any}, found {any}\n", .{ expected, actual }); + return error.TestExpectedApproxEqual; + } + }, + + // recurse into containers + .array => { + const diff_index: usize = diff_index: { + const shortest = @min(expected.len, actual.len); + var index: usize = 0; + while (index < shortest) : (index += 1) { + expectApproxEqual(actual[index], expected[index]) catch break :diff_index index; + } + break :diff_index if (expected.len == actual.len) return else shortest; + }; + print("slices not approximately equal. first significant difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index }); + return error.TestExpectedApproxEqual; + }, + .vector => |info| { + var i: usize = 0; + while (i < info.len) : (i += 1) { + expectApproxEqual(expected[i], actual[i]) catch { + print("index {d} incorrect. expected approximately {any}, found {any}\n", .{ + i, expected[i], actual[i], + }); + return error.TestExpectedApproxEqual; + }; + } + }, + .@"struct" => |structType| { + inline for (structType.fields) |field| { + try expectApproxEqual(@field(expected, field.name), @field(actual, field.name)); + } + }, + + // unwrap unions, optionals, and error unions + .@"union" => |union_info| { + if (union_info.tag_type == null) { + // untagged unions can only be compared bitwise, + // so expectEqual is all we need + std.testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }; + } + + const Tag = std.meta.Tag(@TypeOf(expected)); + + const expectedTag = @as(Tag, expected); + const actualTag = @as(Tag, actual); + + std.testing.expectEqual(expectedTag, actualTag) catch { + return error.TestExpectedApproxEqual; + }; + + // we only reach this switch if the tags are equal + switch (expected) { + inline else => |val, tag| try expectApproxEqual(val, @field(actual, @tagName(tag))), + } + }, + .optional, .error_union => { + if (expected) |expected_payload| if (actual) |actual_payload| { + return expectApproxEqual(expected_payload, actual_payload); + }; + // we only reach this point if there's at least one null or error, + // in which case expectEqual is all we need + std.testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }; + }, + + // fall back to expectEqual for everything else + else => std.testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }, + } +} + +/// Copy of std.testing.print (not public) as of Zig 0.15.1 +fn print(comptime fmt: []const u8, args: anytype) void { + if (@inComptime()) { + @compileError(std.fmt.comptimePrint(fmt, args)); + } else if (std.testing.backend_can_print) { + std.debug.print(fmt, args); + } +} + +test "expectApproxEqual.union(enum)" { + const T = union(enum) { + a: i32, + b: f32, + }; + + const b10 = T{ .b = 10.0 }; + const b10plus = T{ .b = 10.000001 }; + + try expectApproxEqual(b10, b10plus); +} + +test "expectApproxEqual nested array" { + const a = [2][2]f32{ + [_]f32{ 1.0, 0.0 }, + [_]f32{ 0.0, 1.0 }, + }; + + const b = [2][2]f32{ + [_]f32{ 1.000001, 0.0 }, + [_]f32{ 0.0, 0.999999 }, + }; + + try expectApproxEqual(a, b); +} + +test "expectApproxEqual vector" { + const a: @Vector(4, f32) = @splat(4.0); + const b: @Vector(4, f32) = @splat(4.000001); + + try expectApproxEqual(a, b); +} + +test "expectApproxEqual struct" { + const a = .{ 1, @as(f32, 1.0) }; + const b = .{ 1, @as(f32, 0.999999) }; + + try expectApproxEqual(a, b); +} diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 04b9882dc..e91fe03ae 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -19,6 +19,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const config = @import("../config.zig"); +const comparison = @import("../datastruct/comparison.zig"); const font = @import("main.zig"); const options = font.options; const DeferredFace = font.DeferredFace; @@ -1199,7 +1200,7 @@ test "metrics" { try c.updateMetrics(); - try std.testing.expectEqual(font.Metrics{ + try comparison.expectApproxEqual(font.Metrics{ .cell_width = 8, // The cell height is 17 px because the calculation is // @@ -1229,12 +1230,12 @@ test "metrics" { .icon_height = 12.24, .face_width = 8.0, .face_height = 16.784, - .face_y = @round(3.04) - @as(f64, 3.04), // use f64, not comptime float, for exact match with runtime value + .face_y = -0.04, }, c.metrics); // Resize should change metrics try c.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); - try std.testing.expectEqual(font.Metrics{ + try comparison.expectApproxEqual(font.Metrics{ .cell_width = 16, .cell_height = 34, .cell_baseline = 6, @@ -1249,7 +1250,7 @@ test "metrics" { .icon_height = 24.48, .face_width = 16.0, .face_height = 33.568, - .face_y = @round(6.08) - @as(f64, 6.08), // use f64, not comptime float, for exact match with runtime value + .face_y = -0.08, }, c.metrics); } @@ -1493,29 +1494,7 @@ test "face metrics" { .{ narrowMetricsExpected, wideMetricsExpected }, .{ narrowMetrics, wideMetrics }, ) |metricsExpected, metricsActual| { - inline for (@typeInfo(font.Metrics.FaceMetrics).@"struct".fields) |field| { - const expected = @field(metricsExpected, field.name); - const actual = @field(metricsActual, field.name); - // Unwrap optional fields - const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) { - .optional => { - if (expected) |expectedValue| if (actual) |actualValue| { - break :unwrap .{ expectedValue, actualValue }; - }; - // Null values can be compared directly - try std.testing.expectEqual(expected, actual); - continue; - }, - else => break :unwrap .{ expected, actual }, - }; - // All non-null values are floats - const eps = std.math.floatEps(@TypeOf(actualValue - expectedValue)); - try std.testing.expectApproxEqRel( - expectedValue, - actualValue, - std.math.sqrt(eps), - ); - } + try comparison.expectApproxEqual(metricsExpected, metricsActual); } // Verify estimated metrics. icWidth() should equal the smaller of From 3feff75c9971a40e8f9e4a5938ff7aa7308e8f8a Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 30 Sep 2025 13:57:57 -0700 Subject: [PATCH 112/319] Add proper Zig stdlib attribution --- src/datastruct/comparison.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/datastruct/comparison.zig b/src/datastruct/comparison.zig index 61d540353..4427c143c 100644 --- a/src/datastruct/comparison.zig +++ b/src/datastruct/comparison.zig @@ -1,10 +1,11 @@ +// The contents of this file is largely based on testing.zig from the Zig 0.15.1 +// stdlib, distributed under the MIT license, copyright (c) Zig contributors const std = @import("std"); /// Generic, recursive equality testing utility using approximate comparison for /// floats and equality for everything else /// -/// Based on the source code of `std.testing.expectEqual`, -/// `std.testing.expectEqualSlices`, and `std.meta.eql`, as of Zig 0.15.1. +/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`. /// /// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { @@ -95,7 +96,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { } } -/// Copy of std.testing.print (not public) as of Zig 0.15.1 +/// Copy of std.testing.print (not public) fn print(comptime fmt: []const u8, args: anytype) void { if (@inComptime()) { @compileError(std.fmt.comptimePrint(fmt, args)); @@ -104,6 +105,7 @@ fn print(comptime fmt: []const u8, args: anytype) void { } } +// Tests based on the `expectEqual` tests in the Zig stdlib test "expectApproxEqual.union(enum)" { const T = union(enum) { a: i32, From 9407e0fd0d7bc7243b5eaf04658a591cd77e41a3 Mon Sep 17 00:00:00 2001 From: Matthew Hrehirchuk Date: Tue, 30 Sep 2025 22:26:07 -0600 Subject: [PATCH 113/319] fix: cleaned up elvish and fish integrations for bin_dir --- .../elvish/lib/ghostty-integration.elv | 11 ++--------- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 5 +---- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index e570e3aec..04fe8f86e 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -198,15 +198,8 @@ } if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) { # Check if the directory is already in PATH - var path-contains-ghostty = $false - for p $paths { - if (eq $p $E:GHOSTTY_BIN_DIR) { - set path-contains-ghostty = $true - break - } - } - if (not $path-contains-ghostty) { - set paths = [$@paths $E:GHOSTTY_BIN_DIR] + if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) { + set paths = [$E:GHOSTTY_BIN_DIR $@paths] } } if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index fcba767a2..b9add8cfd 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -63,10 +63,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Add Ghostty binary to PATH if the path feature is enabled if contains path $features; and test -n "$GHOSTTY_BIN_DIR" - # Check if the directory is already in PATH - if not contains -- "$GHOSTTY_BIN_DIR" $PATH - set --global --export PATH $PATH "$GHOSTTY_BIN_DIR" - end + fish_add_path "$GHOSTTY_BIN_DIR" end # When using sudo shell integration feature, ensure $TERMINFO is set From 09e4c1e6f2f86de1b92fd1dd4d057f293f58fa34 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Oct 2025 07:00:41 -0700 Subject: [PATCH 114/319] build: isolate XCFramework.Target so runtime code doesn't depend on it This fixes the lazyImport importing outside of the build root. The build root for the build binary is always the root `build.zig` (which we want), but our `src/build_config.zig` transitively imported SharedDeps which led to issues. --- src/build/Config.zig | 6 +++--- src/build/GhosttyXCFramework.zig | 3 +-- src/build/xcframework.zig | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 src/build/xcframework.zig diff --git a/src/build/Config.zig b/src/build/Config.zig index 0b7dae14d..e0e81e519 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -9,7 +9,7 @@ const ApprtRuntime = @import("../apprt/runtime.zig").Runtime; const FontBackend = @import("../font/backend.zig").Backend; const RendererBackend = @import("../renderer/backend.zig").Backend; const TerminalBuildOptions = @import("../terminal/build_options.zig").Options; -const XCFramework = @import("GhosttyXCFramework.zig"); +const XCFrameworkTarget = @import("xcframework.zig").Target; const WasmTarget = @import("../os/wasm/target.zig").Target; const expandPath = @import("../os/path.zig").expand; @@ -26,7 +26,7 @@ const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 1 } /// Standard build configuration options. optimize: std.builtin.OptimizeMode, target: std.Build.ResolvedTarget, -xcframework_target: XCFramework.Target = .universal, +xcframework_target: XCFrameworkTarget = .universal, wasm_target: WasmTarget, /// Comptime interfaces @@ -121,7 +121,7 @@ pub fn init(b: *std.Build) !Config { //--------------------------------------------------------------- // Target-specific properties config.xcframework_target = b.option( - XCFramework.Target, + XCFrameworkTarget, "xcframework-target", "The target for the xcframework.", ) orelse .universal; diff --git a/src/build/GhosttyXCFramework.zig b/src/build/GhosttyXCFramework.zig index d036e7020..3afeb9073 100644 --- a/src/build/GhosttyXCFramework.zig +++ b/src/build/GhosttyXCFramework.zig @@ -5,12 +5,11 @@ const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); const GhosttyLib = @import("GhosttyLib.zig"); const XCFrameworkStep = @import("XCFrameworkStep.zig"); +const Target = @import("xcframework.zig").Target; xcframework: *XCFrameworkStep, target: Target, -pub const Target = enum { native, universal }; - pub fn init( b: *std.Build, deps: *const SharedDeps, diff --git a/src/build/xcframework.zig b/src/build/xcframework.zig new file mode 100644 index 000000000..8713a1c9a --- /dev/null +++ b/src/build/xcframework.zig @@ -0,0 +1,3 @@ +/// Target for xcframework builds. This is a separate file so that +/// our runtime code doesn't need to import build code. +pub const Target = enum { native, universal }; From 4989f92c719005b54cb17dfc20f76bbfbd946f5b Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 1 Oct 2025 10:27:42 -0400 Subject: [PATCH 115/319] shell-integration: remove redundant comments I think the conditions are sufficiently self-descriptive. --- src/shell-integration/bash/ghostty.bash | 1 - src/shell-integration/elvish/lib/ghostty-integration.elv | 1 - src/shell-integration/zsh/ghostty-integration | 1 - 3 files changed, 3 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index cdaddea5c..e910a9885 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -75,7 +75,6 @@ fi # Add Ghostty binary to PATH if the path feature is enabled if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* && -n "$GHOSTTY_BIN_DIR" ]]; then - # Check if the directory is already in PATH if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then export PATH="$PATH:$GHOSTTY_BIN_DIR" fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 04fe8f86e..11f4eae5b 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -197,7 +197,6 @@ set edit:after-readline = (conj $edit:after-readline {|_| block }) } if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) { - # Check if the directory is already in PATH if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) { set paths = [$E:GHOSTTY_BIN_DIR $@paths] } diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index e76aace4a..27ef39bbc 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -222,7 +222,6 @@ _ghostty_deferred_init() { # Add Ghostty binary to PATH if the path feature is enabled if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then - # Check if the directory is already in PATH if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then builtin export PATH="$PATH:$GHOSTTY_BIN_DIR" fi From 6f596ee7c353706d0bba4088eb43c31e4dd677b9 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 1 Oct 2025 10:42:33 -0400 Subject: [PATCH 116/319] shell-integration: append $GHOSTTY_BIN_DIR to $PATH For consistency with the termio/Exec.zig implementation, we always append to the PATH (lowest priority). --- src/shell-integration/elvish/lib/ghostty-integration.elv | 2 +- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 11f4eae5b..33473c8b0 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -198,7 +198,7 @@ } if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) { if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) { - set paths = [$E:GHOSTTY_BIN_DIR $@paths] + set paths = [$@paths $E:GHOSTTY_BIN_DIR] } } if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index b9add8cfd..7042f892a 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -63,7 +63,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Add Ghostty binary to PATH if the path feature is enabled if contains path $features; and test -n "$GHOSTTY_BIN_DIR" - fish_add_path "$GHOSTTY_BIN_DIR" + fish_add_path --append "$GHOSTTY_BIN_DIR" end # When using sudo shell integration feature, ensure $TERMINFO is set From 1ec74f8e396968a6487e6faae92b6340b12cc074 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Oct 2025 13:21:16 -0700 Subject: [PATCH 117/319] Convert framegen to C, add compressed data to source tarball Zig 0.15 removed the ability to compress from the stdlib, which makes porting our framegen tool to Zig 0.15+ more work than it's worth. We already depend on and have the ability to build zlib, and Zig is a full blown C compiler, so let's just use C. The framegen C program doesn't free any memory, because it is meant to exit quickly. It otherwise behaves pretty much the same as the old Zig codebase. The build scripts were modified to build the C program and run it, but also to include the framedata in the generated source tarball so that downstream packagers don't have to do this (although they'll have all the deps anyways). --- src/build/GhosttyDist.zig | 5 + src/build/GhosttyFrameData.zig | 74 ++++++--- src/build/framegen/main.c | 145 +++++++++++++++++ src/build/framegen/main.zig | 273 --------------------------------- 4 files changed, 202 insertions(+), 295 deletions(-) create mode 100644 src/build/framegen/main.c delete mode 100644 src/build/framegen/main.zig diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index d889f2350..f8c221350 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -3,6 +3,7 @@ const GhosttyDist = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); +const GhosttyFrameData = @import("GhosttyFrameData.zig"); /// The final source tarball. archive: std.Build.LazyPath, @@ -25,6 +26,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { try resources.append(alloc, gtk.resources_c); try resources.append(alloc, gtk.resources_h); } + { + const framedata = GhosttyFrameData.distResources(b); + try resources.append(alloc, framedata.framedata); + } // git archive to create the final tarball. "git archive" is the // easiest way I can find to create a tarball that ignores stuff diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig index 1644388bc..52c84a66c 100644 --- a/src/build/GhosttyFrameData.zig +++ b/src/build/GhosttyFrameData.zig @@ -5,35 +5,25 @@ const GhosttyFrameData = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); - -/// The exe. -exe: *std.Build.Step.Compile, +const DistResource = @import("GhosttyDist.zig").Resource; /// The output path for the compressed framedata zig file output: std.Build.LazyPath, pub fn init(b: *std.Build) !GhosttyFrameData { - const exe = b.addExecutable(.{ - .name = "framegen", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/build/framegen/main.zig"), - .target = b.graph.host, - .strip = false, - .omit_frame_pointer = false, - .unwind_tables = .sync, - }), - }); + const dist = distResources(b); - const run = b.addRunArtifact(exe); - // Both the compressed framedata and the Zig source file - // have to be put in the same directory, since the compressed file - // has to be within the source file's include path. - const dir = run.addOutputDirectoryArg("framedata"); + // Generate the Zig source file that embeds the compressed data + const wf = b.addWriteFiles(); + _ = wf.addCopyFile(dist.framedata.path(b), "framedata.compressed"); + const zig_file = wf.add("framedata.zig", + \\//! This file is auto-generated. Do not edit. + \\ + \\pub const compressed = @embedFile("framedata.compressed"); + \\ + ); - return .{ - .exe = exe, - .output = dir.path(b, "framedata.zig"), - }; + return .{ .output = zig_file }; } /// Add the "framedata" import. @@ -43,3 +33,43 @@ pub fn addImport(self: *const GhosttyFrameData, step: *std.Build.Step.Compile) v .root_source_file = self.output, }); } + +/// Creates the framedata resources that can be prebuilt for our dist build. +pub fn distResources(b: *std.Build) struct { + framedata: DistResource, +} { + const exe = b.addExecutable(.{ + .name = "framegen", + .target = b.graph.host, + }); + exe.addCSourceFile(.{ + .file = b.path("src/build/framegen/main.c"), + .flags = &.{}, + }); + exe.linkLibC(); + + if (b.systemIntegrationOption("zlib", .{})) { + exe.linkSystemLibrary2("zlib", .{ + .preferred_link_mode = .dynamic, + .search_strategy = .mode_first, + }); + } else { + if (b.lazyDependency("zlib", .{ + .target = b.graph.host, + .optimize = .ReleaseFast, + })) |zlib_dep| { + exe.linkLibrary(zlib_dep.artifact("z")); + } + } + + const run = b.addRunArtifact(exe); + run.addDirectoryArg(b.path("src/build/framegen/frames")); + const compressed_file = run.addOutputFileArg("framedata.compressed"); + + return .{ + .framedata = .{ + .dist = "src/build/framegen/framedata.compressed", + .generated = compressed_file, + }, + }; +} diff --git a/src/build/framegen/main.c b/src/build/framegen/main.c new file mode 100644 index 000000000..647768006 --- /dev/null +++ b/src/build/framegen/main.c @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include +#include + +#define SEPARATOR '\x01' +#define CHUNK_SIZE 16384 + +static int filter_frames(const struct dirent *entry) { + const char *name = entry->d_name; + size_t len = strlen(name); + return len > 4 && strcmp(name + len - 4, ".txt") == 0; +} + +static int compare_frames(const struct dirent **a, const struct dirent **b) { + return strcmp((*a)->d_name, (*b)->d_name); +} + +static char *read_file(const char *path, size_t *out_size) { + FILE *f = fopen(path, "rb"); + if (!f) { + fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); + return NULL; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *buf = malloc(size); + if (!buf) { + return NULL; + } + + if (fread(buf, 1, size, f) != (size_t)size) { + fprintf(stderr, "Failed to read %s\n", path); + return NULL; + } + + fclose(f); + *out_size = size; + return buf; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char *frames_dir = argv[1]; + const char *output_file = argv[2]; + + struct dirent **namelist; + int n = scandir(frames_dir, &namelist, filter_frames, compare_frames); + if (n < 0) { + fprintf(stderr, "Failed to scan directory %s: %s\n", frames_dir, strerror(errno)); + return 1; + } + + if (n == 0) { + fprintf(stderr, "No frame files found in %s\n", frames_dir); + return 1; + } + + size_t total_size = 0; + char **frame_contents = calloc(n, sizeof(char*)); + size_t *frame_sizes = calloc(n, sizeof(size_t)); + + for (int i = 0; i < n; i++) { + char path[4096]; + snprintf(path, sizeof(path), "%s/%s", frames_dir, namelist[i]->d_name); + + frame_contents[i] = read_file(path, &frame_sizes[i]); + if (!frame_contents[i]) { + return 1; + } + + total_size += frame_sizes[i]; + if (i < n - 1) total_size++; + } + + char *joined = malloc(total_size); + if (!joined) { + fprintf(stderr, "Failed to allocate joined buffer\n"); + return 1; + } + + size_t offset = 0; + for (int i = 0; i < n; i++) { + memcpy(joined + offset, frame_contents[i], frame_sizes[i]); + offset += frame_sizes[i]; + if (i < n - 1) { + joined[offset++] = SEPARATOR; + } + } + + uLongf compressed_size = compressBound(total_size); + unsigned char *compressed = malloc(compressed_size); + if (!compressed) { + fprintf(stderr, "Failed to allocate compression buffer\n"); + return 1; + } + + z_stream stream = {0}; + stream.next_in = (unsigned char*)joined; + stream.avail_in = total_size; + stream.next_out = compressed; + stream.avail_out = compressed_size; + + // Use -MAX_WBITS for raw DEFLATE (no zlib wrapper) + int ret = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + fprintf(stderr, "deflateInit2 failed: %d\n", ret); + return 1; + } + + ret = deflate(&stream, Z_FINISH); + if (ret != Z_STREAM_END) { + fprintf(stderr, "deflate failed: %d\n", ret); + deflateEnd(&stream); + return 1; + } + + compressed_size = stream.total_out; + deflateEnd(&stream); + + FILE *out = fopen(output_file, "wb"); + if (!out) { + fprintf(stderr, "Failed to create %s: %s\n", output_file, strerror(errno)); + return 1; + } + + if (fwrite(compressed, 1, compressed_size, out) != compressed_size) { + fprintf(stderr, "Failed to write compressed data\n"); + return 1; + } + + fclose(out); + + return 0; +} diff --git a/src/build/framegen/main.zig b/src/build/framegen/main.zig deleted file mode 100644 index f4a7d9443..000000000 --- a/src/build/framegen/main.zig +++ /dev/null @@ -1,273 +0,0 @@ -const std = @import("std"); -const fs = std.fs; - -/// Generates a compressed file of all the ghostty frames -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - - var arg_iter = try std.process.argsWithAllocator(gpa.allocator()); - // Skip the exe name - _ = arg_iter.skip(); - - const out_dir_path = arg_iter.next() orelse return error.MissingOutputPath; - const compressed_out = "framedata.compressed"; - const zig_out = "framedata.zig"; - - const out_dir = try fs.cwd().openDir(out_dir_path, .{}); - const compressed_file = try out_dir.createFile(compressed_out, .{}); - - // Join the frames with a null byte. We'll split on this later - const all_frames = try std.mem.join(gpa.allocator(), "\x01", &frames); - var fbs = std.io.fixedBufferStream(all_frames); - - const reader = fbs.reader(); - try std.compress.flate.compress(reader, compressed_file.writer(), .{}); - - const compressed_path = try std.fs.path.join(gpa.allocator(), &.{ out_dir_path, compressed_out }); - - const zig_file = try out_dir.createFile(zig_out, .{}); - - try zig_file.writer().print( - \\//! This file is auto-generated. Do not edit. - \\ - \\pub const compressed = @embedFile("{s}"); - , .{compressed_path}); -} - -const frames = [_][]const u8{ - @embedFile("frames/frame_001.txt"), - @embedFile("frames/frame_002.txt"), - @embedFile("frames/frame_003.txt"), - @embedFile("frames/frame_004.txt"), - @embedFile("frames/frame_005.txt"), - @embedFile("frames/frame_006.txt"), - @embedFile("frames/frame_007.txt"), - @embedFile("frames/frame_008.txt"), - @embedFile("frames/frame_009.txt"), - @embedFile("frames/frame_010.txt"), - @embedFile("frames/frame_011.txt"), - @embedFile("frames/frame_012.txt"), - @embedFile("frames/frame_013.txt"), - @embedFile("frames/frame_014.txt"), - @embedFile("frames/frame_015.txt"), - @embedFile("frames/frame_016.txt"), - @embedFile("frames/frame_017.txt"), - @embedFile("frames/frame_018.txt"), - @embedFile("frames/frame_019.txt"), - @embedFile("frames/frame_020.txt"), - @embedFile("frames/frame_021.txt"), - @embedFile("frames/frame_022.txt"), - @embedFile("frames/frame_023.txt"), - @embedFile("frames/frame_024.txt"), - @embedFile("frames/frame_025.txt"), - @embedFile("frames/frame_026.txt"), - @embedFile("frames/frame_027.txt"), - @embedFile("frames/frame_028.txt"), - @embedFile("frames/frame_029.txt"), - @embedFile("frames/frame_030.txt"), - @embedFile("frames/frame_031.txt"), - @embedFile("frames/frame_032.txt"), - @embedFile("frames/frame_033.txt"), - @embedFile("frames/frame_034.txt"), - @embedFile("frames/frame_035.txt"), - @embedFile("frames/frame_036.txt"), - @embedFile("frames/frame_037.txt"), - @embedFile("frames/frame_038.txt"), - @embedFile("frames/frame_039.txt"), - @embedFile("frames/frame_040.txt"), - @embedFile("frames/frame_041.txt"), - @embedFile("frames/frame_042.txt"), - @embedFile("frames/frame_043.txt"), - @embedFile("frames/frame_044.txt"), - @embedFile("frames/frame_045.txt"), - @embedFile("frames/frame_046.txt"), - @embedFile("frames/frame_047.txt"), - @embedFile("frames/frame_048.txt"), - @embedFile("frames/frame_049.txt"), - @embedFile("frames/frame_050.txt"), - @embedFile("frames/frame_051.txt"), - @embedFile("frames/frame_052.txt"), - @embedFile("frames/frame_053.txt"), - @embedFile("frames/frame_054.txt"), - @embedFile("frames/frame_055.txt"), - @embedFile("frames/frame_056.txt"), - @embedFile("frames/frame_057.txt"), - @embedFile("frames/frame_058.txt"), - @embedFile("frames/frame_059.txt"), - @embedFile("frames/frame_060.txt"), - @embedFile("frames/frame_061.txt"), - @embedFile("frames/frame_062.txt"), - @embedFile("frames/frame_063.txt"), - @embedFile("frames/frame_064.txt"), - @embedFile("frames/frame_065.txt"), - @embedFile("frames/frame_066.txt"), - @embedFile("frames/frame_067.txt"), - @embedFile("frames/frame_068.txt"), - @embedFile("frames/frame_069.txt"), - @embedFile("frames/frame_070.txt"), - @embedFile("frames/frame_071.txt"), - @embedFile("frames/frame_072.txt"), - @embedFile("frames/frame_073.txt"), - @embedFile("frames/frame_074.txt"), - @embedFile("frames/frame_075.txt"), - @embedFile("frames/frame_076.txt"), - @embedFile("frames/frame_077.txt"), - @embedFile("frames/frame_078.txt"), - @embedFile("frames/frame_079.txt"), - @embedFile("frames/frame_080.txt"), - @embedFile("frames/frame_081.txt"), - @embedFile("frames/frame_082.txt"), - @embedFile("frames/frame_083.txt"), - @embedFile("frames/frame_084.txt"), - @embedFile("frames/frame_085.txt"), - @embedFile("frames/frame_086.txt"), - @embedFile("frames/frame_087.txt"), - @embedFile("frames/frame_088.txt"), - @embedFile("frames/frame_089.txt"), - @embedFile("frames/frame_090.txt"), - @embedFile("frames/frame_091.txt"), - @embedFile("frames/frame_092.txt"), - @embedFile("frames/frame_093.txt"), - @embedFile("frames/frame_094.txt"), - @embedFile("frames/frame_095.txt"), - @embedFile("frames/frame_096.txt"), - @embedFile("frames/frame_097.txt"), - @embedFile("frames/frame_098.txt"), - @embedFile("frames/frame_099.txt"), - @embedFile("frames/frame_100.txt"), - @embedFile("frames/frame_101.txt"), - @embedFile("frames/frame_102.txt"), - @embedFile("frames/frame_103.txt"), - @embedFile("frames/frame_104.txt"), - @embedFile("frames/frame_105.txt"), - @embedFile("frames/frame_106.txt"), - @embedFile("frames/frame_107.txt"), - @embedFile("frames/frame_108.txt"), - @embedFile("frames/frame_109.txt"), - @embedFile("frames/frame_110.txt"), - @embedFile("frames/frame_111.txt"), - @embedFile("frames/frame_112.txt"), - @embedFile("frames/frame_113.txt"), - @embedFile("frames/frame_114.txt"), - @embedFile("frames/frame_115.txt"), - @embedFile("frames/frame_116.txt"), - @embedFile("frames/frame_117.txt"), - @embedFile("frames/frame_118.txt"), - @embedFile("frames/frame_119.txt"), - @embedFile("frames/frame_120.txt"), - @embedFile("frames/frame_121.txt"), - @embedFile("frames/frame_122.txt"), - @embedFile("frames/frame_123.txt"), - @embedFile("frames/frame_124.txt"), - @embedFile("frames/frame_125.txt"), - @embedFile("frames/frame_126.txt"), - @embedFile("frames/frame_127.txt"), - @embedFile("frames/frame_128.txt"), - @embedFile("frames/frame_129.txt"), - @embedFile("frames/frame_130.txt"), - @embedFile("frames/frame_131.txt"), - @embedFile("frames/frame_132.txt"), - @embedFile("frames/frame_133.txt"), - @embedFile("frames/frame_134.txt"), - @embedFile("frames/frame_135.txt"), - @embedFile("frames/frame_136.txt"), - @embedFile("frames/frame_137.txt"), - @embedFile("frames/frame_138.txt"), - @embedFile("frames/frame_139.txt"), - @embedFile("frames/frame_140.txt"), - @embedFile("frames/frame_141.txt"), - @embedFile("frames/frame_142.txt"), - @embedFile("frames/frame_143.txt"), - @embedFile("frames/frame_144.txt"), - @embedFile("frames/frame_145.txt"), - @embedFile("frames/frame_146.txt"), - @embedFile("frames/frame_147.txt"), - @embedFile("frames/frame_148.txt"), - @embedFile("frames/frame_149.txt"), - @embedFile("frames/frame_150.txt"), - @embedFile("frames/frame_151.txt"), - @embedFile("frames/frame_152.txt"), - @embedFile("frames/frame_153.txt"), - @embedFile("frames/frame_154.txt"), - @embedFile("frames/frame_155.txt"), - @embedFile("frames/frame_156.txt"), - @embedFile("frames/frame_157.txt"), - @embedFile("frames/frame_158.txt"), - @embedFile("frames/frame_159.txt"), - @embedFile("frames/frame_160.txt"), - @embedFile("frames/frame_161.txt"), - @embedFile("frames/frame_162.txt"), - @embedFile("frames/frame_163.txt"), - @embedFile("frames/frame_164.txt"), - @embedFile("frames/frame_165.txt"), - @embedFile("frames/frame_166.txt"), - @embedFile("frames/frame_167.txt"), - @embedFile("frames/frame_168.txt"), - @embedFile("frames/frame_169.txt"), - @embedFile("frames/frame_170.txt"), - @embedFile("frames/frame_171.txt"), - @embedFile("frames/frame_172.txt"), - @embedFile("frames/frame_173.txt"), - @embedFile("frames/frame_174.txt"), - @embedFile("frames/frame_175.txt"), - @embedFile("frames/frame_176.txt"), - @embedFile("frames/frame_177.txt"), - @embedFile("frames/frame_178.txt"), - @embedFile("frames/frame_179.txt"), - @embedFile("frames/frame_180.txt"), - @embedFile("frames/frame_181.txt"), - @embedFile("frames/frame_182.txt"), - @embedFile("frames/frame_183.txt"), - @embedFile("frames/frame_184.txt"), - @embedFile("frames/frame_185.txt"), - @embedFile("frames/frame_186.txt"), - @embedFile("frames/frame_187.txt"), - @embedFile("frames/frame_188.txt"), - @embedFile("frames/frame_189.txt"), - @embedFile("frames/frame_190.txt"), - @embedFile("frames/frame_191.txt"), - @embedFile("frames/frame_192.txt"), - @embedFile("frames/frame_193.txt"), - @embedFile("frames/frame_194.txt"), - @embedFile("frames/frame_195.txt"), - @embedFile("frames/frame_196.txt"), - @embedFile("frames/frame_197.txt"), - @embedFile("frames/frame_198.txt"), - @embedFile("frames/frame_199.txt"), - @embedFile("frames/frame_200.txt"), - @embedFile("frames/frame_201.txt"), - @embedFile("frames/frame_202.txt"), - @embedFile("frames/frame_203.txt"), - @embedFile("frames/frame_204.txt"), - @embedFile("frames/frame_205.txt"), - @embedFile("frames/frame_206.txt"), - @embedFile("frames/frame_207.txt"), - @embedFile("frames/frame_208.txt"), - @embedFile("frames/frame_209.txt"), - @embedFile("frames/frame_210.txt"), - @embedFile("frames/frame_211.txt"), - @embedFile("frames/frame_212.txt"), - @embedFile("frames/frame_213.txt"), - @embedFile("frames/frame_214.txt"), - @embedFile("frames/frame_215.txt"), - @embedFile("frames/frame_216.txt"), - @embedFile("frames/frame_217.txt"), - @embedFile("frames/frame_218.txt"), - @embedFile("frames/frame_219.txt"), - @embedFile("frames/frame_220.txt"), - @embedFile("frames/frame_221.txt"), - @embedFile("frames/frame_222.txt"), - @embedFile("frames/frame_223.txt"), - @embedFile("frames/frame_224.txt"), - @embedFile("frames/frame_225.txt"), - @embedFile("frames/frame_226.txt"), - @embedFile("frames/frame_227.txt"), - @embedFile("frames/frame_228.txt"), - @embedFile("frames/frame_229.txt"), - @embedFile("frames/frame_230.txt"), - @embedFile("frames/frame_231.txt"), - @embedFile("frames/frame_232.txt"), - @embedFile("frames/frame_233.txt"), - @embedFile("frames/frame_234.txt"), - @embedFile("frames/frame_235.txt"), -}; From 85c879f1125361c57a25139c50257dd133b34d24 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 2 Oct 2025 00:11:33 -0700 Subject: [PATCH 118/319] fix(font): Let powerline glyphs be wide --- src/renderer/cell.zig | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 46e660bfd..8c0215673 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -236,8 +236,8 @@ pub fn isCovering(cp: u21) bool { } /// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area, except -/// the Powerline range, and anything in several unicode blocks: +/// for now we define as anything in a private use area, and anything +/// in several unicode blocks: /// - Dingbats /// - Emoticons /// - Miscellaneous Symbols @@ -249,13 +249,11 @@ pub fn isCovering(cp: u21) bool { /// In the future it may be prudent to expand this to encompass more /// symbol-like characters, and/or exclude some PUA sections. pub fn isSymbol(cp: u21) bool { - return symbols.get(cp) and !isPowerline(cp); + return symbols.get(cp); } /// Returns the appropriate `constraint_width` for /// the provided cell when rendering its glyph(s). -/// -/// Tested as part of the Screen tests. pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const cell = cell_pin.rowAndCell().cell; const cp = cell.codepoint(); @@ -276,6 +274,8 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // If we have a previous cell and it was a symbol then we need // to also constrain. This is so that multiple PUA glyphs align. + // This does not apply if the previous symbol is a graphics + // element such as a block element or Powerline glyph. if (cell_pin.x > 0) { const prev_cp = prev_cp: { var copy = cell_pin; @@ -284,7 +284,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { break :prev_cp prev_cell.codepoint(); }; - if (isSymbol(prev_cp)) { + if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) { return 1; } } @@ -305,11 +305,10 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { return 1; } -/// Whether min contrast should be disabled for a given glyph. -/// True for glyphs used for terminal graphics, such as box -/// drawing characters, block elements, and Powerline glyphs. +/// Whether min contrast should be disabled for a given glyph. True +/// for graphics elements such as blocks and Powerline glyphs. pub fn noMinContrast(cp: u21) bool { - return isBoxDrawing(cp) or isBlockElement(cp) or isLegacyComputing(cp) or isPowerline(cp); + return isGraphicsElement(cp); } // Some general spaces, others intentionally kept @@ -323,6 +322,12 @@ fn isSpace(char: u21) bool { }; } +/// Returns true if the codepoint is used for terminal graphics, such +/// as box drawing characters, block elements, and Powerline glyphs. +fn isGraphicsElement(char: u21) bool { + return isBoxDrawing(char) or isBlockElement(char) or isLegacyComputing(char) or isPowerline(char); +} + // Returns true if the codepoint is a box drawing character. fn isBoxDrawing(char: u21) bool { return switch (char) { @@ -514,7 +519,7 @@ test "Contents with zero-sized screen" { try testing.expect(c.getCursorGlyph() == null); } -test "Screen cell constraint widths" { +test "Cell constraint widths" { const testing = std.testing; const alloc = testing.allocator; @@ -606,4 +611,20 @@ test "Screen cell constraint widths" { try testing.expectEqual(2, constraintWidth(p1)); s.reset(); } + + // powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + + // powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } } From e615b11b2cd0986dff759c38eda7d903107d8ee4 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 2 Oct 2025 10:28:18 -0700 Subject: [PATCH 119/319] fix(config): Make macos-custom-icon null-terminated --- 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 b7a9699c8..2e21b9272 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2884,7 +2884,7 @@ keybind: Keybinds = .{}, /// /// Note: This configuration is required when `macos-icon` is set to /// `custom` -@"macos-custom-icon": ?[]const u8 = null, +@"macos-custom-icon": ?[:0]const u8 = null, /// The material to use for the frame of the macOS app icon. /// From 07124dba64096a36a84b76cab05a937ea3e2aeac Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 1 Oct 2025 23:48:08 -0500 Subject: [PATCH 120/319] core: add 'command finished' notifications Fixes #8991 Uses OSC 133 esc sequences to keep track of how long commands take to execute. If the user chooses, commands that take longer than a user specified limit will trigger a notification. The user can choose between a bell notification or a desktop notification. --- include/ghostty.h | 10 +++ src/Surface.zig | 44 +++++++++++ src/apprt/action.zig | 22 ++++++ src/apprt/gtk/class/application.zig | 31 +++++--- src/apprt/gtk/class/split_tree.zig | 2 +- src/apprt/gtk/class/surface.zig | 118 +++++++++++++++++++++++++++- src/apprt/gtk/class/tab.zig | 2 +- src/apprt/gtk/ext/actions.zig | 51 ++++++++++-- src/apprt/gtk/ui/1.2/surface.blp | 5 ++ src/apprt/surface.zig | 8 ++ src/config/Config.zig | 93 ++++++++++++++++++++++ src/termio/stream_handler.zig | 5 ++ 12 files changed, 366 insertions(+), 25 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 3f1e0c9d9..48836ee96 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -733,6 +733,14 @@ typedef struct { int8_t progress; } ghostty_action_progress_report_s; +// apprt.action.CommandFinished.C +typedef struct { + // -1 if no exit code was reported, otherwise 0-255 + int16_t exit_code; + // number of nanoseconds that command was running for + uint64_t duration; +} ghostty_action_command_finished_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -788,6 +796,7 @@ typedef enum { GHOSTTY_ACTION_SHOW_CHILD_EXITED, GHOSTTY_ACTION_PROGRESS_REPORT, GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, + GHOSTTY_ACTION_COMMAND_FINISHED, } ghostty_action_tag_e; typedef union { @@ -819,6 +828,7 @@ typedef union { ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; ghostty_action_progress_report_s progress_report; + ghostty_action_command_finished_s command_finished; } ghostty_action_u; typedef struct { diff --git a/src/Surface.zig b/src/Surface.zig index 03974dfc6..637af80cb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -33,6 +33,7 @@ const font = @import("font/main.zig"); const Command = @import("Command.zig"); const terminal = @import("terminal/main.zig"); const configpkg = @import("config.zig"); +const Duration = configpkg.Config.Duration; const input = @import("input.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); @@ -147,6 +148,13 @@ focused: bool = true, /// Used to determine whether to continuously scroll. selection_scroll_active: bool = false, +/// Used to send notifications that long running commands have finished. +/// Requires that shell integration be active. Should represent a nanosecond +/// precision timestamp. It does not necessarily need to correspond to the +/// actual time, but we must be able to compare two subsequent timestamps to get +/// the wall clock time that has elapsed between timestamps. +command_timer: ?i128 = null, + /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key /// input can be forwarded to the OS for further processing if it @@ -280,6 +288,9 @@ const DerivedConfig = struct { links: []Link, link_previews: configpkg.LinkPreviews, scroll_to_bottom: configpkg.Config.ScrollToBottom, + notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, + notify_on_command_finish_action: configpkg.Config.NotifyOnCommandFinishAction, + notify_on_command_finish_after: Duration, const Link = struct { regex: oni.Regex, @@ -350,6 +361,9 @@ const DerivedConfig = struct { .links = links, .link_previews = config.@"link-previews", .scroll_to_bottom = config.@"scroll-to-bottom", + .notify_on_command_finish = config.@"notify-on-command-finish", + .notify_on_command_finish_action = config.@"notify-on-command-finish-action", + .notify_on_command_finish_after = config.@"notify-on-command-finish-after", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -984,6 +998,36 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { self.selection_scroll_active = active; try self.selectionScrollTick(); }, + + .start_command_timer => { + self.command_timer = std.time.nanoTimestamp(); + }, + + .stop_command_timer => |v| timer: { + const end = std.time.nanoTimestamp(); + const start = self.command_timer orelse break :timer; + self.command_timer = null; + + const difference = end - start; + + // skip obviously silly results + if (difference < 0) break :timer; + if (difference > std.math.maxInt(u64)) break :timer; + + const duration: Duration = .{ .duration = @intCast(difference) }; + log.debug("command took {}", .{duration}); + + _ = self.rt_app.performAction( + .{ .surface = self }, + .command_finished, + .{ + .exit_code = v, + .duration = duration, + }, + ) catch |err| { + log.warn("apprt failed to notify command finish={}", .{err}); + }; + }, } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index b7dc80e03..b356ff32f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -295,6 +295,9 @@ pub const Action = union(Key) { /// Show the on-screen keyboard. show_on_screen_keyboard, + /// A command has finished, + command_finished: CommandFinished, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -350,6 +353,7 @@ pub const Action = union(Key) { show_child_exited, progress_report, show_on_screen_keyboard, + command_finished, }; /// Sync with: ghostty_action_u @@ -741,3 +745,21 @@ pub const CloseTabMode = enum(c_int) { /// Close all other tabs. other, }; + +pub const CommandFinished = struct { + exit_code: ?u8, + duration: configpkg.Config.Duration, + + /// sync with ghostty_action_command_finished_s in ghostty.h + pub const C = extern struct { + exit_code: i16, + duration: u64, + }; + + pub fn cval(self: CommandFinished) C { + return .{ + .exit_code = self.exit_code orelse -1, + .duration = self.duration.duration, + }; + } +}; diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index f7ed0d38c..90c72681d 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -713,6 +713,7 @@ pub const Application = extern struct { .toggle_command_palette => return Action.toggleCommandPalette(target), .toggle_split_zoom => return Action.toggleSplitZoom(target), .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target), + .command_finished => return Action.commandFinished(target, value), // Unimplemented .secure_input, @@ -1824,13 +1825,13 @@ const Action = struct { target: apprt.Target, n: apprt.action.DesktopNotification, ) void { - // TODO: We should move the surface target to a function call - // on Surface and emit a signal that embedders can connect to. This - // will let us handle notifications differently depending on where - // a surface is presented. At the time of writing this, we always - // want to show the notification AND the logic below was directly - // ported from "legacy" GTK so this is fine, but I want to leave this - // note so we can do it one day. + switch (target) { + .app => {}, + .surface => |v| { + v.rt_surface.gobj().sendDesktopNotification(n.title, n.body); + return; + }, + } // Set a default title if we don't already have one const t = switch (n.title.len) { @@ -1845,14 +1846,9 @@ const Action = struct { const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); defer icon.unref(); notification.setIcon(icon.as(gio.Icon)); - - const pointer = glib.Variant.newUint64(switch (target) { - .app => 0, - .surface => |v| @intFromPtr(v), - }); notification.setDefaultActionAndTargetValue( "app.present-surface", - pointer, + glib.Variant.newUint64(0), ); // We set the notification ID to the body content. If the content is the @@ -2457,6 +2453,15 @@ const Action = struct { }, } } + + pub fn commandFinished(target: apprt.Target, value: apprt.Action.Value(.command_finished)) bool { + switch (target) { + .app => return false, + .surface => |surface| { + return surface.rt_surface.gobj().commandFinished(value); + }, + } + } }; /// This sets various GTK-related environment variables as necessary diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 755b51e9a..977a7eab2 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -198,7 +198,7 @@ pub const SplitTree = extern struct { .init("zoom", actionZoom, null), }; - ext.actions.addAsGroup(Self, self, "split-tree", &actions); + _ = ext.actions.addAsGroup(Self, self, "split-tree", &actions); } /// Create a new split in the given direction from the currently diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 344bf8f21..401e542e4 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -32,6 +32,7 @@ const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog; const Window = @import("window.zig").Window; const WeakRef = @import("../weak_ref.zig").WeakRef; const InspectorWindow = @import("inspector_window.zig").InspectorWindow; +const i18n = @import("../../../os/i18n.zig"); const log = std.log.scoped(.gtk_ghostty_surface); @@ -545,6 +546,8 @@ pub const Surface = extern struct { // unfocused-split-* options is_split: bool = false, + action_group: ?*gio.SimpleActionGroup = null, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -809,6 +812,63 @@ pub const Surface = extern struct { ); } + pub fn commandFinished(self: *Self, value: apprt.Action.Value(.command_finished)) bool { + const app = Application.default(); + const alloc = app.allocator(); + const priv: *Private = self.private(); + + const notify_next_command_finish = notify: { + const simple_action_group = priv.action_group orelse break :notify false; + const action_group = simple_action_group.as(gio.ActionGroup); + const state = action_group.getActionState("notify-on-next-command-finish") orelse break :notify false; + const bool_variant_type = glib.ext.VariantType.newFor(bool); + defer bool_variant_type.free(); + if (state.isOfType(bool_variant_type) == 0) break :notify false; + const notify = state.getBoolean() != 0; + action_group.changeActionState("notify-on-next-command-finish", glib.Variant.newBoolean(@intFromBool(false))); + break :notify notify; + }; + + const config = priv.config orelse return false; + + const cfg = config.get(); + + if (!notify_next_command_finish) { + if (cfg.@"notify-on-command-finish" == .never) return true; + if (cfg.@"notify-on-command-finish" == .unfocused and self.getFocused()) return true; + } + + const action = cfg.@"notify-on-command-finish-action"; + + if (action.bell) self.setBellRinging(true); + + if (action.notify) notify: { + const title_ = title: { + const exit_code = value.exit_code orelse break :title i18n._("Command Finished"); + if (exit_code == 0) break :title i18n._("Command Succeeded"); + break :title i18n._("Command Failed"); + }; + const title = std.mem.span(title_); + const body = body: { + const exit_code = value.exit_code orelse break :body std.fmt.allocPrintZ( + alloc, + "Command took {}.", + .{value.duration.round(std.time.ns_per_ms)}, + ) catch break :notify; + break :body std.fmt.allocPrintZ( + alloc, + "Command took {} and exited with code {d}.", + .{ value.duration.round(std.time.ns_per_ms), exit_code }, + ) catch break :notify; + }; + defer alloc.free(body); + + self.sendDesktopNotification(title, body); + } + + return true; + } + /// Key press event (press or release). /// /// At a high level, we want to construct an `input.KeyEvent` and @@ -1404,6 +1464,34 @@ pub const Surface = extern struct { _ = priv.gl_area.as(gtk.Widget).grabFocus(); } + pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void { + const app = Application.default(); + + const t = switch (title.len) { + 0 => "Ghostty", + else => title, + }; + + const notification = gio.Notification.new(t); + defer notification.unref(); + notification.setBody(body); + + const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); + defer icon.unref(); + notification.setIcon(icon.as(gio.Icon)); + + const pointer = glib.Variant.newUint64(@intFromPtr(self)); + notification.setDefaultActionAndTargetValue( + "app.present-surface", + pointer, + ); + + // We set the notification ID to the body content. If the content is the + // same, this notification may replace a previous notification + const gio_app = app.as(gio.Application); + gio_app.sendNotification(body, notification); + } + //--------------------------------------------------------------- // Virtual Methods @@ -1460,11 +1548,23 @@ pub const Surface = extern struct { } fn initActionMap(self: *Self) void { + const priv: *Private = self.private(); + const actions = [_]ext.actions.Action(Self){ - .init("prompt-title", actionPromptTitle, null), + .init( + "prompt-title", + actionPromptTitle, + null, + ), + .initStateful( + "notify-on-next-command-finish", + actionNotifyOnNextCommandFinish, + null, + glib.Variant.newBoolean(@intFromBool(false)), + ), }; - ext.actions.addAsGroup(Self, self, "surface", &actions); + priv.action_group = ext.actions.addAsGroup(Self, self, "surface", &actions); } fn dispose(self: *Self) callconv(.c) void { @@ -1966,6 +2066,20 @@ pub const Surface = extern struct { }; } + pub fn actionNotifyOnNextCommandFinish( + action: *gio.SimpleAction, + _: ?*glib.Variant, + _: *Self, + ) callconv(.c) void { + const state = action.as(gio.Action).getState() orelse glib.Variant.newBoolean(@intFromBool(false)); + defer state.unref(); + const bool_variant_type = glib.ext.VariantType.newFor(bool); + defer bool_variant_type.free(); + if (state.isOfType(bool_variant_type) == 0) return; + const value = state.getBoolean() != 0; + action.setState(glib.Variant.newBoolean(@intFromBool(!value))); + } + fn childExitedClose( _: *ChildExited, self: *Self, diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index d8f9b97f8..373507507 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -206,7 +206,7 @@ pub const Tab = extern struct { .init("ring-bell", actionRingBell, null), }; - ext.actions.addAsGroup(Self, self, "tab", &actions); + _ = ext.actions.addAsGroup(Self, self, "tab", &actions); } //--------------------------------------------------------------- diff --git a/src/apprt/gtk/ext/actions.zig b/src/apprt/gtk/ext/actions.zig index 8499e7de8..344c08e05 100644 --- a/src/apprt/gtk/ext/actions.zig +++ b/src/apprt/gtk/ext/actions.zig @@ -40,14 +40,21 @@ test "gActionNameIsValid" { /// Function to create a structure for describing an action. pub fn Action(comptime T: type) type { return struct { + const Self = @This(); pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType, + state: ?*glib.Variant = null, - /// Function to initialize a new action so that we can comptime check the name. - pub fn init(comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType) @This() { + /// Function to initialize a new action so that we can comptime check + /// the name. + pub fn init( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + ) Self { comptime assert(gActionNameIsValid(name)); return .{ @@ -56,6 +63,23 @@ pub fn Action(comptime T: type) type { .parameter_type = parameter_type, }; } + + /// Function to initialize a new stateful action so that we can comptime + /// check the name. + pub fn initStateful( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + state: *glib.Variant, + ) Self { + comptime assert(gActionNameIsValid(name)); + return .{ + .name = name, + .callback = callback, + .parameter_type = parameter_type, + .state = state, + }; + } }; } @@ -68,10 +92,19 @@ pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void { pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void { for (actions) |entry| { assert(gActionNameIsValid(entry.name)); - const action = gio.SimpleAction.new( - entry.name, - entry.parameter_type, - ); + const action = action: { + if (entry.state) |state| { + break :action gio.SimpleAction.newStateful( + entry.name, + entry.parameter_type, + state, + ); + } + break :action gio.SimpleAction.new( + entry.name, + entry.parameter_type, + ); + }; defer action.unref(); _ = gio.SimpleAction.signals.activate.connect( action, @@ -85,7 +118,7 @@ pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []cons } /// Add actions to a widget that doesn't implement ActionGroup directly. -pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) void { +pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup { comptime assert(gActionNameIsValid(name)); // Collect our actions into a group since we're just a plain widget that @@ -99,6 +132,8 @@ pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actio name, group.as(gio.ActionGroup), ); + + return group; } test "adding actions to an object" { @@ -138,7 +173,7 @@ test "adding actions to an object" { .init("test", callbacks.callback, i32_variant_type), }; - addAsGroup(gtk.Box, box, "test", &actions); + _ = addAsGroup(gtk.Box, box, "test", &actions); } const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31)); diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 7ed78ecb3..84e00ac4a 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -203,6 +203,11 @@ menu context_menu_model { label: _("Paste"); action: "win.paste"; } + + item { + label: _("Notify on Next Command Finish"); + action: "surface.notify-on-next-command-finish"; + } } section { diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index e4effe128..70866c609 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -96,6 +96,14 @@ pub const Message = union(enum) { /// Report the progress of an action using a GUI element progress_report: terminal.osc.Command.ProgressReport, + /// A command has started in the shell, start a timer. + start_command_timer, + + /// A command has finished in the shell, stop the timer and send out + /// notifications as appropriate. The optional u8 is the exit code + /// of the command. + stop_command_timer: ?u8, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/config/Config.zig b/src/config/Config.zig index b7a9699c8..f33936e00 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1004,6 +1004,82 @@ command: ?Command = null, /// manually. @"initial-command": ?Command = null, +/// Controls when command finished notifications are sent. There are +/// three options: +/// +/// * `never` - Never send notifications (the default). +/// * `unfocused` - Only send notifications if the surface that the command is +/// running in is not focused. +/// * `always` - Always send notifications. +/// +/// Command finished notifications requires that either shell integration is +/// enabled, or that your shell sends OSC 133 escape sequences to mark the start +/// and end of commands. +/// +/// On GTK, there is a context menu item that will enable command finished +/// notifications for a single command, overriding the `never` and `unfocused` +/// options. +/// +/// GTK only. +/// +/// Available since 1.3.0. +@"notify-on-command-finish": NotifyOnCommandFinish = .never, + +/// If command finished notifications are enabled, this controls how the user is +/// notified. +/// +/// Available options: +/// +/// * `bell` - enabled by default +/// * `notify` - disabled by default +/// +/// Options can be combined by listing them as a comma separated list. Options +/// can be negated by prefixing them with `no-`. For example `no-bell,notify`. +/// +/// GTK only. +/// +/// Available since 1.3.0. +@"notify-on-command-finish-action": NotifyOnCommandFinishAction = .{ + .bell = true, + .notify = false, +}, + +/// If command finished notifications are enabled, this controls how long a +/// command must have been running before a notification will be sent. The +/// default is five seconds. +/// +/// The duration is specified as a series of numbers followed by time units. +/// Whitespace is allowed between numbers and units. Each number and unit will +/// be added together to form the total duration. +/// +/// The allowed time units are as follows: +/// +/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments +/// are made for leap years or leap seconds. +/// * `d` - one SI day, or 86400 seconds. +/// * `h` - one hour, or 3600 seconds. +/// * `m` - one minute, or 60 seconds. +/// * `s` - one second. +/// * `ms` - one millisecond, or 0.001 second. +/// * `us` or `µs` - one microsecond, or 0.000001 second. +/// * `ns` - one nanosecond, or 0.000000001 second. +/// +/// Examples: +/// * `1h30m` +/// * `45s` +/// +/// Units can be repeated and will be added together. This means that +/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided. +/// A future update may disallow this. +/// +/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any +/// value larger than this will be clamped to the maximum value. +/// +/// GTK only. +/// +/// Available since 1.3.0 +@"notify-on-command-finish-after": Duration = .{ .duration = 5 * std.time.ns_per_s }, + /// Extra environment variables to pass to commands launched in a terminal /// surface. The format is `env=KEY=VALUE`. /// @@ -8165,6 +8241,10 @@ pub const Duration = struct { return .{ .duration = self.duration / to * to }; } + pub fn lte(self: Duration, other: Duration) bool { + return self.duration <= other.duration; + } + pub fn parseCLI(input: ?[]const u8) !Duration { var remaining = input orelse return error.ValueRequired; @@ -8378,6 +8458,19 @@ pub const ScrollToBottom = packed struct { pub const default: ScrollToBottom = .{}; }; +/// See notify-on-command-finish +pub const NotifyOnCommandFinish = enum { + never, + unfocused, + always, +}; + +/// See notify-on-command-finish-action +pub const NotifyOnCommandFinishAction = packed struct { + bell: bool = true, + notify: bool = false, +}; + test "parse duration" { inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2d90831f2..b2b2af3d0 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1054,6 +1054,11 @@ pub const StreamHandler = struct { pub inline fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); + self.surfaceMessageWriter(.start_command_timer); + } + + pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void { + self.surfaceMessageWriter(.{ .stop_command_timer = exit_code }); } pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { From 1c23ebc6f16941237a567d3c1c93ec9c6d4d24d2 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 2 Oct 2025 12:57:53 -0500 Subject: [PATCH 121/319] address review comments --- src/Surface.zig | 18 ++++++------------ src/apprt/surface.zig | 4 ++-- src/termio/stream_handler.zig | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 637af80cb..3b4bf872f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -153,7 +153,7 @@ selection_scroll_active: bool = false, /// precision timestamp. It does not necessarily need to correspond to the /// actual time, but we must be able to compare two subsequent timestamps to get /// the wall clock time that has elapsed between timestamps. -command_timer: ?i128 = null, +command_timer: ?std.time.Instant = null, /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key @@ -999,22 +999,16 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { try self.selectionScrollTick(); }, - .start_command_timer => { - self.command_timer = std.time.nanoTimestamp(); + .start_command => { + self.command_timer = try .now(); }, - .stop_command_timer => |v| timer: { - const end = std.time.nanoTimestamp(); + .stop_command => |v| timer: { + const end: std.time.Instant = try .now(); const start = self.command_timer orelse break :timer; self.command_timer = null; - const difference = end - start; - - // skip obviously silly results - if (difference < 0) break :timer; - if (difference > std.math.maxInt(u64)) break :timer; - - const duration: Duration = .{ .duration = @intCast(difference) }; + const duration: Duration = .{ .duration = end.since(start) }; log.debug("command took {}", .{duration}); _ = self.rt_app.performAction( diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 70866c609..a46732c16 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -97,12 +97,12 @@ pub const Message = union(enum) { progress_report: terminal.osc.Command.ProgressReport, /// A command has started in the shell, start a timer. - start_command_timer, + start_command, /// A command has finished in the shell, stop the timer and send out /// notifications as appropriate. The optional u8 is the exit code /// of the command. - stop_command_timer: ?u8, + stop_command: ?u8, pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b2b2af3d0..9a7e8b416 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1054,11 +1054,11 @@ pub const StreamHandler = struct { pub inline fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); - self.surfaceMessageWriter(.start_command_timer); + self.surfaceMessageWriter(.start_command); } pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void { - self.surfaceMessageWriter(.{ .stop_command_timer = exit_code }); + self.surfaceMessageWriter(.{ .stop_command = exit_code }); } pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { From f76dd96c7e680e9133f3f816882f56e35215fedf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 11:09:47 -0700 Subject: [PATCH 122/319] crash: remove minidump parser We never used it because our minidump files on Linux didn't contain meaningful information. With Zig's Writergate, let's drop this and rewrite it later, we can always resurrect it from the git history. --- src/crash/main.zig | 1 - src/crash/minidump.zig | 7 - src/crash/minidump/external.zig | 59 ------ src/crash/minidump/reader.zig | 242 ----------------------- src/crash/minidump/stream.zig | 30 --- src/crash/minidump/stream_threadlist.zig | 117 ----------- 6 files changed, 456 deletions(-) delete mode 100644 src/crash/minidump.zig delete mode 100644 src/crash/minidump/external.zig delete mode 100644 src/crash/minidump/reader.zig delete mode 100644 src/crash/minidump/stream.zig delete mode 100644 src/crash/minidump/stream_threadlist.zig diff --git a/src/crash/main.zig b/src/crash/main.zig index 5f9aa96c5..1ac971851 100644 --- a/src/crash/main.zig +++ b/src/crash/main.zig @@ -5,7 +5,6 @@ const dir = @import("dir.zig"); const sentry_envelope = @import("sentry_envelope.zig"); -pub const minidump = @import("minidump.zig"); pub const sentry = @import("sentry.zig"); pub const Envelope = sentry_envelope.Envelope; pub const defaultDir = dir.defaultDir; diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig deleted file mode 100644 index 0abd67eae..000000000 --- a/src/crash/minidump.zig +++ /dev/null @@ -1,7 +0,0 @@ -pub const reader = @import("minidump/reader.zig"); -pub const stream = @import("minidump/stream.zig"); -pub const Reader = reader.Reader; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/crash/minidump/external.zig b/src/crash/minidump/external.zig deleted file mode 100644 index 451810883..000000000 --- a/src/crash/minidump/external.zig +++ /dev/null @@ -1,59 +0,0 @@ -//! This file contains the external structs and constants for the minidump -//! format. Most are from the Microsoft documentation on the minidump format: -//! https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ -//! -//! Wherever possible, we also compare our definitions to other projects -//! such as rust-minidump, libmdmp, breakpad, etc. to ensure we're doing -//! the right thing. - -/// "MDMP" in little-endian. -pub const signature = 0x504D444D; - -/// The version of the minidump format. -pub const version = 0xA793; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header -pub const Header = extern struct { - signature: u32, - version: packed struct(u32) { low: u16, high: u16 }, - stream_count: u32, - stream_directory_rva: u32, - checksum: u32, - time_date_stamp: u32, - flags: u64, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory -pub const Directory = extern struct { - stream_type: u32, - location: LocationDescriptor, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor -pub const LocationDescriptor = extern struct { - data_size: u32, - rva: u32, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_descriptor -pub const MemoryDescriptor = extern struct { - start_of_memory_range: u64, - memory: LocationDescriptor, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list -pub const ThreadList = extern struct { - number_of_threads: u32, - threads: [1]Thread, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread -pub const Thread = extern struct { - thread_id: u32, - suspend_count: u32, - priority_class: u32, - priority: u32, - teb: u64, - stack: MemoryDescriptor, - thread_context: LocationDescriptor, -}; diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig deleted file mode 100644 index b7f5efe80..000000000 --- a/src/crash/minidump/reader.zig +++ /dev/null @@ -1,242 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const external = @import("external.zig"); -const stream = @import("stream.zig"); -const EncodedStream = stream.EncodedStream; - -const log = std.log.scoped(.minidump_reader); - -/// Possible minidump-specific errors that can occur when reading a minidump. -/// This isn't the full error set since IO errors can also occur depending -/// on the Source type. -pub const ReadError = error{ - InvalidHeader, - InvalidVersion, - StreamSizeMismatch, -}; - -/// Reader creates a new minidump reader for the given source type. The -/// source must have both a "reader()" and "seekableStream()" function. -/// -/// Given the format of a minidump file, we must keep the source open and -/// continually access it because the format of the minidump is full of -/// pointers and offsets that we must follow depending on the stream types. -/// Also, since we're not aware of all stream types (in fact its impossible -/// to be aware since custom stream types are allowed), its possible any stream -/// type can define their own pointers and offsets. So, the source must always -/// be available so callers can decode the streams as needed. -pub fn Reader(comptime S: type) type { - return struct { - const Self = @This(); - - /// The source data. - source: Source, - - /// The endianness of the minidump file. This is detected by reading - /// the byte order of the header. - endian: std.builtin.Endian, - - /// The number of streams within the minidump file. This is read from - /// the header and stored here so we can quickly access them. Note - /// the stream types require reading the source; this is an optimization - /// to avoid any allocations on the reader and the caller can choose - /// to store them if they want. - stream_count: u32, - stream_directory_rva: u32, - - const SourceCallable = switch (@typeInfo(Source)) { - .pointer => |v| v.child, - .@"struct" => Source, - else => @compileError("Source type must be a pointer or struct"), - }; - - const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).@"fn".return_type.?; - const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).@"fn".return_type.?; - - /// A limited reader for reading data from the source. - pub const LimitedReader = std.io.LimitedReader(SourceReader); - - /// The source type for the reader. - pub const Source = S; - - /// The stream types for reading - pub const ThreadList = stream.thread_list.ThreadListReader(Self); - - /// The reader type for stream reading. This has some other methods so - /// you must still call reader() on the result to get the actual - /// reader to read the data. - pub const StreamReader = struct { - source: Source, - endian: std.builtin.Endian, - directory: external.Directory, - - /// Should not be accessed directly. This is setup whenever - /// reader() is called. - limit_reader: LimitedReader = undefined, - - pub const Reader = LimitedReader.Reader; - - /// Returns a Reader implementation that reads the bytes of the - /// stream. - /// - /// The reader is dependent on the state of Source so any - /// state-changing operations on Source will invalidate the - /// reader. For example, making another reader, reading another - /// stream directory, closing the source, etc. - pub fn reader(self: *StreamReader) LimitedReader.Reader { - try self.source.seekableStream().seekTo(self.directory.location.rva); - self.limit_reader = .{ - .inner_reader = self.source.reader(), - .bytes_left = self.directory.location.data_size, - }; - return self.limit_reader.reader(); - } - - /// Seeks the source to the location of the directory. - pub fn seekToPayload(self: *StreamReader) !void { - try self.source.seekableStream().seekTo(self.directory.location.rva); - } - }; - - /// Iterator type to read over the streams in the minidump file. - pub const StreamIterator = struct { - reader: *const Self, - i: u32 = 0, - - pub fn next(self: *StreamIterator) !?StreamReader { - if (self.i >= self.reader.stream_count) return null; - const dir = try self.reader.directory(self.i); - self.i += 1; - return try self.reader.streamReader(dir); - } - }; - - /// Initialize a reader. The source must remain available for the entire - /// lifetime of the reader. The reader does not take ownership of the - /// source so if it has resources that need to be cleaned up, the caller - /// must do so once the reader is no longer needed. - pub fn init(source: Source) !Self { - const header, const endian = try readHeader(Source, source); - return .{ - .source = source, - .endian = endian, - .stream_count = header.stream_count, - .stream_directory_rva = header.stream_directory_rva, - }; - } - - /// Return an iterator to read over the streams in the minidump file. - /// This is very similar to using a simple for loop to stream_count - /// and calling directory() on each index, but is more idiomatic - /// Zig. - pub fn streamIterator(self: *const Self) StreamIterator { - return .{ .reader = self }; - } - - /// Return a StreamReader for the given directory type. This streams - /// from the underlying source so the returned reader is only valid - /// as long as the source is unmodified (i.e. the source is not - /// closed, the source seek position is not moved, etc.). - pub fn streamReader( - self: *const Self, - dir: external.Directory, - ) SourceSeeker.SeekError!StreamReader { - return .{ - .source = self.source, - .endian = self.endian, - .directory = dir, - }; - } - - /// Get the directory entry with the given index. - /// - /// Asserts the index is valid (idx < stream_count). - pub fn directory(self: *const Self, idx: usize) !external.Directory { - assert(idx < self.stream_count); - - // Seek to the directory. - const offset: u32 = @intCast(@sizeOf(external.Directory) * idx); - const rva: u32 = self.stream_directory_rva + offset; - try self.source.seekableStream().seekTo(rva); - - // Read the directory. - return try self.source.reader().readStructEndian( - external.Directory, - self.endian, - ); - } - - /// Return a reader for the given location descriptor. This is only - /// valid until the reader source is modified in some way. - pub fn locationReader( - self: *const Self, - loc: external.LocationDescriptor, - ) !LimitedReader { - try self.source.seekableStream().seekTo(loc.rva); - return .{ - .inner_reader = self.source.reader(), - .bytes_left = loc.data_size, - }; - } - }; -} - -/// Reads the header for the minidump file and returns endianness of -/// the file. -fn readHeader(comptime T: type, source: T) !struct { - external.Header, - std.builtin.Endian, -} { - // Start by trying LE. - var endian: std.builtin.Endian = .little; - var header = try source.reader().readStructEndian(external.Header, endian); - - // If the signature doesn't match, we assume its BE. - if (header.signature != external.signature) { - // Seek back to the start of the file so we can reread. - try source.seekableStream().seekTo(0); - - // Try BE, if the signature doesn't match, return an error. - endian = .big; - header = try source.reader().readStructEndian(external.Header, endian); - if (header.signature != external.signature) return ReadError.InvalidHeader; - } - - // "The low-order word is MINIDUMP_VERSION. The high-order word is an - // internal value that is implementation specific." - if (header.version.low != external.version) return ReadError.InvalidVersion; - - return .{ header, endian }; -} - -// Uncomment to dump some debug information for a minidump file. -test "minidump debug" { - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const r = try Reader(*@TypeOf(fbs)).init(&fbs); - var it = r.streamIterator(); - while (try it.next()) |s| { - log.warn("directory i={} dir={}", .{ it.i - 1, s.directory }); - } -} - -test "minidump read" { - const testing = std.testing; - const alloc = testing.allocator; - - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const r = try Reader(*@TypeOf(fbs)).init(&fbs); - try testing.expectEqual(std.builtin.Endian.little, r.endian); - try testing.expectEqual(7, r.stream_count); - { - const dir = try r.directory(0); - try testing.expectEqual(3, dir.stream_type); - try testing.expectEqual(584, dir.location.data_size); - - var bytes = std.ArrayList(u8).init(alloc); - defer bytes.deinit(); - var sr = try r.streamReader(dir); - try sr.reader().readAllArrayList(&bytes, std.math.maxInt(usize)); - try testing.expectEqual(584, bytes.items.len); - } -} diff --git a/src/crash/minidump/stream.zig b/src/crash/minidump/stream.zig deleted file mode 100644 index 00ec6b042..000000000 --- a/src/crash/minidump/stream.zig +++ /dev/null @@ -1,30 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const log = std.log.scoped(.minidump_stream); - -/// The known stream types. -pub const thread_list = @import("stream_threadlist.zig"); - -/// A stream within the minidump file. A stream can be either in an encoded -/// form or decoded form. The encoded form are raw bytes and aren't validated -/// until they're decoded. The decoded form is a structured form of the stream. -/// -/// The decoded form is more ergonomic to work with but the encoded form is -/// more efficient to read/write. -pub const Stream = union(enum) { - encoded: EncodedStream, -}; - -/// An encoded stream value. It is "encoded" in the sense that it is raw bytes -/// with a type associated. The raw bytes are not validated to be correct for -/// the type. -pub const EncodedStream = struct { - type: u32, - data: []const u8, -}; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/crash/minidump/stream_threadlist.zig b/src/crash/minidump/stream_threadlist.zig deleted file mode 100644 index 51f3f9d4c..000000000 --- a/src/crash/minidump/stream_threadlist.zig +++ /dev/null @@ -1,117 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const external = @import("external.zig"); -const readerpkg = @import("reader.zig"); -const Reader = readerpkg.Reader; -const ReadError = readerpkg.ReadError; - -const log = std.log.scoped(.minidump_stream); - -/// This is the list of threads from the process. -/// -/// This is the Reader implementation. You usually do not use this directly. -/// Instead, use Reader(T).ThreadList which will get you the same thing. -/// -/// ThreadList is stream type 0x3. -/// StreamReader is the Reader(T).StreamReader type. -pub fn ThreadListReader(comptime R: type) type { - return struct { - const Self = @This(); - - /// The number of threads in the list. - count: u32, - - /// The rva to the first thread in the list. - rva: u32, - - /// Source data and endianness so we can read. - source: R.Source, - endian: std.builtin.Endian, - - pub fn init(r: *R.StreamReader) !Self { - assert(r.directory.stream_type == 0x3); - try r.seekToPayload(); - const reader = r.source.reader(); - - // Our count is always a u32 in the header. - const count = try reader.readInt(u32, r.endian); - - // Determine if we have padding in our header. It is possible - // for there to be padding if the list header was written by - // a 32-bit process but is being read on a 64-bit process. - const padding = padding: { - const maybe_size = @sizeOf(u32) + (@sizeOf(external.Thread) * count); - switch (std.math.order(maybe_size, r.directory.location.data_size)) { - // It should never be larger than what the directory says. - .gt => return ReadError.StreamSizeMismatch, - - // If the sizes match exactly we're good. - .eq => break :padding 0, - - .lt => { - const padding = r.directory.location.data_size - maybe_size; - if (padding != 4) return ReadError.StreamSizeMismatch; - break :padding padding; - }, - } - }; - - // Rva is the location of the first thread in the list. - const rva = r.directory.location.rva + @as(u32, @sizeOf(u32)) + padding; - - return .{ - .count = count, - .rva = rva, - .source = r.source, - .endian = r.endian, - }; - } - - /// Get the thread entry for the given index. - /// - /// Index is asserted to be less than count. - pub fn thread(self: *const Self, i: usize) !external.Thread { - assert(i < self.count); - - // Seek to the thread - const offset: u32 = @intCast(@sizeOf(external.Thread) * i); - const rva: u32 = self.rva + offset; - try self.source.seekableStream().seekTo(rva); - - // Read the thread - return try self.source.reader().readStructEndian( - external.Thread, - self.endian, - ); - } - }; -} - -test "minidump: threadlist" { - const testing = std.testing; - const alloc = testing.allocator; - - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const R = Reader(*@TypeOf(fbs)); - const r = try R.init(&fbs); - - // Get our thread list stream - const dir = try r.directory(0); - try testing.expectEqual(3, dir.stream_type); - var sr = try r.streamReader(dir); - - // Get our rich structure - const v = try R.ThreadList.init(&sr); - log.warn("threadlist count={} rva={}", .{ v.count, v.rva }); - - try testing.expectEqual(12, v.count); - for (0..v.count) |i| { - const t = try v.thread(i); - log.warn("thread i={} thread={}", .{ i, t }); - - // Read our stack memory - var stack_reader = try r.locationReader(t.stack.memory); - const bytes = try stack_reader.reader().readAllAlloc(alloc, t.stack.memory.data_size); - defer alloc.free(bytes); - } -} From d6063428bd9b58ca377b576eb0638be72afaa933 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 2 Oct 2025 14:06:58 -0600 Subject: [PATCH 123/319] font/coretext: tiny shaper improvements Reduce potential allocation while processing glyphs by ensuring capacity in the buffer ahead of time and also using CTRunGet*Ptr functions first and only allocating for those if that didn't work (it should almost always work in practice.) --- pkg/macos/text/run.zig | 40 ++++++++++++++++++++++++++++-------- src/font/shaper/coretext.zig | 13 ++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/pkg/macos/text/run.zig b/pkg/macos/text/run.zig index 9d40de81f..2895bfe34 100644 --- a/pkg/macos/text/run.zig +++ b/pkg/macos/text/run.zig @@ -15,10 +15,13 @@ pub const Run = opaque { return @intCast(c.CTRunGetGlyphCount(@ptrCast(self))); } - pub fn getGlyphsPtr(self: *Run) []const graphics.Glyph { + pub fn getGlyphsPtr(self: *Run) ?[]const graphics.Glyph { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetGlyphsPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const graphics.Glyph = @ptrCast( + c.CTRunGetGlyphsPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -34,10 +37,13 @@ pub const Run = opaque { return ptr; } - pub fn getPositionsPtr(self: *Run) []const graphics.Point { + pub fn getPositionsPtr(self: *Run) ?[]const graphics.Point { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetPositionsPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const graphics.Point = @ptrCast( + c.CTRunGetPositionsPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -53,10 +59,13 @@ pub const Run = opaque { return ptr; } - pub fn getAdvancesPtr(self: *Run) []const graphics.Size { + pub fn getAdvancesPtr(self: *Run) ?[]const graphics.Size { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetAdvancesPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const graphics.Size = @ptrCast( + c.CTRunGetAdvancesPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -72,10 +81,13 @@ pub const Run = opaque { return ptr; } - pub fn getStringIndicesPtr(self: *Run) []const usize { + pub fn getStringIndicesPtr(self: *Run) ?[]const usize { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetStringIndicesPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const usize = @ptrCast( + c.CTRunGetStringIndicesPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -90,4 +102,16 @@ pub const Run = opaque { ); return ptr; } + + pub fn getStatus(self: *Run) Status { + return @bitCast(c.CTRunGetStatus(@ptrCast(self))); + } +}; + +/// https://developer.apple.com/documentation/coretext/ctrunstatus?language=objc +pub const Status = packed struct(u32) { + right_to_left: bool, + non_monotonic: bool, + has_non_identity_matrix: bool, + _pad: u29 = 0, }; diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 285a5a6b9..b00610d2f 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -369,7 +369,12 @@ pub const Shaper = struct { x: f64 = 0, y: f64 = 0, } = .{}; + + // Clear our cell buf and make sure we have enough room for the whole + // line of glyphs, so that we can just assume capacity when appending + // instead of maybe allocating. self.cell_buf.clearRetainingCapacity(); + try self.cell_buf.ensureTotalCapacity(self.alloc, line.getGlyphCount()); // CoreText may generate multiple runs even though our input to // CoreText is already split into runs by our own run iterator. @@ -381,9 +386,9 @@ pub const Shaper = struct { const ctrun = runs.getValueAtIndex(macos.text.Run, i); // Get our glyphs and positions - const glyphs = try ctrun.getGlyphs(alloc); - const advances = try ctrun.getAdvances(alloc); - const indices = try ctrun.getStringIndices(alloc); + const glyphs = ctrun.getGlyphsPtr() orelse try ctrun.getGlyphs(alloc); + const advances = ctrun.getAdvancesPtr() orelse try ctrun.getAdvances(alloc); + const indices = ctrun.getStringIndicesPtr() orelse try ctrun.getStringIndices(alloc); assert(glyphs.len == advances.len); assert(glyphs.len == indices.len); @@ -406,7 +411,7 @@ pub const Shaper = struct { cell_offset = .{ .cluster = cluster }; } - try self.cell_buf.append(self.alloc, .{ + self.cell_buf.appendAssumeCapacity(.{ .x = @intCast(cluster), .x_offset = @intFromFloat(@round(cell_offset.x)), .y_offset = @intFromFloat(@round(cell_offset.y)), From efc6e0d6738d3ff070422db1512eb1b9f91b278f Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 2 Oct 2025 15:01:44 -0600 Subject: [PATCH 124/319] fix(font/coretext): always prevent shaper from emitting rtl The solution we had before worked in most cases but there were some which caused problems still. This is what HarfBuzz's CoreText shaper backend does, it uses a CTTypesetter with the forced embedding level attribute. This fixes the failure case I found that was causing non- monotonic outputs which can have all sorts of unexpected results, and causes a crash in Debug modes because we assert the monotonicity while rendering. --- pkg/macos/text.zig | 2 + pkg/macos/text/typesetter.zig | 36 +++++++++++++++++ src/font/shaper/coretext.zig | 73 ++++++++++++++++++++--------------- 3 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 pkg/macos/text/typesetter.zig diff --git a/pkg/macos/text.zig b/pkg/macos/text.zig index 0589f8692..bfaa388b3 100644 --- a/pkg/macos/text.zig +++ b/pkg/macos/text.zig @@ -4,6 +4,7 @@ const font_descriptor = @import("text/font_descriptor.zig"); const font_manager = @import("text/font_manager.zig"); const frame = @import("text/frame.zig"); const framesetter = @import("text/framesetter.zig"); +const typesetter = @import("text/typesetter.zig"); const line = @import("text/line.zig"); const paragraph_style = @import("text/paragraph_style.zig"); const run = @import("text/run.zig"); @@ -23,6 +24,7 @@ pub const createFontDescriptorsFromData = font_manager.createFontDescriptorsFrom pub const createFontDescriptorFromData = font_manager.createFontDescriptorFromData; pub const Frame = frame.Frame; pub const Framesetter = framesetter.Framesetter; +pub const Typesetter = typesetter.Typesetter; pub const Line = line.Line; pub const ParagraphStyle = paragraph_style.ParagraphStyle; pub const ParagraphStyleSetting = paragraph_style.ParagraphStyleSetting; diff --git a/pkg/macos/text/typesetter.zig b/pkg/macos/text/typesetter.zig new file mode 100644 index 000000000..dc07df980 --- /dev/null +++ b/pkg/macos/text/typesetter.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const foundation = @import("../foundation.zig"); +const graphics = @import("../graphics.zig"); +const text = @import("../text.zig"); +const c = @import("c.zig").c; + +pub const Typesetter = opaque { + pub fn createWithAttributedStringAndOptions( + str: *foundation.AttributedString, + opts: *foundation.Dictionary, + ) Allocator.Error!*Typesetter { + return @as( + ?*Typesetter, + @ptrFromInt(@intFromPtr(c.CTTypesetterCreateWithAttributedStringAndOptions( + @ptrCast(str), + @ptrCast(opts), + ))), + ) orelse Allocator.Error.OutOfMemory; + } + + pub fn release(self: *Typesetter) void { + foundation.CFRelease(self); + } + + pub fn createLine( + self: *Typesetter, + range: foundation.c.CFRange, + ) *text.Line { + return @ptrFromInt(@intFromPtr(c.CTTypesetterCreateLine( + @ptrCast(self), + range, + ))); + } +}; diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index b00610d2f..f1368679d 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -52,10 +52,10 @@ pub const Shaper = struct { /// The shared memory used for shaping results. cell_buf: CellBuf, - /// The cached writing direction value for shaping. This isn't - /// configurable we just use this as a cache to avoid creating - /// and releasing many objects when shaping. - writing_direction: *macos.foundation.Array, + /// Cached attributes dict for creating CTTypesetter objects. + /// The values in this never change so we can avoid overhead + /// by just creating it once and saving it for re-use. + typesetter_attr_dict: *macos.foundation.Dictionary, /// List where we cache fonts, so we don't have to remake them for /// every single shaping operation. @@ -174,21 +174,28 @@ pub const Shaper = struct { // // See: https://github.com/mitchellh/ghostty/issues/1737 // See: https://github.com/mitchellh/ghostty/issues/1442 - const writing_direction = array: { - const dir: macos.text.WritingDirection = .lro; - const num = try macos.foundation.Number.create( - .int, - &@intFromEnum(dir), - ); + // + // We used to do this by setting the writing direction attribute + // on the attributed string we used, but it seems like that will + // still allow some weird results, for example a single space at + // the end of a line composed of RTL characters will be cause it + // to output a run containing just that space, BEFORE it outputs + // the rest of the line as a separate run, very weirdly with the + // "right to left" flag set in the single space run's run status... + // + // So instead what we do is use a CTTypesetter to create our line, + // using the kCTTypesetterOptionForcedEmbeddingLevel attribute to + // force CoreText not to try doing any sort of BiDi, instead just + // treat all text as embedding level 0 (left to right). + const typesetter_attr_dict = dict: { + const num = try macos.foundation.Number.create(.int, &0); defer num.release(); - - var arr_init = [_]*const macos.foundation.Number{num}; - break :array try macos.foundation.Array.create( - macos.foundation.Number, - &arr_init, + break :dict try macos.foundation.Dictionary.create( + &.{macos.c.kCTTypesetterOptionForcedEmbeddingLevel}, + &.{num}, ); }; - errdefer writing_direction.release(); + errdefer typesetter_attr_dict.release(); // Create the CF release thread. var cf_release_thread = try alloc.create(CFReleaseThread); @@ -210,7 +217,7 @@ pub const Shaper = struct { .run_state = run_state, .features = features, .features_no_default = features_no_default, - .writing_direction = writing_direction, + .typesetter_attr_dict = typesetter_attr_dict, .cached_fonts = .{}, .cached_font_grid = 0, .cf_release_pool = .{}, @@ -224,7 +231,7 @@ pub const Shaper = struct { self.run_state.deinit(self.alloc); self.features.release(); self.features_no_default.release(); - self.writing_direction.release(); + self.typesetter_attr_dict.release(); { for (self.cached_fonts.items) |ft| { @@ -346,8 +353,8 @@ pub const Shaper = struct { run.font_index, ); - // Make room for the attributed string and the CTLine. - try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 3); + // Make room for the attributed string, CTTypesetter, and CTLine. + try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 4); const str = macos.foundation.String.createWithCharactersNoCopy(state.unichars.items); self.cf_release_pool.appendAssumeCapacity(str); @@ -359,8 +366,17 @@ pub const Shaper = struct { ); self.cf_release_pool.appendAssumeCapacity(attr_str); - // We should always have one run because we do our own run splitting. - const line = try macos.text.Line.createWithAttributedString(attr_str); + // Create a typesetter from the attributed string and the cached + // attr dict. (See comment in init for more info on the attr dict.) + const typesetter = + try macos.text.Typesetter.createWithAttributedStringAndOptions( + attr_str, + self.typesetter_attr_dict, + ); + self.cf_release_pool.appendAssumeCapacity(typesetter); + + // Create a line from the typesetter + const line = typesetter.createLine(.{ .location = 0, .length = 0 }); self.cf_release_pool.appendAssumeCapacity(line); // This keeps track of the current offsets within a single cell. @@ -516,15 +532,10 @@ pub const Shaper = struct { // Get our font and use that get the attributes to set for the // attributed string so the whole string uses the same font. const attr_dict = dict: { - var keys = [_]?*const anyopaque{ - macos.text.StringAttribute.font.key(), - macos.text.StringAttribute.writing_direction.key(), - }; - var values = [_]?*const anyopaque{ - run_font, - self.writing_direction, - }; - break :dict try macos.foundation.Dictionary.create(&keys, &values); + break :dict try macos.foundation.Dictionary.create( + &.{macos.text.StringAttribute.font.key()}, + &.{run_font}, + ); }; self.cached_fonts.items[index_int] = attr_dict; From 10adef3092d29d6dcd6944e594dfadcbe927bed0 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 2 Oct 2025 13:43:33 -0500 Subject: [PATCH 125/319] gtk: fix duplicate signal handlers --- src/apprt/gtk/class/window.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index c0dd6ab1f..31a4cc6ff 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -693,6 +693,10 @@ pub const Window = extern struct { self: *Self, tree: *const Surface.Tree, ) void { + // Ensure that all old signal handlers have been removed before adding + // them. Otherwise we get duplicate surface handlers. + self.disconnectSurfaceHandlers(tree); + const priv = self.private(); var it = tree.iterator(); while (it.next()) |entry| { From c8ed3031bcd8853666b5b78170379dffa11c741d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 2 Oct 2025 17:18:08 -0500 Subject: [PATCH 126/319] gtk: improve signal handler management Instead of making two separate passes over the surfaces in a split tree to manage signal handlers, do it in one pass. --- src/apprt/gtk/class/window.zig | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 31a4cc6ff..8efff8729 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -693,14 +693,23 @@ pub const Window = extern struct { self: *Self, tree: *const Surface.Tree, ) void { - // Ensure that all old signal handlers have been removed before adding - // them. Otherwise we get duplicate surface handlers. - self.disconnectSurfaceHandlers(tree); - const priv = self.private(); var it = tree.iterator(); while (it.next()) |entry| { const surface = entry.view; + // Before adding any new signal handlers, disconnect any that we may + // have added before. Otherwise we may get multiple handlers for the + // same signal. + _ = gobject.signalHandlersDisconnectMatched( + surface.as(gobject.Object), + .{ .data = true }, + 0, + 0, + null, + null, + self, + ); + _ = Surface.signals.@"present-request".connect( surface, *Self, From d02770d292fe24acde02c61de94fb0c9f152a537 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Sep 2025 12:24:03 -0700 Subject: [PATCH 127/319] zig-15: build binary builds --- build.zig | 6 +-- build.zig.zon | 33 +++++++++-------- flake.lock | 6 +-- flake.nix | 2 +- pkg/apple-sdk/build.zig | 10 ++--- pkg/breakpad/build.zig | 5 +-- pkg/cimgui/build.zig | 10 ++--- pkg/fontconfig/build.zig | 24 ++++++------ pkg/freetype/build.zig | 8 ++-- pkg/glslang/build.zig | 6 +-- pkg/gtk4-layer-shell/build.zig | 9 +++-- pkg/harfbuzz/build.zig | 12 +++--- pkg/highway/build.zig | 8 ++-- pkg/libintl/build.zig | 6 +-- pkg/libpng/build.zig | 6 +-- pkg/libxml2/build.zig | 20 +++++----- pkg/oniguruma/build.zig | 5 +-- pkg/sentry/build.zig | 15 ++++---- pkg/simdutf/build.zig | 6 +-- pkg/spirv-cross/build.zig | 6 +-- pkg/utfcpp/build.zig | 5 +-- pkg/wuffs/build.zig | 8 ++-- pkg/zlib/build.zig | 6 +-- src/build/Config.zig | 2 +- src/build/GhosttyBench.zig | 8 ++-- src/build/GhosttyDist.zig | 14 +++---- src/build/GhosttyDocs.zig | 10 ++--- src/build/GhosttyFrameData.zig | 4 +- src/build/GhosttyI18n.zig | 8 ++-- src/build/GhosttyLib.zig | 5 ++- src/build/GhosttyLibVt.zig | 3 +- src/build/GhosttyResources.zig | 67 +++++++++++++++++----------------- src/build/GhosttyWebdata.zig | 22 ++++++----- src/build/MetallibStep.zig | 2 +- src/build/SharedDeps.zig | 44 +++++++++++++++++----- 35 files changed, 221 insertions(+), 190 deletions(-) diff --git a/build.zig b/build.zig index 62fa77511..cb8f175a4 100644 --- a/build.zig +++ b/build.zig @@ -4,7 +4,7 @@ const builtin = @import("builtin"); const buildpkg = @import("src/build/main.zig"); comptime { - buildpkg.requireZig("0.14.0"); + buildpkg.requireZig("0.15.1"); } pub fn build(b: *std.Build) !void { @@ -249,8 +249,6 @@ pub fn build(b: *std.Build) !void { { const mod_vt_test = b.addTest(.{ .root_module = mod.vt, - .target = config.target, - .optimize = config.optimize, .filters = test_filters, }); const mod_vt_test_run = b.addRunArtifact(mod_vt_test); @@ -258,8 +256,6 @@ pub fn build(b: *std.Build) !void { const mod_vt_c_test = b.addTest(.{ .root_module = mod.vt_c, - .target = config.target, - .optimize = config.optimize, .filters = test_filters, }); const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test); diff --git a/build.zig.zon b/build.zig.zon index 992284bf7..b28cd4991 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,48 +9,49 @@ .libxev = .{ // mitchellh/libxev - .url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz", - .hash = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q", + .url = "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + .hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", .lazy = true, }, .vaxis = .{ // rockorager/libvaxis - .url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", - .hash = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn", + .url = "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", + .hash = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", .lazy = true, }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", - .hash = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP", + .url = "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", + .hash = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", .lazy = true, }, .zig_objc = .{ // mitchellh/zig-objc - .url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", - .hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk", + .url = "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + .hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", .lazy = true, }, .zig_js = .{ // mitchellh/zig-js - .url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", - .hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", + .url = "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + .hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", .lazy = true, }, .uucode = .{ - .url = "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", - .hash = "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE", + // TODO: currently the use-llvm branch because its broken on self-hosted + .url = "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + .hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland - .url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", - .hash = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy", + .url = "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + .hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", .lazy = true, }, .zf = .{ // natecraddock/zf - .url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", - .hash = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9", + .url = "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", + .hash = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", .lazy = true, }, .gobject = .{ diff --git a/flake.lock b/flake.lock index bbd4567e3..03413ce88 100644 --- a/flake.lock +++ b/flake.lock @@ -97,11 +97,11 @@ ] }, "locked": { - "lastModified": 1748261582, - "narHash": "sha256-3i0IL3s18hdDlbsf0/E+5kyPRkZwGPbSFngq5eToiAA=", + "lastModified": 1759192380, + "narHash": "sha256-0BWJgt4OSzxCESij5oo8WLWrPZ+1qLp8KUQe32QeV4Q=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "aafb1b093fb838f7a02613b719e85ec912914221", + "rev": "0bcd1401ed43d10f10cbded49624206553e92f57", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7d72a9af3..2f2d8a2a9 100644 --- a/flake.nix +++ b/flake.nix @@ -47,7 +47,7 @@ pkgs = nixpkgs.legacyPackages.${system}; in { devShell.${system} = pkgs.callPackage ./nix/devShell.nix { - zig = zig.packages.${system}."0.14.1"; + zig = zig.packages.${system}."0.15.1"; wraptest = pkgs.callPackage ./nix/wraptest.nix {}; zon2nix = zon2nix; }; diff --git a/pkg/apple-sdk/build.zig b/pkg/apple-sdk/build.zig index 18a6c0968..c573c3910 100644 --- a/pkg/apple-sdk/build.zig +++ b/pkg/apple-sdk/build.zig @@ -46,19 +46,19 @@ pub fn addPaths( // find the SDK path. const libc = try std.zig.LibCInstallation.findNative(.{ .allocator = b.allocator, - .target = step.rootModuleTarget(), + .target = &step.rootModuleTarget(), .verbose = false, }); // Render the file compatible with the `--libc` Zig flag. - var list: std.ArrayList(u8) = .init(b.allocator); - defer list.deinit(); - try libc.render(list.writer()); + var stream: std.io.Writer.Allocating = .init(b.allocator); + defer stream.deinit(); + try libc.render(&stream.writer); // Create a temporary file to store the libc path because // `--libc` expects a file path. const wf = b.addWriteFiles(); - const path = wf.add("libc.txt", list.items); + const path = wf.add("libc.txt", stream.written()); // Determine our framework path. Zig has a bug where it doesn't // parse this from the libc txt file for `-framework` flags: diff --git a/pkg/breakpad/build.zig b/pkg/breakpad/build.zig index 9ab6b89cd..56d51b159 100644 --- a/pkg/breakpad/build.zig +++ b/pkg/breakpad/build.zig @@ -19,9 +19,8 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); if (b.lazyDependency("breakpad", .{})) |upstream| { lib.addIncludePath(upstream.path("src")); diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index f14bc1242..b94f11943 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -55,19 +55,19 @@ pub fn build(b: *std.Build) !void { if (imgui_) |imgui| lib.addIncludePath(imgui.path("")); module.addIncludePath(b.path("vendor")); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DCIMGUI_FREETYPE=1", "-DIMGUI_USE_WCHAR32=1", "-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1", }); if (target.result.os.tag == .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DIMGUI_IMPL_API=extern\t\"C\"", }); } diff --git a/pkg/fontconfig/build.zig b/pkg/fontconfig/build.zig index c9ea517ed..7c87d1f2e 100644 --- a/pkg/fontconfig/build.zig +++ b/pkg/fontconfig/build.zig @@ -82,9 +82,9 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu lib.addIncludePath(b.path("override/include")); module.addIncludePath(b.path("override/include")); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_DIRENT_H", "-DHAVE_FCNTL_H", "-DHAVE_STDLIB_H", @@ -129,12 +129,12 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu }); switch (target.result.ptrBitWidth()) { - 32 => try flags.appendSlice(&.{ + 32 => try flags.appendSlice(b.allocator, &.{ "-DSIZEOF_VOID_P=4", "-DALIGNOF_VOID_P=4", }), - 64 => try flags.appendSlice(&.{ + 64 => try flags.appendSlice(b.allocator, &.{ "-DSIZEOF_VOID_P=8", "-DALIGNOF_VOID_P=8", }), @@ -142,14 +142,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu else => @panic("unsupported arch"), } if (target.result.os.tag == .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DFC_CACHEDIR=\"LOCAL_APPDATA_FONTCONFIG_CACHE\"", "-DFC_TEMPLATEDIR=\"c:/share/fontconfig/conf.avail\"", "-DCONFIGDIR=\"c:/etc/fonts/conf.d\"", "-DFC_DEFAULT_FONTS=\"\\tWINDOWSFONTDIR\\n\\tWINDOWSUSERFONTDIR\\n\"", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_FSTATFS", "-DHAVE_FSTATVFS", "-DHAVE_GETOPT", @@ -173,13 +173,13 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu }); if (target.result.os.tag == .freebsd) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DFC_TEMPLATEDIR=\"/usr/local/etc/fonts/conf.avail\"", "-DFONTCONFIG_PATH=\"/usr/local/etc/fonts\"", "-DCONFIGDIR=\"/usr/local/etc/fonts/conf.d\"", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"", "-DFONTCONFIG_PATH=\"/etc/fonts\"", "-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"", @@ -187,7 +187,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu } if (target.result.os.tag == .linux) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_SYS_STATFS_H", "-DHAVE_SYS_VFS_H", }); @@ -214,14 +214,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu // Libxml2 _ = b.systemIntegrationOption("libxml2", .{}); // So it shows up in help if (libxml2_enabled) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DENABLE_LIBXML2", "-DLIBXML_STATIC", "-DLIBXML_PUSH_ENABLED", }); if (target.result.os.tag == .windows) { // NOTE: this should be defined on all targets - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-Werror=implicit-function-declaration", }); } diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index d000442be..a25dc18da 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -77,9 +77,9 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DFT2_BUILD_LIBRARY", "-DFT_CONFIG_OPTION_SYSTEM_ZLIB=1", @@ -103,7 +103,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu // Libpng _ = b.systemIntegrationOption("libpng", .{}); // So it shows up in help if (libpng_enabled) { - try flags.append("-DFT_CONFIG_OPTION_USE_PNG=1"); + try flags.append(b.allocator, "-DFT_CONFIG_OPTION_USE_PNG=1"); if (b.systemIntegrationOption("libpng", .{})) { lib.linkSystemLibrary2("libpng", dynamic_link_opts); diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 52993a662..746a41497 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -59,9 +59,9 @@ fn buildGlslang( try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined", }); diff --git a/pkg/gtk4-layer-shell/build.zig b/pkg/gtk4-layer-shell/build.zig index 543faf129..b9cf78a23 100644 --- a/pkg/gtk4-layer-shell/build.zig +++ b/pkg/gtk4-layer-shell/build.zig @@ -36,10 +36,13 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu const optimize = options.optimize; // Shared library - const lib = b.addSharedLibrary(.{ + const lib = b.addLibrary(.{ .name = "gtk4-layer-shell", - .target = target, - .optimize = optimize, + .linkage = .dynamic, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), }); b.installArtifact(lib); diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index bf247461a..8696c0203 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -111,13 +111,13 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu const dynamic_link_opts = options.dynamic_link_opts; - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_STDBOOL_H", }); if (target.result.os.tag != .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_UNISTD_H", "-DHAVE_SYS_MMAN_H", "-DHAVE_PTHREAD=1", @@ -127,7 +127,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu // Freetype _ = b.systemIntegrationOption("freetype", .{}); // So it shows up in help if (freetype_enabled) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_FREETYPE=1", // Let's just assume a new freetype @@ -153,7 +153,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu } if (coretext_enabled) { - try flags.appendSlice(&.{"-DHAVE_CORETEXT=1"}); + try flags.appendSlice(b.allocator, &.{"-DHAVE_CORETEXT=1"}); lib.linkFramework("CoreText"); module.linkFramework("CoreText", .{}); } diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 1013f1643..4c75de49e 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -31,9 +31,9 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ // Avoid changing binaries based on the current time and date. "-Wno-builtin-macro-redefined", "-D__DATE__=\"redacted\"", @@ -69,7 +69,7 @@ pub fn build(b: *std.Build) !void { "-fno-vectorize", }); if (target.result.os.tag != .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-fmath-errno", "-fno-exceptions", }); diff --git a/pkg/libintl/build.zig b/pkg/libintl/build.zig index 0e32648e7..32221e5ad 100644 --- a/pkg/libintl/build.zig +++ b/pkg/libintl/build.zig @@ -22,9 +22,9 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_CONFIG_H", "-DLOCALEDIR=\"\"", }); diff --git a/pkg/libpng/build.zig b/pkg/libpng/build.zig index 11ed29b18..dbedac632 100644 --- a/pkg/libpng/build.zig +++ b/pkg/libpng/build.zig @@ -46,9 +46,9 @@ pub fn build(b: *std.Build) !void { } if (b.lazyDependency("libpng", .{})) |upstream| { - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DPNG_ARM_NEON_OPT=0", "-DPNG_POWERPC_VSX_OPT=0", "-DPNG_INTEL_SSE_OPT=0", diff --git a/pkg/libxml2/build.zig b/pkg/libxml2/build.zig index acebfaf63..a9b3e4b1a 100644 --- a/pkg/libxml2/build.zig +++ b/pkg/libxml2/build.zig @@ -25,9 +25,9 @@ pub fn build(b: *std.Build) !void { lib.addIncludePath(b.path("override/config/posix")); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ // Version info, hardcoded comptime "-DLIBXML_VERSION=" ++ Version.number(), comptime "-DLIBXML_VERSION_STRING=" ++ Version.string(), @@ -46,7 +46,7 @@ pub fn build(b: *std.Build) !void { "-DWITHOUT_TRIO=1", }); if (target.result.os.tag != .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_ARPA_INET_H=1", "-DHAVE_ARPA_NAMESER_H=1", "-DHAVE_DL_H=1", @@ -74,25 +74,25 @@ pub fn build(b: *std.Build) !void { var nameBuf: [32]u8 = undefined; const name = std.ascii.upperString(&nameBuf, field.name); const define = try std.fmt.allocPrint(b.allocator, "-DLIBXML_{s}_ENABLED=1", .{name}); - try flags.append(define); + try flags.append(b.allocator, define); if (std.mem.eql(u8, field.name, "history")) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_LIBHISTORY=1", "-DHAVE_LIBREADLINE=1", }); } if (std.mem.eql(u8, field.name, "mem_debug")) { - try flags.append("-DDEBUG_MEMORY_LOCATION=1"); + try flags.append(b.allocator, "-DDEBUG_MEMORY_LOCATION=1"); } if (std.mem.eql(u8, field.name, "regexp")) { - try flags.append("-DLIBXML_UNICODE_ENABLED=1"); + try flags.append(b.allocator, "-DLIBXML_UNICODE_ENABLED=1"); } if (std.mem.eql(u8, field.name, "run_debug")) { - try flags.append("-DLIBXML_DEBUG_RUNTIME=1"); + try flags.append(b.allocator, "-DLIBXML_DEBUG_RUNTIME=1"); } if (std.mem.eql(u8, field.name, "thread")) { - try flags.append("-DHAVE_LIBPTHREAD=1"); + try flags.append(b.allocator, "-DHAVE_LIBPTHREAD=1"); } } } diff --git a/pkg/oniguruma/build.zig b/pkg/oniguruma/build.zig index 77e3b6f65..ea39b4814 100644 --- a/pkg/oniguruma/build.zig +++ b/pkg/oniguruma/build.zig @@ -100,9 +100,8 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .SIZEOF_VOIDP = t.ptrBitWidth() / t.cTypeBitSize(.char), })); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); lib.addCSourceFiles(.{ .root = upstream.path(""), .flags = flags.items, diff --git a/pkg/sentry/build.zig b/pkg/sentry/build.zig index 95900ae8f..3c88df56d 100644 --- a/pkg/sentry/build.zig +++ b/pkg/sentry/build.zig @@ -26,22 +26,21 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); if (target.result.os.tag == .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DSENTRY_WITH_UNWINDER_DBGHELP", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DSENTRY_WITH_UNWINDER_LIBBACKTRACE", }); } switch (backend) { - .crashpad => try flags.append("-DSENTRY_BACKEND_CRASHPAD"), - .breakpad => try flags.append("-DSENTRY_BACKEND_BREAKPAD"), - .inproc => try flags.append("-DSENTRY_BACKEND_INPROC"), + .crashpad => try flags.append(b.allocator, "-DSENTRY_BACKEND_CRASHPAD"), + .breakpad => try flags.append(b.allocator, "-DSENTRY_BACKEND_BREAKPAD"), + .inproc => try flags.append(b.allocator, "-DSENTRY_BACKEND_INPROC"), .none => {}, } diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index f96eeae45..f2ddfeba4 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -20,11 +20,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 // (See root Ghostty build.zig on why we do this) - try flags.appendSlice(&.{"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0"}); + try flags.appendSlice(b.allocator, &.{"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0"}); lib.addCSourceFiles(.{ .flags = flags.items, diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index ff7f15c94..003ec43cf 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -64,9 +64,9 @@ fn buildSpirvCross( try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DSPIRV_CROSS_C_API_GLSL=1", "-DSPIRV_CROSS_C_API_MSL=1", diff --git a/pkg/utfcpp/build.zig b/pkg/utfcpp/build.zig index 341b35578..e06813b83 100644 --- a/pkg/utfcpp/build.zig +++ b/pkg/utfcpp/build.zig @@ -19,9 +19,8 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); lib.addCSourceFiles(.{ .flags = flags.items, diff --git a/pkg/wuffs/build.zig b/pkg/wuffs/build.zig index 57d89e6b6..3d9f83daa 100644 --- a/pkg/wuffs/build.zig +++ b/pkg/wuffs/build.zig @@ -17,11 +17,11 @@ pub fn build(b: *std.Build) !void { }); unit_tests.linkLibC(); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.append("-DWUFFS_IMPLEMENTATION"); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.append(b.allocator, "-DWUFFS_IMPLEMENTATION"); inline for (@import("src/c.zig").defines) |key| { - try flags.append("-D" ++ key); + try flags.append(b.allocator, "-D" ++ key); } if (b.lazyDependency("wuffs", .{})) |wuffs_dep| { diff --git a/pkg/zlib/build.zig b/pkg/zlib/build.zig index caa557454..246ab1bcb 100644 --- a/pkg/zlib/build.zig +++ b/pkg/zlib/build.zig @@ -26,9 +26,9 @@ pub fn build(b: *std.Build) !void { .{ .include_extensions = &.{".h"} }, ); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_SYS_TYPES_H", "-DHAVE_STDINT_H", "-DHAVE_STDDEF_H", diff --git a/src/build/Config.zig b/src/build/Config.zig index e0e81e519..643dfe928 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -477,7 +477,7 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void { step.addOption(std.SemanticVersion, "app_version", self.version); step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ( &buf, - "{}", + "{f}", .{self.version}, )); step.addOption( diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 5859a8bcf..c9cd5dd33 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -11,8 +11,8 @@ pub fn init( b: *std.Build, deps: *const SharedDeps, ) !GhosttyBench { - var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step.Compile) = .empty; + errdefer steps.deinit(b.allocator); // Our synthetic data generator { @@ -28,7 +28,7 @@ pub fn init( }); exe.linkLibC(); _ = try deps.add(exe); - try steps.append(exe); + try steps.append(b.allocator, exe); } // Our benchmarking application. @@ -44,7 +44,7 @@ pub fn init( }); exe.linkLibC(); _ = try deps.add(exe); - try steps.append(exe); + try steps.append(b.allocator, exe); } return .{ .steps = steps.items }; diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index f8c221350..092322689 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -43,10 +43,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // embed the Ghostty version in the tarball { - const version = b.addWriteFiles().add("VERSION", b.fmt("{}", .{cfg.version})); + const version = b.addWriteFiles().add("VERSION", b.fmt("{f}", .{cfg.version})); // --add-file uses the most recent --prefix to determine the path // in the archive to copy the file (the directory only). - git_archive.addArg(b.fmt("--prefix=ghostty-{}/", .{ + git_archive.addArg(b.fmt("--prefix=ghostty-{f}/", .{ cfg.version, })); git_archive.addPrefixedFileArg("--add-file=", version); @@ -65,7 +65,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // --add-file uses the most recent --prefix to determine the path // in the archive to copy the file (the directory only). - git_archive.addArg(b.fmt("--prefix=ghostty-{}/{s}/", .{ + git_archive.addArg(b.fmt("--prefix=ghostty-{f}/{s}/", .{ cfg.version, std.fs.path.dirname(resource.dist).?, })); @@ -77,11 +77,11 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // This is important. Standard source tarballs extract into // a directory named `project-version`. This is expected by // standard tooling such as debhelper and rpmbuild. - b.fmt("--prefix=ghostty-{}/", .{cfg.version}), + b.fmt("--prefix=ghostty-{f}/", .{cfg.version}), "-o", }); const output = git_archive.addOutputFileArg(b.fmt( - "ghostty-{}.tar.gz", + "ghostty-{f}.tar.gz", .{cfg.version}, )); git_archive.addArg("HEAD"); @@ -89,7 +89,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // The install step to put the dist into the build directory. const install = b.addInstallFile( output, - b.fmt("dist/ghostty-{}.tar.gz", .{cfg.version}), + b.fmt("dist/ghostty-{f}.tar.gz", .{cfg.version}), ); // The check step to ensure the archive works. @@ -101,7 +101,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // i.e. this is way `build.zig` is. const extract_dir = check .addOutputDirectoryArg("ghostty") - .path(b, b.fmt("ghostty-{}", .{cfg.version})); + .path(b, b.fmt("ghostty-{f}", .{cfg.version})); // Check that tests pass within the extracted directory. This isn't // a fully hermetic test because we're sharing the Zig cache. In diff --git a/src/build/GhosttyDocs.zig b/src/build/GhosttyDocs.zig index b95b56f74..cd75fc061 100644 --- a/src/build/GhosttyDocs.zig +++ b/src/build/GhosttyDocs.zig @@ -12,8 +12,8 @@ pub fn init( b: *std.Build, deps: *const SharedDeps, ) !GhosttyDocs { - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + errdefer steps.deinit(b.allocator); const manpages = [_]struct { name: []const u8, @@ -52,7 +52,7 @@ pub fn init( const generate_markdown_step = b.addRunArtifact(generate_markdown); const markdown_output = generate_markdown_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( markdown_output, "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".md", ).step); @@ -67,7 +67,7 @@ pub fn init( }); generate_html.addFileArg(markdown_output); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( generate_html.captureStdOut(), "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html", ).step); @@ -82,7 +82,7 @@ pub fn init( }); generate_manpage.addFileArg(markdown_output); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( generate_manpage.captureStdOut(), "share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section, ).step); diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig index 52c84a66c..7193162bd 100644 --- a/src/build/GhosttyFrameData.zig +++ b/src/build/GhosttyFrameData.zig @@ -40,7 +40,9 @@ pub fn distResources(b: *std.Build) struct { } { const exe = b.addExecutable(.{ .name = "framegen", - .target = b.graph.host, + .root_module = b.createModule(.{ + .target = b.graph.host, + }), }); exe.addCSourceFile(.{ .file = b.path("src/build/framegen/main.c"), diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig index b99e60426..8e31f61b3 100644 --- a/src/build/GhosttyI18n.zig +++ b/src/build/GhosttyI18n.zig @@ -18,8 +18,8 @@ update_step: *std.Build.Step, pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n { _ = cfg; - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - defer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + defer steps.deinit(b.allocator); inline for (locales) |locale| { // There is no encoding suffix in the LC_MESSAGES path on FreeBSD, @@ -33,7 +33,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n { const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" }); msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po")); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( msgfmt.captureStdOut(), std.fmt.comptimePrint( "share/locale/{s}/LC_MESSAGES/{s}.mo", @@ -45,7 +45,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n { return .{ .owner = b, .update_step = try createUpdateStep(b), - .steps = try steps.toOwnedSlice(), + .steps = try steps.toOwnedSlice(b.allocator), }; } diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index b244a72c5..4b9729170 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -40,7 +40,7 @@ pub fn initStatic( // Add our dependencies. Get the list of all static deps so we can // build a combined archive if necessary. var lib_list = try deps.add(lib); - try lib_list.append(lib.getEmittedBin()); + try lib_list.append(b.allocator, lib.getEmittedBin()); if (!deps.config.target.result.os.tag.isDarwin()) return .{ .step = &lib.step, @@ -69,8 +69,9 @@ pub fn initShared( b: *std.Build, deps: *const SharedDeps, ) !GhosttyLib { - const lib = b.addSharedLibrary(.{ + const lib = b.addLibrary(.{ .name = "ghostty", + .linkage = .dynamic, .root_module = b.createModule(.{ .root_source_file = b.path("src/main_c.zig"), .target = deps.config.target, diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 9eb945293..1e57da7b1 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -24,8 +24,9 @@ pub fn initShared( zig: *const GhosttyZig, ) !GhosttyLibVt { const target = zig.vt.resolved_target.?; - const lib = b.addSharedLibrary(.{ + const lib = b.addLibrary(.{ .name = "ghostty-vt", + .linkage = .dynamic, .root_module = zig.vt_c, .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, }); diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 0db1fd418..7880a98a0 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -10,8 +10,8 @@ const RunStep = std.Build.Step.Run; steps: []*std.Build.Step, pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + errdefer steps.deinit(b.allocator); // This is the exe used to generate some build data. const build_data_exe = b.addExecutable(.{ @@ -49,7 +49,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { "share/terminfo/ghostty.terminfo", ); - try steps.append(&source_install.step); + try steps.append(b.allocator, &source_install.step); } // Windows doesn't have the binaries below. @@ -73,7 +73,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { "share/terminfo/ghostty.termcap", ); - try steps.append(&cap_install.step); + try steps.append(b.allocator, &cap_install.step); } // Compile the terminfo source into a terminfo database @@ -99,7 +99,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .{ b.install_path, terminfo_share_dir }, )); - try steps.append(&mkdir_step.step); + try steps.append(b.allocator, &mkdir_step.step); // Use cp -R instead of Step.InstallDir because we need to preserve // symlinks in the terminfo database. Zig's InstallDir step doesn't @@ -109,7 +109,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { copy_step.addFileArg(path); copy_step.addArg(b.fmt("{s}/share", .{b.install_path})); copy_step.step.dependOn(&mkdir_step.step); - try steps.append(©_step.step); + try steps.append(b.allocator, ©_step.step); } } @@ -121,7 +121,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_subdir = b.pathJoin(&.{ "ghostty", "shell-integration" }), .exclude_extensions = &.{".md"}, }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // Themes @@ -132,7 +132,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), .exclude_extensions = &.{".md"}, }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // Fish shell completions @@ -147,7 +147,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/fish/vendor_completions.d", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // zsh shell completions @@ -162,7 +162,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/zsh/site-functions", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // bash shell completions @@ -177,7 +177,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/bash-completion/completions", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // Vim and Neovim plugin @@ -210,14 +210,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/vim/vimfiles", }); - try steps.append(&vim_step.step); + try steps.append(b.allocator, &vim_step.step); const neovim_step = b.addInstallDirectory(.{ .source_dir = wf.getDirectory(), .install_dir = .prefix, .install_subdir = "share/nvim/site", }); - try steps.append(&neovim_step.step); + try steps.append(b.allocator, &neovim_step.step); } // Sublime syntax highlighting for bat cli tool @@ -237,7 +237,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/bat/syntaxes", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // App (Linux) @@ -286,16 +286,17 @@ fn addLinuxAppResources( // second element of the tuple. const Template = struct { std.Build.LazyPath, []const u8 }; const templates: []const Template = templates: { - var ts: std.ArrayList(Template) = .init(b.allocator); + var ts: std.ArrayList(Template) = .empty; + defer ts.deinit(b.allocator); // Desktop file so that we have an icon and other metadata - try ts.append(.{ + try ts.append(b.allocator, .{ b.path("dist/linux/app.desktop.in"), b.fmt("share/applications/{s}.desktop", .{app_id}), }); // Service for DBus activation. - try ts.append(.{ + try ts.append(b.allocator, .{ if (cfg.flatpak) b.path("dist/linux/dbus.service.flatpak.in") else @@ -320,7 +321,7 @@ fn addLinuxAppResources( // See the following code: // // https://github.com/flatpak/xdg-desktop-portal/blob/7d4d48cf079147c8887da17ec6c3954acd5a285c/src/xdp-utils.c#L152-L220 - if (!cfg.flatpak) try ts.append(.{ + if (!cfg.flatpak) try ts.append(b.allocator, .{ b.path("dist/linux/systemd.service.in"), b.fmt( "{s}/systemd/user/app-{s}.service", @@ -333,12 +334,12 @@ fn addLinuxAppResources( // AppStream metainfo so that application has rich metadata // within app stores - try ts.append(.{ + try ts.append(b.allocator, .{ b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"), b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}), }); - break :templates ts.items; + break :templates try ts.toOwnedSlice(b.allocator); }; // Process all our templates @@ -361,65 +362,65 @@ fn addLinuxAppResources( template[1], ); - try steps.append(©.step); + try steps.append(b.allocator, ©.step); } // Right click menu action for Plasma desktop - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("dist/linux/ghostty_dolphin.desktop"), "share/kio/servicemenus/com.mitchellh.ghostty.desktop", ).step); // Right click menu action for Nautilus. Note that this _must_ be named // `ghostty.py`. Using the full app id causes problems (see #5468). - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("dist/linux/ghostty_nautilus.py"), "share/nautilus-python/extensions/ghostty.py", ).step); // Various icons that our application can use, including the icon // that will be used for the desktop. - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/16.png"), "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/32.png"), "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/128.png"), "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/256.png"), "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/512.png"), "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png", ).step); // Flatpaks only support icons up to 512x512. if (!cfg.flatpak) { - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/1024.png"), "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png", ).step); } - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/32.png"), "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/64.png"), "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/256.png"), "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/512.png"), "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png", ).step); diff --git a/src/build/GhosttyWebdata.zig b/src/build/GhosttyWebdata.zig index b0201c3ff..145bb91fa 100644 --- a/src/build/GhosttyWebdata.zig +++ b/src/build/GhosttyWebdata.zig @@ -12,8 +12,8 @@ pub fn init( b: *std.Build, deps: *const SharedDeps, ) !GhosttyWebdata { - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + errdefer steps.deinit(b.allocator); { const webgen_config = b.addExecutable(.{ @@ -43,7 +43,7 @@ pub fn init( const webgen_config_step = b.addRunArtifact(webgen_config); const webgen_config_out = webgen_config_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( webgen_config_out, "share/ghostty/webdata/config.mdx", ).step); @@ -52,8 +52,10 @@ pub fn init( { const webgen_actions = b.addExecutable(.{ .name = "webgen_actions", - .root_source_file = b.path("src/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + }), }); deps.help_strings.addImport(webgen_actions); @@ -72,7 +74,7 @@ pub fn init( const webgen_actions_step = b.addRunArtifact(webgen_actions); const webgen_actions_out = webgen_actions_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( webgen_actions_out, "share/ghostty/webdata/actions.mdx", ).step); @@ -81,8 +83,10 @@ pub fn init( { const webgen_commands = b.addExecutable(.{ .name = "webgen_commands", - .root_source_file = b.path("src/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + }), }); deps.help_strings.addImport(webgen_commands); @@ -101,7 +105,7 @@ pub fn init( const webgen_commands_step = b.addRunArtifact(webgen_commands); const webgen_commands_out = webgen_commands_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( webgen_commands_out, "share/ghostty/webdata/commands.mdx", ).step); diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index 6999f8f31..fcf3055f8 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -44,7 +44,7 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { const self = b.allocator.create(MetallibStep) catch @panic("OOM"); const min_version = if (opts.target.query.os_version_min) |v| - b.fmt("{}", .{v.semver}) + b.fmt("{f}", .{v.semver}) else switch (opts.target.result.os.tag) { .macos => "10.14", .ios => "11.0", diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 9461d48b7..785830ab9 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -113,8 +113,8 @@ pub fn add( // We maintain a list of our static libraries and return it so that // we can build a single fat static library for the final app. - var static_libs = LazyPathList.init(b.allocator); - errdefer static_libs.deinit(); + var static_libs: LazyPathList = .empty; + errdefer static_libs.deinit(b.allocator); // WARNING: This is a hack! // If we're cross-compiling to Darwin then we don't add any deps. @@ -154,6 +154,7 @@ pub fn add( } else { step.linkLibrary(freetype_dep.artifact("freetype")); try static_libs.append( + b.allocator, freetype_dep.artifact("freetype").getEmittedBin(), ); } @@ -178,6 +179,7 @@ pub fn add( } else { step.linkLibrary(harfbuzz_dep.artifact("harfbuzz")); try static_libs.append( + b.allocator, harfbuzz_dep.artifact("harfbuzz").getEmittedBin(), ); } @@ -201,6 +203,7 @@ pub fn add( } else { step.linkLibrary(fontconfig_dep.artifact("fontconfig")); try static_libs.append( + b.allocator, fontconfig_dep.artifact("fontconfig").getEmittedBin(), ); } @@ -218,6 +221,7 @@ pub fn add( })) |libpng_dep| { step.linkLibrary(libpng_dep.artifact("png")); try static_libs.append( + b.allocator, libpng_dep.artifact("png").getEmittedBin(), ); } @@ -231,6 +235,7 @@ pub fn add( })) |zlib_dep| { step.linkLibrary(zlib_dep.artifact("z")); try static_libs.append( + b.allocator, zlib_dep.artifact("z").getEmittedBin(), ); } @@ -250,6 +255,7 @@ pub fn add( } else { step.linkLibrary(oniguruma_dep.artifact("oniguruma")); try static_libs.append( + b.allocator, oniguruma_dep.artifact("oniguruma").getEmittedBin(), ); } @@ -270,6 +276,7 @@ pub fn add( } else { step.linkLibrary(glslang_dep.artifact("glslang")); try static_libs.append( + b.allocator, glslang_dep.artifact("glslang").getEmittedBin(), ); } @@ -289,6 +296,7 @@ pub fn add( } else { step.linkLibrary(spirv_cross_dep.artifact("spirv_cross")); try static_libs.append( + b.allocator, spirv_cross_dep.artifact("spirv_cross").getEmittedBin(), ); } @@ -307,6 +315,7 @@ pub fn add( ); step.linkLibrary(sentry_dep.artifact("sentry")); try static_libs.append( + b.allocator, sentry_dep.artifact("sentry").getEmittedBin(), ); @@ -316,6 +325,7 @@ pub fn add( .optimize = optimize, })) |breakpad_dep| { try static_libs.append( + b.allocator, breakpad_dep.artifact("breakpad").getEmittedBin(), ); } @@ -443,6 +453,7 @@ pub fn add( macos_dep.artifact("macos"), ); try static_libs.append( + b.allocator, macos_dep.artifact("macos").getEmittedBin(), ); } @@ -461,6 +472,7 @@ pub fn add( })) |libintl_dep| { step.linkLibrary(libintl_dep.artifact("intl")); try static_libs.append( + b.allocator, libintl_dep.artifact("intl").getEmittedBin(), ); } @@ -473,7 +485,10 @@ pub fn add( })) |cimgui_dep| { step.root_module.addImport("cimgui", cimgui_dep.module("cimgui")); step.linkLibrary(cimgui_dep.artifact("cimgui")); - try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin()); + try static_libs.append( + b.allocator, + cimgui_dep.artifact("cimgui").getEmittedBin(), + ); } // Fonts @@ -697,6 +712,7 @@ pub fn addSimd( })) |simdutf_dep| { m.linkLibrary(simdutf_dep.artifact("simdutf")); if (static_libs) |v| try v.append( + b.allocator, simdutf_dep.artifact("simdutf").getEmittedBin(), ); } @@ -708,7 +724,10 @@ pub fn addSimd( .optimize = optimize, })) |highway_dep| { m.linkLibrary(highway_dep.artifact("highway")); - if (static_libs) |v| try v.append(highway_dep.artifact("highway").getEmittedBin()); + if (static_libs) |v| try v.append( + b.allocator, + highway_dep.artifact("highway").getEmittedBin(), + ); } // utfcpp - This is used as a dependency on our hand-written C++ code @@ -717,7 +736,10 @@ pub fn addSimd( .optimize = optimize, })) |utfcpp_dep| { m.linkLibrary(utfcpp_dep.artifact("utfcpp")); - if (static_libs) |v| try v.append(utfcpp_dep.artifact("utfcpp").getEmittedBin()); + if (static_libs) |v| try v.append( + b.allocator, + utfcpp_dep.artifact("utfcpp").getEmittedBin(), + ); } // SIMD C++ files @@ -761,16 +783,20 @@ pub fn gtkNgDistResources( const gresource_xml = gresource_xml: { const xml_exe = b.addExecutable(.{ .name = "generate_gresource_xml", - .root_source_file = b.path("src/apprt/gtk/build/gresource.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/apprt/gtk/build/gresource.zig"), + .target = b.graph.host, + }), }); const xml_run = b.addRunArtifact(xml_exe); // Run our blueprint compiler across all of our blueprint files. const blueprint_exe = b.addExecutable(.{ .name = "gtk_blueprint_compiler", - .root_source_file = b.path("src/apprt/gtk/build/blueprint.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/apprt/gtk/build/blueprint.zig"), + .target = b.graph.host, + }), }); blueprint_exe.linkLibC(); blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts); From 7ec57aeebd8cae4ef13bb2cdcd041d6a9672003c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Oct 2025 11:23:06 -0700 Subject: [PATCH 128/319] Zig 0.15: zig fmt --- pkg/freetype/face.zig | 4 ++-- pkg/macos/foundation/array.zig | 2 +- pkg/macos/foundation/attributed_string.zig | 2 +- pkg/macos/foundation/dictionary.zig | 4 ++-- pkg/macos/text/font.zig | 4 ++-- pkg/macos/text/line.zig | 2 +- pkg/macos/video/display_link.zig | 2 +- pkg/oniguruma/init.zig | 2 +- pkg/wuffs/src/jpeg.zig | 2 +- pkg/wuffs/src/png.zig | 2 +- src/apprt/gtk/class.zig | 2 +- src/apprt/gtk/class/resize_overlay.zig | 4 ++-- src/apprt/gtk/class/surface.zig | 16 ++++++++-------- src/apprt/gtk/class/surface_title_dialog.zig | 2 +- src/apprt/gtk/class/tab.zig | 4 ++-- src/cli/args.zig | 4 ++-- src/inspector/Inspector.zig | 2 +- src/terminal/bitmap_allocator.zig | 2 +- 18 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index 84178b860..f8714d4fe 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -193,8 +193,8 @@ pub const Face = struct { ) void { c.FT_Set_Transform( self.handle, - @constCast(@ptrCast(matrix)), - @constCast(@ptrCast(delta)), + @ptrCast(@constCast(matrix)), + @ptrCast(@constCast(delta)), ); } }; diff --git a/pkg/macos/foundation/array.zig b/pkg/macos/foundation/array.zig index d3a977539..7b580eb03 100644 --- a/pkg/macos/foundation/array.zig +++ b/pkg/macos/foundation/array.zig @@ -68,7 +68,7 @@ pub const MutableArray = opaque { comptime Elem: type, value: *const Elem, ) void { - CFArrayAppendValue(self, @constCast(@ptrCast(value))); + CFArrayAppendValue(self, @ptrCast(@constCast(value))); } pub fn removeValue(self: *MutableArray, idx: usize) void { diff --git a/pkg/macos/foundation/attributed_string.zig b/pkg/macos/foundation/attributed_string.zig index de509b2c0..c7f27d7d7 100644 --- a/pkg/macos/foundation/attributed_string.zig +++ b/pkg/macos/foundation/attributed_string.zig @@ -10,7 +10,7 @@ pub const AttributedString = opaque { str: *foundation.String, attributes: *foundation.Dictionary, ) Allocator.Error!*AttributedString { - return @constCast(@ptrCast(c.CFAttributedStringCreate( + return @ptrCast(@constCast(c.CFAttributedStringCreate( null, @ptrCast(str), @ptrCast(attributes), diff --git a/pkg/macos/foundation/dictionary.zig b/pkg/macos/foundation/dictionary.zig index 90642e59a..a529442ac 100644 --- a/pkg/macos/foundation/dictionary.zig +++ b/pkg/macos/foundation/dictionary.zig @@ -17,8 +17,8 @@ pub const Dictionary = opaque { return @as(?*Dictionary, @ptrFromInt(@intFromPtr(c.CFDictionaryCreate( null, - @constCast(@ptrCast(if (keys) |slice| slice.ptr else null)), - @constCast(@ptrCast(if (values) |slice| slice.ptr else null)), + @ptrCast(@constCast(if (keys) |slice| slice.ptr else null)), + @ptrCast(@constCast(if (values) |slice| slice.ptr else null)), @intCast(if (keys) |slice| slice.len else 0), &c.kCFTypeDictionaryKeyCallBacks, &c.kCFTypeDictionaryValueCallBacks, diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 383861d62..ea37891f5 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -68,7 +68,7 @@ pub const Font = opaque { } pub fn copyTable(self: *Font, tag: FontTableTag) ?*foundation.Data { - return @constCast(@ptrCast(c.CTFontCopyTable( + return @ptrCast(@constCast(c.CTFontCopyTable( @ptrCast(self), @intFromEnum(tag), c.kCTFontTableOptionNoOptions, @@ -90,7 +90,7 @@ pub const Font = opaque { } pub fn createPathForGlyph(self: *Font, glyph: graphics.Glyph) ?*graphics.Path { - return @constCast(@ptrCast(c.CTFontCreatePathForGlyph( + return @ptrCast(@constCast(c.CTFontCreatePathForGlyph( @ptrCast(self), glyph, null, diff --git a/pkg/macos/text/line.zig b/pkg/macos/text/line.zig index 135fd8558..248f8e645 100644 --- a/pkg/macos/text/line.zig +++ b/pkg/macos/text/line.zig @@ -51,7 +51,7 @@ pub const Line = opaque { } pub fn getGlyphRuns(self: *Line) *foundation.Array { - return @constCast(@ptrCast(c.CTLineGetGlyphRuns(@ptrCast(self)))); + return @ptrCast(@constCast(c.CTLineGetGlyphRuns(@ptrCast(self)))); } }; diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig index 4bbf58a0c..7d6b437f9 100644 --- a/pkg/macos/video/display_link.zig +++ b/pkg/macos/video/display_link.zig @@ -74,7 +74,7 @@ pub const DisplayLink = opaque { callbackFn( displayLink, - @alignCast(@ptrCast(inner_userinfo)), + @ptrCast(@alignCast(inner_userinfo)), ); return c.kCVReturnSuccess; } diff --git a/pkg/oniguruma/init.zig b/pkg/oniguruma/init.zig index 933e50b5a..ea64724c2 100644 --- a/pkg/oniguruma/init.zig +++ b/pkg/oniguruma/init.zig @@ -6,7 +6,7 @@ const errors = @import("errors.zig"); /// the encodings that the program will use. pub fn init(encs: []const *Encoding) !void { _ = try errors.convertError(c.onig_initialize( - @constCast(@ptrCast(@alignCast(encs.ptr))), + @ptrCast(@alignCast(@constCast(encs.ptr))), @intCast(encs.len), )); } diff --git a/pkg/wuffs/src/jpeg.zig b/pkg/wuffs/src/jpeg.zig index c07278eed..700ba01b9 100644 --- a/pkg/wuffs/src/jpeg.zig +++ b/pkg/wuffs/src/jpeg.zig @@ -31,7 +31,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData { } var source_buffer: c.wuffs_base__io_buffer = .{ - .data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len }, + .data = .{ .ptr = @ptrCast(@constCast(data.ptr)), .len = data.len }, .meta = .{ .wi = data.len, .ri = 0, diff --git a/pkg/wuffs/src/png.zig b/pkg/wuffs/src/png.zig index 1f37bb375..d79ae5b56 100644 --- a/pkg/wuffs/src/png.zig +++ b/pkg/wuffs/src/png.zig @@ -31,7 +31,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData { } var source_buffer: c.wuffs_base__io_buffer = .{ - .data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len }, + .data = .{ .ptr = @ptrCast(@constCast(data.ptr)), .len = data.len }, .meta = .{ .wi = data.len, .ri = 0, diff --git a/src/apprt/gtk/class.zig b/src/apprt/gtk/class.zig index 4b46f8365..942666cf4 100644 --- a/src/apprt/gtk/class.zig +++ b/src/apprt/gtk/class.zig @@ -282,7 +282,7 @@ pub fn Common( fn setter(self: *Self, value: ?[:0]const u8) void { const priv = private(self); if (@field(priv, name)) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); } // We don't need to copy this because it was already diff --git a/src/apprt/gtk/class/resize_overlay.zig b/src/apprt/gtk/class/resize_overlay.zig index 9bb9a0a7c..f6e0c1442 100644 --- a/src/apprt/gtk/class/resize_overlay.zig +++ b/src/apprt/gtk/class/resize_overlay.zig @@ -172,7 +172,7 @@ pub const ResizeOverlay = extern struct { /// overlay if it is currently hidden; you must call schedule. pub fn setLabel(self: *Self, label: ?[:0]const u8) void { const priv = self.private(); - if (priv.label_text) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.label_text) |v| glib.free(@ptrCast(@constCast(v))); priv.label_text = null; if (label) |v| priv.label_text = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.label.impl.param_spec); @@ -285,7 +285,7 @@ pub const ResizeOverlay = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); if (priv.label_text) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.label_text = null; } diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 401e542e4..d49885256 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1626,7 +1626,7 @@ pub const Surface = extern struct { priv.core_surface = null; } if (priv.mouse_hover_url) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.mouse_hover_url = null; } if (priv.default_size) |v| { @@ -1642,15 +1642,15 @@ pub const Surface = extern struct { priv.min_size = null; } if (priv.pwd) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.pwd = null; } if (priv.title) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.title = null; } if (priv.title_override) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } self.clearCgroup(); @@ -1674,7 +1674,7 @@ pub const Surface = extern struct { /// title. For manually set titles see `setTitleOverride`. pub fn setTitle(self: *Self, title: ?[:0]const u8) void { const priv = self.private(); - if (priv.title) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.title) |v| glib.free(@ptrCast(@constCast(v))); priv.title = null; if (title) |v| priv.title = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec); @@ -1684,7 +1684,7 @@ pub const Surface = extern struct { /// unless this is unset (null). pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void { const priv = self.private(); - if (priv.title_override) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.title_override) |v| glib.free(@ptrCast(@constCast(v))); priv.title_override = null; if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec); @@ -1698,7 +1698,7 @@ pub const Surface = extern struct { /// Set the pwd for this surface, copies the value. pub fn setPwd(self: *Self, pwd: ?[:0]const u8) void { const priv = self.private(); - if (priv.pwd) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.pwd) |v| glib.free(@ptrCast(@constCast(v))); priv.pwd = null; if (pwd) |v| priv.pwd = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec); @@ -1783,7 +1783,7 @@ pub const Surface = extern struct { pub fn setMouseHoverUrl(self: *Self, url: ?[:0]const u8) void { const priv = self.private(); - if (priv.mouse_hover_url) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.mouse_hover_url) |v| glib.free(@ptrCast(@constCast(v))); priv.mouse_hover_url = null; if (url) |v| priv.mouse_hover_url = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.@"mouse-hover-url".impl.param_spec); diff --git a/src/apprt/gtk/class/surface_title_dialog.zig b/src/apprt/gtk/class/surface_title_dialog.zig index de36f3090..6d3bf33de 100644 --- a/src/apprt/gtk/class/surface_title_dialog.zig +++ b/src/apprt/gtk/class/surface_title_dialog.zig @@ -136,7 +136,7 @@ pub const SurfaceTitleDialog = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); if (priv.initial_value) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.initial_value = null; } diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 373507507..26b006bb6 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -270,11 +270,11 @@ pub const Tab = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); if (priv.tooltip) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.tooltip = null; } if (priv.title) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.title = null; } diff --git a/src/cli/args.zig b/src/cli/args.zig index b8f393864..c4a40acf5 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -324,7 +324,7 @@ pub fn parseIntoField( return; } const raw = field.default_value_ptr orelse break :default; - const ptr: *const field.type = @alignCast(@ptrCast(raw)); + const ptr: *const field.type = @ptrCast(@alignCast(raw)); @field(dst, field.name) = ptr.*; return; } @@ -586,7 +586,7 @@ pub fn parseAutoStruct( break :default @field(default, field.name); } else { const default_ptr = field.default_value_ptr orelse return error.InvalidValue; - const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); + const typed_ptr: *const field.type = @ptrCast(@alignCast(default_ptr)); break :default typed_ptr.*; } }; diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 27abb8657..d23510949 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -149,7 +149,7 @@ pub fn setup() void { font_config.FontDataOwnedByAtlas = false; _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( io.Fonts, - @constCast(@ptrCast(font.embedded.regular)), + @ptrCast(@constCast(font.embedded.regular)), font.embedded.regular.len, font_size, font_config, diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index 724c71be5..1121058a3 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -92,7 +92,7 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { return error.OutOfMemory; const chunks = self.chunks.ptr(base); - const ptr: [*]T = @alignCast(@ptrCast(&chunks[idx * chunk_size])); + const ptr: [*]T = @ptrCast(@alignCast(&chunks[idx * chunk_size])); return ptr[0..n]; } From 913d2dfb23a7d74de2230ef1a60d74eeed55c895 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Sep 2025 16:10:53 -0700 Subject: [PATCH 129/319] unicode: fix lookup table generation --- src/build/UnicodeTables.zig | 6 ++++++ src/unicode/Properties.zig | 8 ++------ src/unicode/lut.zig | 36 +++++++++++++++++++++------------- src/unicode/props_uucode.zig | 6 +++++- src/unicode/symbols_uucode.zig | 6 +++++- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index 9972c851a..aba3e8f24 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -21,6 +21,9 @@ pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables { .omit_frame_pointer = false, .unwind_tables = .sync, }), + + // TODO: x86_64 self-hosted crashes + .use_llvm = true, }); const symbols_exe = b.addExecutable(.{ @@ -32,6 +35,9 @@ pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables { .omit_frame_pointer = false, .unwind_tables = .sync, }), + + // TODO: x86_64 self-hosted crashes + .use_llvm = true, }); if (b.lazyDependency("uucode", .{ diff --git a/src/unicode/Properties.zig b/src/unicode/Properties.zig index b7840743a..c8c4a581c 100644 --- a/src/unicode/Properties.zig +++ b/src/unicode/Properties.zig @@ -24,13 +24,9 @@ pub fn eql(a: Properties, b: Properties) bool { // Needed for lut.Generator pub fn format( self: Properties, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, + try writer.print( \\.{{ \\ .width= {}, \\ .grapheme_boundary_class= .{s}, diff --git a/src/unicode/lut.zig b/src/unicode/lut.zig index e10c5c0b8..da90f1ee7 100644 --- a/src/unicode/lut.zig +++ b/src/unicode/lut.zig @@ -54,12 +54,14 @@ pub fn Generator( defer blocks_map.deinit(); // Our stages - var stage1 = std.ArrayList(u16).init(alloc); - defer stage1.deinit(); - var stage2 = std.ArrayList(u16).init(alloc); - defer stage2.deinit(); - var stage3 = std.ArrayList(Elem).init(alloc); - defer stage3.deinit(); + var stage1: std.ArrayList(u16) = .empty; + var stage2: std.ArrayList(u16) = .empty; + var stage3: std.ArrayList(Elem) = .empty; + defer { + stage1.deinit(alloc); + stage2.deinit(alloc); + stage3.deinit(alloc); + } var block: Block = undefined; var block_len: u16 = 0; @@ -74,7 +76,7 @@ pub fn Generator( } const idx = stage3.items.len; - try stage3.append(elem); + try stage3.append(alloc, elem); break :block_idx idx; }; @@ -96,11 +98,11 @@ pub fn Generator( u16, stage2.items.len, ) orelse return error.Stage2TooLarge; - for (block[0..block_len]) |entry| try stage2.append(entry); + for (block[0..block_len]) |entry| try stage2.append(alloc, entry); } // Map stage1 => stage2 and reset our block - try stage1.append(gop.value_ptr.*); + try stage1.append(alloc, gop.value_ptr.*); block_len = 0; } @@ -109,11 +111,11 @@ pub fn Generator( assert(stage2.items.len <= std.math.maxInt(u16)); assert(stage3.items.len <= std.math.maxInt(u16)); - const stage1_owned = try stage1.toOwnedSlice(); + const stage1_owned = try stage1.toOwnedSlice(alloc); errdefer alloc.free(stage1_owned); - const stage2_owned = try stage2.toOwnedSlice(); + const stage2_owned = try stage2.toOwnedSlice(alloc); errdefer alloc.free(stage2_owned); - const stage3_owned = try stage3.toOwnedSlice(); + const stage3_owned = try stage3.toOwnedSlice(alloc); errdefer alloc.free(stage3_owned); return .{ @@ -145,7 +147,7 @@ pub fn Tables(comptime Elem: type) type { /// Writes the lookup table as Zig to the given writer. The /// written file exports three constants: stage1, stage2, and /// stage3. These can be used to rebuild the lookup table in Zig. - pub fn writeZig(self: *const Self, writer: anytype) !void { + pub fn writeZig(self: *const Self, writer: *std.Io.Writer) !void { try writer.print( \\//! This file is auto-generated. Do not edit. \\ @@ -168,7 +170,13 @@ pub fn Tables(comptime Elem: type) type { \\ \\pub const stage3: [{}]Elem = .{{ , .{self.stage3.len}); - for (self.stage3) |entry| try writer.print("{},", .{entry}); + for (self.stage3) |entry| { + if (@typeInfo(@TypeOf(entry)) == .@"struct" and + @hasDecl(@TypeOf(entry), "format")) + try writer.print("{f},", .{entry}) + else + try writer.print("{},", .{entry}); + } try writer.writeAll( \\}; \\ }; diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index ba0511ea4..6aed7d7d5 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -84,7 +84,11 @@ pub fn main() !void { defer alloc.free(t.stage1); defer alloc.free(t.stage2); defer alloc.free(t.stage3); - try t.writeZig(std.io.getStdOut().writer()); + + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + try t.writeZig(&stdout.interface); + try stdout.end(); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig index 3da019e81..8cbd59211 100644 --- a/src/unicode/symbols_uucode.zig +++ b/src/unicode/symbols_uucode.zig @@ -30,7 +30,11 @@ pub fn main() !void { defer alloc.free(t.stage1); defer alloc.free(t.stage2); defer alloc.free(t.stage3); - try t.writeZig(std.io.getStdOut().writer()); + + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + try t.writeZig(&stdout.interface); + try stdout.end(); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ From 3770f97608409d450a1ab0364974ee7fb1c50f13 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Oct 2025 08:05:28 -0700 Subject: [PATCH 130/319] terminal: Zig 0.15, lib-vt and test-lib-vt work --- src/datastruct/lru.zig | 70 +++++--- src/datastruct/main.zig | 2 +- src/terminal/PageList.zig | 19 +- src/terminal/Parser.zig | 34 ++-- src/terminal/Screen.zig | 84 ++++----- src/terminal/Terminal.zig | 4 +- src/terminal/bitmap_allocator.zig | 4 +- src/terminal/dcs.zig | 31 ++-- src/terminal/hash_map.zig | 22 ++- src/terminal/kitty/color.zig | 11 +- src/terminal/osc.zig | 18 +- src/terminal/page.zig | 56 +++--- src/terminal/ref_counted_set.zig | 4 +- src/terminal/search.zig | 142 ++++++++------- src/terminal/stream.zig | 286 +++++++++++++++--------------- 15 files changed, 417 insertions(+), 370 deletions(-) diff --git a/src/datastruct/lru.zig b/src/datastruct/lru.zig index 7bf42e82d..1c6df69ce 100644 --- a/src/datastruct/lru.zig +++ b/src/datastruct/lru.zig @@ -33,8 +33,13 @@ pub fn HashMap( ) type { return struct { const Self = @This(); - const Map = std.HashMapUnmanaged(K, *Queue.Node, Context, max_load_percentage); - const Queue = std.DoublyLinkedList(KV); + const Queue = std.DoublyLinkedList; + const Map = std.HashMapUnmanaged( + K, + *Entry, + Context, + max_load_percentage, + ); /// Map to maintain our entries. map: Map, @@ -46,6 +51,15 @@ pub fn HashMap( /// misses will begin evicting entries. capacity: Map.Size, + const Entry = struct { + data: KV, + node: Queue.Node, + + fn fromNode(node: *Queue.Node) *Entry { + return @fieldParentPtr("node", node); + } + }; + pub const KV = struct { key: K, value: V, @@ -82,7 +96,7 @@ pub fn HashMap( var it = self.queue.first; while (it) |node| { it = node.next; - alloc.destroy(node); + alloc.destroy(Entry.fromNode(node)); } self.map.deinit(alloc); @@ -108,8 +122,8 @@ pub fn HashMap( const map_gop = try self.map.getOrPutContext(alloc, key, ctx); if (map_gop.found_existing) { // Move to end to mark as most recently used - self.queue.remove(map_gop.value_ptr.*); - self.queue.append(map_gop.value_ptr.*); + self.queue.remove(&map_gop.value_ptr.*.node); + self.queue.append(&map_gop.value_ptr.*.node); return GetOrPutResult{ .found_existing = true, @@ -122,37 +136,34 @@ pub fn HashMap( // We're evicting if our map insertion increased our capacity. const evict = self.map.count() > self.capacity; - // Get our node. If we're not evicting then we allocate a new - // node. If we are evicting then we avoid allocation by just - // reusing the node we would've evicted. - var node = if (!evict) try alloc.create(Queue.Node) else node: { + // Get our entry. If we're not evicting then we allocate a new + // entry. If we are evicting then we avoid allocation by just + // reusing the entry we would've evicted. + const entry: *Entry = if (!evict) try alloc.create(Entry) else entry: { // Our first node is the least recently used. - const least_used = self.queue.first.?; - - // Move our least recently used to the end to make - // it the most recently used. - self.queue.remove(least_used); + const least_used_node = self.queue.popFirst().?; + const least_used_entry: *Entry = .fromNode(least_used_node); // Remove the least used from the map - _ = self.map.remove(least_used.data.key); + _ = self.map.remove(least_used_entry.data.key); - break :node least_used; + break :entry least_used_entry; }; - errdefer if (!evict) alloc.destroy(node); + errdefer if (!evict) alloc.destroy(entry); - // Store our node in the map. - map_gop.value_ptr.* = node; + // Store our entry in the map. + map_gop.value_ptr.* = entry; - // Mark the node as most recently used - self.queue.append(node); + // Mark the entry as most recently used + self.queue.append(&entry.node); // Set our key - node.data.key = key; + entry.data.key = key; - return GetOrPutResult{ + return .{ .found_existing = map_gop.found_existing, - .value_ptr = &node.data.value, - .evicted = if (!evict) null else node.data, + .value_ptr = &entry.data.value, + .evicted = if (!evict) null else entry.data, }; } @@ -193,11 +204,12 @@ pub fn HashMap( var i: Map.Size = 0; while (i < delta) : (i += 1) { - const node = self.queue.first.?; - evicted[i] = node.data.value; + const node = self.queue.popFirst().?; + const entry: *Entry = .fromNode(node); + evicted[i] = entry.data.value; self.queue.remove(node); - _ = self.map.remove(node.data.key); - alloc.destroy(node); + _ = self.map.remove(entry.data.key); + alloc.destroy(entry); } self.capacity = capacity; diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 14ee0e504..5aa68555f 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -14,7 +14,7 @@ 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; -pub const SplitTree = split_tree.SplitTree; +//pub const SplitTree = split_tree.SplitTree; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 8aeb6f6dc..2f6864784 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -56,7 +56,7 @@ const std_size = Page.layout(std_capacity).total_size; /// allocator because we need memory that is zero-initialized and page-aligned. const PagePool = std.heap.MemoryPoolAligned( [std_size]u8, - std.heap.page_size_min, + .fromByteUnits(std.heap.page_size_min), ); /// List of pins, known as "tracked" pins. These are pins that are kept @@ -388,11 +388,18 @@ pub fn reset(self: *PageList) void { const page_arena = &self.pool.pages.arena; var it = page_arena.state.buffer_list.first; while (it) |node| : (it = node.next) { - // The fully allocated buffer - const alloc_buf = @as([*]u8, @ptrCast(node))[0..node.data]; + // WARN: Since HeapAllocator's BufNode is not public API, + // we have to hardcode its layout here. We do a comptime assert + // on Zig version to verify we check it on every bump. + const BufNode = struct { + data: usize, + node: std.SinglyLinkedList.Node, + }; + const buf_node: *BufNode = @fieldParentPtr("node", node); + // The fully allocated buffer + const alloc_buf = @as([*]u8, @ptrCast(buf_node))[0..buf_node.data]; // The buffer minus our header - const BufNode = @TypeOf(page_arena.state.buffer_list).Node; const data_buf = alloc_buf[@sizeOf(BufNode)..]; @memset(data_buf, 0); } @@ -2075,7 +2082,7 @@ inline fn createPageExt( else try page_alloc.alignedAlloc( u8, - std.heap.page_size_min, + .fromByteUnits(std.heap.page_size_min), layout.total_size, ); errdefer if (pooled) @@ -2676,7 +2683,7 @@ pub const EncodeUtf8Options = struct { /// predates this and is a thin wrapper around it so the tests all live there. pub fn encodeUtf8( self: *const PageList, - writer: anytype, + writer: *std.Io.Writer, opts: EncodeUtf8Options, ) anyerror!void { // We don't currently use self at all. There is an argument that this diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 05cbe7957..ca2fd3718 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -97,13 +97,9 @@ pub const Action = union(enum) { // Implement formatter for logging pub fn format( self: CSI, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, "ESC [ {s} {any} {c}", .{ + try writer.print("ESC [ {s} {any} {c}", .{ self.intermediates, self.params, self.final, @@ -118,13 +114,9 @@ pub const Action = union(enum) { // Implement formatter for logging pub fn format( self: ESC, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, "ESC {s} {c}", .{ + try writer.print("ESC {s} {c}", .{ self.intermediates, self.final, }); @@ -142,11 +134,8 @@ pub const Action = union(enum) { // print out custom formats for some of our primitives. pub fn format( self: Action, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; const T = Action; const info = @typeInfo(T).@"union"; @@ -162,21 +151,20 @@ pub const Action = union(enum) { const value = @field(self, u_field.name); switch (@TypeOf(value)) { // Unicode - u21 => try std.fmt.format(writer, "'{u}' (U+{X})", .{ value, value }), + u21 => try writer.print("'{u}' (U+{X})", .{ value, value }), // Byte - u8 => try std.fmt.format(writer, "0x{x}", .{value}), + u8 => try writer.print("0x{x}", .{value}), // Note: we don't do ASCII (u8) because there are a lot // of invisible characters we don't want to handle right // now. // All others do the default behavior - else => try std.fmt.formatType( - @field(self, u_field.name), + else => try writer.printValue( "any", - opts, - writer, + .{}, + @field(self, u_field.name), 3, ), } @@ -391,7 +379,7 @@ inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { // We only allow colon or mixed separators for the 'm' command. if (c != 'm' and self.params_sep.count() > 0) { log.warn( - "CSI colon or mixed separators only allowed for 'm' command, got: {}", + "CSI colon or mixed separators only allowed for 'm' command, got: {f}", .{result}, ); break :csi_dispatch null; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0c60dcec8..a98407af7 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2168,17 +2168,21 @@ pub const SelectionString = struct { /// Returns the raw text associated with a selection. This will unwrap /// soft-wrapped edges. The returned slice is owned by the caller and allocated /// using alloc, not the allocator associated with the screen (unless they match). -pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ![:0]const u8 { +pub fn selectionString( + self: *Screen, + alloc: Allocator, + opts: SelectionString, +) ![:0]const u8 { // Use an ArrayList so that we can grow the array as we go. We // build an initial capacity of just our rows in our selection times // columns. It can be more or less based on graphemes, newlines, etc. - var strbuilder = std.ArrayList(u8).init(alloc); - defer strbuilder.deinit(); + var strbuilder: std.ArrayList(u8) = .empty; + defer strbuilder.deinit(alloc); // If we're building a stringmap, create our builder for the pins. const MapBuilder = std.ArrayList(Pin); - var mapbuilder: ?MapBuilder = if (opts.map != null) MapBuilder.init(alloc) else null; - defer if (mapbuilder) |*b| b.deinit(); + var mapbuilder: ?MapBuilder = if (opts.map != null) .empty else null; + defer if (mapbuilder) |*b| b.deinit(alloc); const sel_ordered = opts.sel.ordered(self, .forward); const sel_start: Pin = start: { @@ -2235,9 +2239,9 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! const raw: u21 = if (cell.hasText()) cell.content.codepoint else 0; const char = if (raw > 0) raw else ' '; const encode_len = try std.unicode.utf8Encode(char, &buf); - try strbuilder.appendSlice(buf[0..encode_len]); + try strbuilder.appendSlice(alloc, buf[0..encode_len]); if (mapbuilder) |*b| { - for (0..encode_len) |_| try b.append(.{ + for (0..encode_len) |_| try b.append(alloc, .{ .node = chunk.node, .y = @intCast(y), .x = @intCast(x), @@ -2248,9 +2252,9 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! 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]); + try strbuilder.appendSlice(alloc, buf[0..encode_len]); if (mapbuilder) |*b| { - for (0..encode_len) |_| try b.append(.{ + for (0..encode_len) |_| try b.append(alloc, .{ .node = chunk.node, .y = @intCast(y), .x = @intCast(x), @@ -2265,8 +2269,8 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! if (!is_final_row and (!row.wrap or sel_ordered.rectangle)) { - try strbuilder.append('\n'); - if (mapbuilder) |*b| try b.append(.{ + try strbuilder.append(alloc, '\n'); + if (mapbuilder) |*b| try b.append(alloc, .{ .node = chunk.node, .y = @intCast(y), .x = chunk.node.data.size.cols - 1, @@ -2281,11 +2285,11 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! // If we have a mapbuilder, we need to setup our string map. if (mapbuilder) |*b| { - var strclone = try strbuilder.clone(); - defer strclone.deinit(); - const str = try strclone.toOwnedSliceSentinel(0); + var strclone = try strbuilder.clone(alloc); + defer strclone.deinit(alloc); + const str = try strclone.toOwnedSliceSentinel(alloc, 0); errdefer alloc.free(str); - const map = try b.toOwnedSlice(); + const map = try b.toOwnedSlice(alloc); errdefer alloc.free(map); opts.map.?.* = .{ .string = str, .map = map }; } @@ -2306,7 +2310,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! const i = strbuilder.items.len; strbuilder.items.len += trimmed.len; std.mem.copyForwards(u8, strbuilder.items[i..], trimmed); - try strbuilder.append('\n'); + try strbuilder.append(alloc, '\n'); } // Remove all trailing newlines @@ -2317,7 +2321,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! } // Get our final string - const string = try strbuilder.toOwnedSliceSentinel(0); + const string = try strbuilder.toOwnedSliceSentinel(alloc, 0); errdefer alloc.free(string); return string; @@ -2902,7 +2906,7 @@ pub fn promptPath( /// one byte at a time. pub fn dumpString( self: *const Screen, - writer: anytype, + writer: *std.Io.Writer, opts: PageList.EncodeUtf8Options, ) anyerror!void { try self.pages.encodeUtf8(writer, opts); @@ -2915,10 +2919,10 @@ pub fn dumpStringAlloc( alloc: Allocator, tl: point.Point, ) ![]const u8 { - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try self.dumpString(builder.writer(), .{ + try self.dumpString(&builder.writer, .{ .tl = self.pages.getTopLeft(tl), .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, .unwrap = false, @@ -2934,10 +2938,10 @@ pub fn dumpStringAllocUnwrapped( alloc: Allocator, tl: point.Point, ) ![]const u8 { - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try self.dumpString(builder.writer(), .{ + try self.dumpString(&builder.writer, .{ .tl = self.pages.getTopLeft(tl), .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, .unwrap = true, @@ -9030,33 +9034,33 @@ test "Screen UTF8 cell map with newlines" { var cell_map = Page.CellMap.init(alloc); defer cell_map.deinit(); - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try s.dumpString(builder.writer(), .{ + try s.dumpString(&builder.writer, .{ .tl = s.pages.getTopLeft(.screen), .br = s.pages.getBottomRight(.screen), .cell_map = &cell_map, }); - try testing.expectEqual(7, builder.items.len); - try testing.expectEqualStrings("A\n\nB\n\nC", builder.items); - try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqual(7, builder.written().len); + try testing.expectEqualStrings("A\n\nB\n\nC", builder.written()); + try testing.expectEqual(builder.written().len, cell_map.map.items.len); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 0, - }, cell_map.items[0]); + }, cell_map.map.items[0]); try testing.expectEqual(Page.CellMapEntry{ .x = 1, .y = 0, - }, cell_map.items[1]); + }, cell_map.map.items[1]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 1, - }, cell_map.items[2]); + }, cell_map.map.items[2]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 2, - }, cell_map.items[3]); + }, cell_map.map.items[3]); } test "Screen UTF8 cell map with blank prefix" { @@ -9068,32 +9072,32 @@ test "Screen UTF8 cell map with blank prefix" { s.cursorAbsolute(2, 1); try s.testWriteString("B"); - var cell_map = Page.CellMap.init(alloc); + var cell_map: Page.CellMap = .init(alloc); defer cell_map.deinit(); - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try s.dumpString(builder.writer(), .{ + try s.dumpString(&builder.writer, .{ .tl = s.pages.getTopLeft(.screen), .br = s.pages.getBottomRight(.screen), .cell_map = &cell_map, }); - try testing.expectEqualStrings("\n B", builder.items); - try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqualStrings("\n B", builder.written()); + try testing.expectEqual(builder.written().len, cell_map.map.items.len); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 0, - }, cell_map.items[0]); + }, cell_map.map.items[0]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 1, - }, cell_map.items[1]); + }, cell_map.map.items[1]); try testing.expectEqual(Page.CellMapEntry{ .x = 1, .y = 1, - }, cell_map.items[2]); + }, cell_map.map.items[2]); try testing.expectEqual(Page.CellMapEntry{ .x = 2, .y = 1, - }, cell_map.items[3]); + }, cell_map.map.items[3]); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9857d4798..d15f2deb3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -223,7 +223,7 @@ pub fn init( .left = 0, .right = cols - 1, }, - .pwd = std.ArrayList(u8).init(alloc), + .pwd = .empty, .modes = .{ .values = opts.default_modes, .default = opts.default_modes, @@ -235,7 +235,7 @@ pub fn deinit(self: *Terminal, alloc: Allocator) void { self.tabstops.deinit(alloc); self.screen.deinit(); self.secondary_screen.deinit(); - self.pwd.deinit(); + self.pwd.deinit(alloc); self.* = undefined; } diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index 1121058a3..894172b4c 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -34,7 +34,7 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { assert(std.math.isPowerOfTwo(chunk_size)); } - pub const base_align = @alignOf(u64); + pub const base_align: std.mem.Alignment = .fromByteUnits(@alignOf(u64)); pub const bitmap_bit_size = @bitSizeOf(u64); /// The bitmap of available chunks. Each bit represents a chunk. A @@ -49,7 +49,7 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { /// Initialize the allocator map with a given buf and memory layout. pub fn init(buf: OffsetBuf, l: Layout) Self { - assert(@intFromPtr(buf.start()) % base_align == 0); + assert(base_align.check(@intFromPtr(buf.start()))); // Initialize our bitmaps to all 1s to note that all chunks are free. const bitmap = buf.member(u64, l.bitmap_start); diff --git a/src/terminal/dcs.zig b/src/terminal/dcs.zig index e4d0f3de2..4694fc457 100644 --- a/src/terminal/dcs.zig +++ b/src/terminal/dcs.zig @@ -83,7 +83,7 @@ pub const Handler = struct { // https://github.com/mitchellh/ghostty/issues/517 'q' => .{ .state = .{ - .xtgettcap = try std.ArrayList(u8).initCapacity( + .xtgettcap = try .initCapacity( alloc, 128, // Arbitrary choice ), @@ -134,11 +134,11 @@ pub const Handler = struct { } else unreachable, .xtgettcap => |*list| { - if (list.items.len >= self.max_bytes) { + if (list.written().len >= self.max_bytes) { return error.OutOfMemory; } - try list.append(byte); + try list.writer.writeByte(byte); }, .decrqss => |*buffer| { @@ -170,11 +170,12 @@ pub const Handler = struct { break :tmux .{ .tmux = .{ .exit = {} } }; } else unreachable, - .xtgettcap => |list| xtgettcap: { - for (list.items, 0..) |b, i| { - list.items[i] = std.ascii.toUpper(b); - } - break :xtgettcap .{ .xtgettcap = .{ .data = list } }; + .xtgettcap => |*list| xtgettcap: { + // Note: purposely do not deinit our state here because + // we copy it into the resulting command. + const items = list.written(); + for (items, 0..) |b, i| items[i] = std.ascii.toUpper(b); + break :xtgettcap .{ .xtgettcap = .{ .data = list.* } }; }, .decrqss => |buffer| .{ .decrqss = switch (buffer.len) { @@ -216,8 +217,8 @@ pub const Command = union(enum) { else void, - pub fn deinit(self: Command) void { - switch (self) { + pub fn deinit(self: *Command) void { + switch (self.*) { .xtgettcap => |*v| v.data.deinit(), .decrqss => {}, .tmux => {}, @@ -225,16 +226,16 @@ pub const Command = union(enum) { } pub const XTGETTCAP = struct { - data: std.ArrayList(u8), + data: std.Io.Writer.Allocating, i: usize = 0, /// Returns the next terminfo key being requested and null /// when there are no more keys. The returned value is NOT hex-decoded /// because we expect to use a comptime lookup table. pub fn next(self: *XTGETTCAP) ?[]const u8 { - if (self.i >= self.data.items.len) return null; - - var rem = self.data.items[self.i..]; + const items = self.data.written(); + if (self.i >= items.len) return null; + var rem = items[self.i..]; const idx = std.mem.indexOf(u8, rem, ";") orelse rem.len; // Note that if we're at the end, idx + 1 is len + 1 so we're over @@ -271,7 +272,7 @@ const State = union(enum) { ignore: void, /// XTGETTCAP - xtgettcap: std.ArrayList(u8), + xtgettcap: std.Io.Writer.Allocating, /// DECRQSS decrqss: struct { diff --git a/src/terminal/hash_map.zig b/src/terminal/hash_map.zig index 9a16be3b2..23b10950e 100644 --- a/src/terminal/hash_map.zig +++ b/src/terminal/hash_map.zig @@ -88,7 +88,7 @@ pub fn OffsetHashMap( /// Initialize a new HashMap with the given capacity and backing /// memory. The backing memory must be aligned to base_align. pub fn init(buf: OffsetBuf, l: Layout) Self { - assert(@intFromPtr(buf.start()) % base_align == 0); + assert(base_align.check(@intFromPtr(buf.start()))); const m = Unmanaged.init(buf, l); return .{ .metadata = getOffset( @@ -124,7 +124,11 @@ fn HashMapUnmanaged( const header_align = @alignOf(Header); const key_align = if (@sizeOf(K) == 0) 1 else @alignOf(K); const val_align = if (@sizeOf(V) == 0) 1 else @alignOf(V); - const base_align = @max(header_align, key_align, val_align); + const base_align: mem.Alignment = .fromByteUnits(@max( + header_align, + key_align, + val_align, + )); // This is actually a midway pointer to the single buffer containing // a `Header` field, the `Metadata`s and `Entry`s. @@ -287,7 +291,7 @@ fn HashMapUnmanaged( /// Initialize a hash map with a given capacity and a buffer. The /// buffer must fit within the size defined by `layoutForCapacity`. pub fn init(buf: OffsetBuf, layout: Layout) Self { - assert(@intFromPtr(buf.start()) % base_align == 0); + assert(base_align.check(@intFromPtr(buf.start()))); // Get all our main pointers const metadata_buf = buf.rebase(@sizeOf(Header)); @@ -862,7 +866,11 @@ fn HashMapUnmanaged( // Our total memory size required is the end of our values // aligned to the base required alignment. - const total_size = std.mem.alignForward(usize, vals_end, base_align); + const total_size = std.mem.alignForward( + usize, + vals_end, + base_align.toByteUnits(), + ); // The offsets we actually store in the map are from the // metadata pointer so that we can use self.metadata as @@ -1126,15 +1134,15 @@ test "HashMap put and remove loop in random order" { defer alloc.free(buf); var map = Map.init(.init(buf), layout); - var keys = std.ArrayList(u32).init(alloc); - defer keys.deinit(); + var keys: std.ArrayList(u32) = .empty; + defer keys.deinit(alloc); const size = 32; const iterations = 100; var i: u32 = 0; while (i < size) : (i += 1) { - try keys.append(i); + try keys.append(alloc, i); } var prng = std.Random.DefaultPrng.init(0); const random = prng.random(); diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig index b23e30ad8..099002f39 100644 --- a/src/terminal/kitty/color.zig +++ b/src/terminal/kitty/color.zig @@ -42,13 +42,8 @@ pub const Kind = union(enum) { pub fn format( self: Kind, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - switch (self) { .palette => |p| try writer.print("{d}", .{p}), .special => |s| try writer.print("{s}", .{@tagName(s)}), @@ -61,11 +56,11 @@ test "OSC: kitty color protocol kind string" { var buf: [256]u8 = undefined; { - const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind{ .special = .foreground }}); + const actual = try std.fmt.bufPrint(&buf, "{f}", .{Kind{ .special = .foreground }}); try testing.expectEqualStrings("foreground", actual); } { - const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind{ .palette = 42 }}); + const actual = try std.fmt.bufPrint(&buf, "{f}", .{Kind{ .palette = 42 }}); try testing.expectEqualStrings("42", actual); } } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 800257c3d..d244310bb 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -475,7 +475,7 @@ pub const Parser = struct { // Some commands have their own memory management we need to clear. switch (self.command) { - .kitty_color_protocol => |*v| v.list.deinit(), + .kitty_color_protocol => |*v| v.list.deinit(self.alloc.?), .color_operation => |*v| v.requests.deinit(self.alloc.?), else => {}, } @@ -821,15 +821,15 @@ pub const Parser = struct { .@"21" => switch (c) { ';' => kitty: { - const alloc = self.alloc orelse { + if (self.alloc == null) { log.info("OSC 21 requires an allocator, but none was provided", .{}); self.state = .invalid; break :kitty; - }; + } self.command = .{ .kitty_color_protocol = .{ - .list = std.ArrayList(kitty_color.OSC.Request).init(alloc), + .list = .empty, }, }; @@ -1553,18 +1553,22 @@ pub const Parser = struct { return; } + // Asserted when the command is set to kitty_color_protocol + // that we have an allocator. + const alloc = self.alloc.?; + if (kind == .key_only or value.len == 0) { - v.list.append(.{ .reset = key }) catch |err| { + v.list.append(alloc, .{ .reset = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); return; }; } else if (mem.eql(u8, "?", value)) { - v.list.append(.{ .query = key }) catch |err| { + v.list.append(alloc, .{ .query = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); return; }; } else { - v.list.append(.{ + v.list.append(alloc, .{ .set = .{ .key = key, .color = RGB.parse(value) catch |err| switch (err) { diff --git a/src/terminal/page.zig b/src/terminal/page.zig index b2fe993d2..331168a27 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -86,7 +86,7 @@ pub const Page = struct { assert(std.heap.page_size_min % @max( @alignOf(Row), @alignOf(Cell), - style.Set.base_align, + style.Set.base_align.toByteUnits(), ) == 0); } @@ -1528,7 +1528,21 @@ pub const Page = struct { }; /// See cell_map - pub const CellMap = std.ArrayList(CellMapEntry); + pub const CellMap = struct { + alloc: Allocator, + map: std.ArrayList(CellMapEntry), + + pub fn init(alloc: Allocator) CellMap { + return .{ + .alloc = alloc, + .map = .empty, + }; + } + + pub fn deinit(self: *CellMap) void { + self.map.deinit(self.alloc); + } + }; /// The x/y coordinate of a single cell in the cell map. pub const CellMapEntry = struct { @@ -1547,7 +1561,7 @@ pub const Page = struct { /// it makes it easier to test input contents. pub fn encodeUtf8( self: *const Page, - writer: anytype, + writer: *std.Io.Writer, opts: EncodeUtf8Options, ) anyerror!EncodeUtf8Options.TrailingUtf8State { var blank_rows: usize = opts.preceding.rows; @@ -1583,7 +1597,7 @@ pub const Page = struct { // This is tested in Screen.zig, i.e. one test is // "cell map with newlines" if (opts.cell_map) |cell_map| { - try cell_map.append(.{ + try cell_map.map.append(cell_map.alloc, .{ .x = last_x, .y = @intCast(y - blank_rows + i - 1), }); @@ -1618,9 +1632,9 @@ pub const Page = struct { continue; } if (blank_cells > 0) { - try writer.writeByteNTimes(' ', blank_cells); + try writer.splatByteAll(' ', blank_cells); if (opts.cell_map) |cell_map| { - for (0..blank_cells) |i| try cell_map.append(.{ + for (0..blank_cells) |i| try cell_map.map.append(cell_map.alloc, .{ .x = @intCast(x - blank_cells + i), .y = y, }); @@ -1634,7 +1648,7 @@ pub const Page = struct { try writer.print("{u}", .{cell.content.codepoint}); if (opts.cell_map) |cell_map| { last_x = x + 1; - try cell_map.append(.{ + try cell_map.map.append(cell_map.alloc, .{ .x = x, .y = y, }); @@ -1645,7 +1659,7 @@ pub const Page = struct { try writer.print("{u}", .{cell.content.codepoint}); if (opts.cell_map) |cell_map| { last_x = x + 1; - try cell_map.append(.{ + try cell_map.map.append(cell_map.alloc, .{ .x = x, .y = y, }); @@ -1653,7 +1667,7 @@ pub const Page = struct { for (self.lookupGrapheme(cell).?) |cp| { try writer.print("{u}", .{cp}); - if (opts.cell_map) |cell_map| try cell_map.append(.{ + if (opts.cell_map) |cell_map| try cell_map.map.append(cell_map.alloc, .{ .x = x, .y = y, }); @@ -1743,25 +1757,25 @@ pub const Page = struct { const dirty_end: usize = dirty_start + (dirty_usize_length * @sizeOf(usize)); const styles_layout: style.Set.Layout = .init(cap.styles); - const styles_start = alignForward(usize, dirty_end, style.Set.base_align); + const styles_start = alignForward(usize, dirty_end, style.Set.base_align.toByteUnits()); const styles_end = styles_start + styles_layout.total_size; const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes); - const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align); + const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align.toByteUnits()); const grapheme_alloc_end = grapheme_alloc_start + grapheme_alloc_layout.total_size; const grapheme_count = @divFloor(cap.grapheme_bytes, grapheme_chunk); const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count)); - const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align); + const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align.toByteUnits()); const grapheme_map_end = grapheme_map_start + grapheme_map_layout.total_size; const string_layout = StringAlloc.layout(cap.string_bytes); - const string_start = alignForward(usize, grapheme_map_end, StringAlloc.base_align); + const string_start = alignForward(usize, grapheme_map_end, StringAlloc.base_align.toByteUnits()); const string_end = string_start + string_layout.total_size; const hyperlink_count = @divFloor(cap.hyperlink_bytes, @sizeOf(hyperlink.Set.Item)); const hyperlink_set_layout: hyperlink.Set.Layout = .init(@intCast(hyperlink_count)); - const hyperlink_set_start = alignForward(usize, string_end, hyperlink.Set.base_align); + const hyperlink_set_start = alignForward(usize, string_end, hyperlink.Set.base_align.toByteUnits()); const hyperlink_set_end = hyperlink_set_start + hyperlink_set_layout.total_size; const hyperlink_map_count: u32 = count: { @@ -1773,7 +1787,7 @@ pub const Page = struct { break :count std.math.ceilPowerOfTwoAssert(u32, mult); }; const hyperlink_map_layout = hyperlink.Map.layout(hyperlink_map_count); - const hyperlink_map_start = alignForward(usize, hyperlink_set_end, hyperlink.Map.base_align); + const hyperlink_map_start = alignForward(usize, hyperlink_set_end, hyperlink.Map.base_align.toByteUnits()); const hyperlink_map_end = hyperlink_map_start + hyperlink_map_layout.total_size; const total_size = alignForward(usize, hyperlink_map_end, std.heap.page_size_min); @@ -1867,12 +1881,12 @@ pub const Capacity = struct { // for rows & cells (which will allow us to calculate the number of // rows we can fit at a certain column width) we need to layout the // "meta" members of the page (i.e. everything else) from the end. - const hyperlink_map_start = alignBackward(usize, layout.total_size - layout.hyperlink_map_layout.total_size, hyperlink.Map.base_align); - const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align); - const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align); - const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align); - const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align); - const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align); + const hyperlink_map_start = alignBackward(usize, layout.total_size - layout.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits()); + const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits()); + const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits()); + const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits()); + const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits()); + const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align.toByteUnits()); // The size per row is: // - The row metadata itself diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index 153e331a6..e07de4e97 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -59,12 +59,12 @@ pub fn RefCountedSet( return struct { const Self = @This(); - pub const base_align = @max( + pub const base_align: std.mem.Alignment = .fromByteUnits(@max( @alignOf(Context), @alignOf(Layout), @alignOf(Item), @alignOf(Id), - ); + )); /// Set item pub const Item = struct { diff --git a/src/terminal/search.zig b/src/terminal/search.zig index b3c6494a3..d9f6c5663 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -55,7 +55,7 @@ pub const PageListSearch = struct { needle: []const u8, ) Allocator.Error!PageListSearch { var window = try SlidingWindow.init(alloc, needle); - errdefer window.deinit(alloc); + errdefer window.deinit(); return .{ .list = list, @@ -63,16 +63,13 @@ pub const PageListSearch = struct { }; } - pub fn deinit(self: *PageListSearch, alloc: Allocator) void { - self.window.deinit(alloc); + pub fn deinit(self: *PageListSearch) void { + self.window.deinit(); } /// Find the next match for the needle in the pagelist. This returns /// null when there are no more matches. - pub fn next( - self: *PageListSearch, - alloc: Allocator, - ) Allocator.Error!?Selection { + pub fn next(self: *PageListSearch) Allocator.Error!?Selection { // Try to search for the needle in the window. If we find a match // then we can return that and we're done. if (self.window.next()) |sel| return sel; @@ -89,7 +86,7 @@ pub const PageListSearch = struct { // until we find a match or we reach the end of the pagelist. // This append then next pattern limits memory usage of the window. while (node_) |node| : (node_ = node.next) { - try self.window.append(alloc, node); + try self.window.append(node); if (self.window.next()) |sel| return sel; } @@ -115,6 +112,14 @@ pub const PageListSearch = struct { /// and repeat the process. This will always maintain the minimum /// required memory to search for the needle. const SlidingWindow = struct { + /// The allocator to use for all the data within this window. We + /// store this rather than passing it around because its already + /// part of multiple elements (eg. Meta's CellMap) and we want to + /// ensure we always use a consistent allocator. Additionally, only + /// a small amount of sliding windows are expected to be in use + /// at any one time so the memory overhead isn't that large. + alloc: Allocator, + /// The data buffer is a circular buffer of u8 that contains the /// encoded page text that we can use to search for the needle. data: DataBuf, @@ -163,6 +168,7 @@ const SlidingWindow = struct { errdefer alloc.free(overlap_buf); return .{ + .alloc = alloc, .data = data, .meta = meta, .needle = needle, @@ -170,13 +176,13 @@ const SlidingWindow = struct { }; } - pub fn deinit(self: *SlidingWindow, alloc: Allocator) void { - alloc.free(self.overlap_buf); - self.data.deinit(alloc); + pub fn deinit(self: *SlidingWindow) void { + self.alloc.free(self.overlap_buf); + self.data.deinit(self.alloc); var meta_it = self.meta.iterator(.forward); while (meta_it.next()) |meta| meta.deinit(); - self.meta.deinit(alloc); + self.meta.deinit(self.alloc); } /// Clear all data but retain allocated capacity. @@ -206,7 +212,10 @@ const SlidingWindow = struct { // Search the first slice for the needle. if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { - return self.selection(idx, self.needle.len); + return self.selection( + idx, + self.needle.len, + ); } // Search the overlap buffer for the needle. @@ -244,7 +253,10 @@ const SlidingWindow = struct { // Search the last slice for the needle. if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { - return self.selection(slices[0].len + idx, self.needle.len); + return self.selection( + slices[0].len + idx, + self.needle.len, + ); } // No match. We keep `needle.len - 1` bytes available to @@ -254,15 +266,15 @@ const SlidingWindow = struct { var saved: usize = 0; while (meta_it.next()) |meta| { const needed = self.needle.len - 1 - saved; - if (meta.cell_map.items.len >= needed) { + if (meta.cell_map.map.items.len >= needed) { // We save up to this meta. We set our data offset // to exactly where it needs to be to continue // searching. - self.data_offset = meta.cell_map.items.len - needed; + self.data_offset = meta.cell_map.map.items.len - needed; break; } - saved += meta.cell_map.items.len; + saved += meta.cell_map.map.items.len; } else { // If we exited the while loop naturally then we // never got the amount we needed and so there is @@ -284,7 +296,7 @@ const SlidingWindow = struct { var prune_data_len: usize = 0; for (0..prune_count) |_| { const meta = meta_it.next().?; - prune_data_len += meta.cell_map.items.len; + prune_data_len += meta.cell_map.map.items.len; meta.deinit(); } self.meta.deleteOldest(prune_count); @@ -384,16 +396,16 @@ const SlidingWindow = struct { // meta_i is the index we expect to find the match in the // cell map within this meta if it contains it. const meta_i = idx - offset.*; - if (meta_i >= meta.cell_map.items.len) { + if (meta_i >= meta.cell_map.map.items.len) { // This meta doesn't contain the match. This means we // can also prune this set of data because we only look // forward. - offset.* += meta.cell_map.items.len; + offset.* += meta.cell_map.map.items.len; continue; } // We found the meta that contains the start of the match. - const map = meta.cell_map.items[meta_i]; + const map = meta.cell_map.map.items[meta_i]; return .{ .node = meta.node, .y = map.y, @@ -411,13 +423,15 @@ const SlidingWindow = struct { /// via a search (via next()). pub fn append( self: *SlidingWindow, - alloc: Allocator, node: *PageList.List.Node, ) Allocator.Error!void { // Initialize our metadata for the node. var meta: Meta = .{ .node = node, - .cell_map = .init(alloc), + .cell_map = .{ + .alloc = self.alloc, + .map = .empty, + }, }; errdefer meta.deinit(); @@ -425,27 +439,27 @@ const SlidingWindow = struct { // temporary memory, and then copy it into our circular buffer. // In the future, we should benchmark and see if we can encode // directly into the circular buffer. - var encoded: std.ArrayListUnmanaged(u8) = .{}; - defer encoded.deinit(alloc); + var encoded: std.Io.Writer.Allocating = .init(self.alloc); + defer encoded.deinit(); // Encode the page into the buffer. const page: *const Page = &meta.node.data; _ = page.encodeUtf8( - encoded.writer(alloc), + &encoded.writer, .{ .cell_map = &meta.cell_map }, ) catch { // writer uses anyerror but the only realistic error on // an ArrayList is out of memory. return error.OutOfMemory; }; - assert(meta.cell_map.items.len == encoded.items.len); + assert(meta.cell_map.map.items.len == encoded.written().len); // Ensure our buffers are big enough to store what we need. - try self.data.ensureUnusedCapacity(alloc, encoded.items.len); - try self.meta.ensureUnusedCapacity(alloc, 1); + try self.data.ensureUnusedCapacity(self.alloc, encoded.written().len); + try self.meta.ensureUnusedCapacity(self.alloc, 1); // Append our new node to the circular buffer. - try self.data.appendSlice(encoded.items); + try self.data.appendSlice(encoded.written()); try self.meta.append(meta); self.assertIntegrity(); @@ -462,7 +476,7 @@ const SlidingWindow = struct { // Integrity check: verify our data matches our metadata exactly. var meta_it = self.meta.iterator(.forward); var data_len: usize = 0; - while (meta_it.next()) |m| data_len += m.cell_map.items.len; + while (meta_it.next()) |m| data_len += m.cell_map.map.items.len; assert(data_len == self.data.len()); // Integrity check: verify our data offset is within bounds. @@ -480,11 +494,11 @@ test "PageListSearch single page" { try testing.expect(s.pages.pages.first == s.pages.pages.last); var search = try PageListSearch.init(alloc, &s.pages, "boo!"); - defer search.deinit(alloc); + defer search.deinit(); // We should be able to find two matches. { - const sel = (try search.next(alloc)).?; + const sel = (try search.next()).?; try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 0, @@ -495,7 +509,7 @@ test "PageListSearch single page" { } }, s.pages.pointFromPin(.active, sel.end()).?); } { - const sel = (try search.next(alloc)).?; + const sel = (try search.next()).?; try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, @@ -505,8 +519,8 @@ test "PageListSearch single page" { .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); } - try testing.expect((try search.next(alloc)) == null); - try testing.expect((try search.next(alloc)) == null); + try testing.expect((try search.next()) == null); + try testing.expect((try search.next()) == null); } test "SlidingWindow empty on init" { @@ -514,7 +528,7 @@ test "SlidingWindow empty on init" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "boo!"); - defer w.deinit(alloc); + defer w.deinit(); try testing.expectEqual(0, w.data.len()); try testing.expectEqual(0, w.meta.len()); } @@ -524,7 +538,7 @@ test "SlidingWindow single append" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "boo!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -533,7 +547,7 @@ test "SlidingWindow single append" { // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); + try w.append(node); // We should be able to find two matches. { @@ -567,7 +581,7 @@ test "SlidingWindow single append no match" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "nope!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -576,7 +590,7 @@ test "SlidingWindow single append no match" { // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); + try w.append(node); // No matches try testing.expect(w.next() == null); @@ -591,7 +605,7 @@ test "SlidingWindow two pages" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "boo!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -609,8 +623,8 @@ test "SlidingWindow two pages" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find two matches { @@ -644,7 +658,7 @@ test "SlidingWindow two pages match across boundary" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "hello, world"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -661,8 +675,8 @@ test "SlidingWindow two pages match across boundary" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find a match { @@ -688,7 +702,7 @@ test "SlidingWindow two pages no match prunes first page" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "nope!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -706,8 +720,8 @@ test "SlidingWindow two pages no match prunes first page" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find nothing try testing.expect(w.next() == null); @@ -737,18 +751,18 @@ test "SlidingWindow two pages no match keeps both pages" { try s.testWriteString("hello. boo!"); // Imaginary needle for search. Doesn't match! - var needle_list = std.ArrayList(u8).init(alloc); - defer needle_list.deinit(); - try needle_list.appendNTimes('x', first_page_rows * s.pages.cols); + var needle_list: std.ArrayList(u8) = .empty; + defer needle_list.deinit(alloc); + try needle_list.appendNTimes(alloc, 'x', first_page_rows * s.pages.cols); const needle: []const u8 = needle_list.items; var w = try SlidingWindow.init(alloc, needle); - defer w.deinit(alloc); + defer w.deinit(); // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find nothing try testing.expect(w.next() == null); @@ -763,7 +777,7 @@ test "SlidingWindow single append across circular buffer boundary" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "abc"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -776,8 +790,8 @@ test "SlidingWindow single append across circular buffer boundary" { // our implementation changes our test will fail. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node); + try w.append(node); + try w.append(node); { // No wrap around yet const slices = w.data.getPtrSlice(0, w.data.len()); @@ -793,7 +807,7 @@ test "SlidingWindow single append across circular buffer boundary" { w.needle = "boo"; // Add new page, now wraps - try w.append(alloc, node); + try w.append(node); { const slices = w.data.getPtrSlice(0, w.data.len()); try testing.expect(slices[0].len > 0); @@ -818,7 +832,7 @@ test "SlidingWindow single append match on boundary" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "abcd"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -831,8 +845,8 @@ test "SlidingWindow single append match on boundary" { // our implementation changes our test will fail. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node); + try w.append(node); + try w.append(node); { // No wrap around yet const slices = w.data.getPtrSlice(0, w.data.len()); @@ -848,7 +862,7 @@ test "SlidingWindow single append match on boundary" { w.needle = "boo!"; // Add new page, now wraps - try w.append(alloc, node); + try w.append(node); { const slices = w.data.getPtrSlice(0, w.data.len()); try testing.expect(slices[0].len > 0); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index db43aae47..c85e72f0f 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -288,13 +288,13 @@ pub fn Stream(comptime Handler: type) type { for (actions) |action_opt| { const action = action_opt orelse continue; - if (comptime debug) log.info("action: {}", .{action}); + if (comptime debug) log.info("action: {f}", .{action}); // If this handler handles everything manually then we do nothing // if it can be processed. if (@hasDecl(T, "handleManually")) { const processed = self.handler.handleManually(action) catch |err| err: { - log.warn("error handling action manually err={} action={}", .{ + log.warn("error handling action manually err={} action={f}", .{ err, action, }); @@ -341,7 +341,7 @@ pub fn Stream(comptime Handler: type) type { pub inline fn execute(self: *Self, c: u8) !void { const c0: ansi.C0 = @enumFromInt(c); - if (comptime debug) log.info("execute: {}", .{c0}); + if (comptime debug) log.info("execute: {f}", .{c0}); switch (c0) { // We ignore SOH/STX: https://github.com/microsoft/terminal/issues/10786 .NUL, .SOH, .STX => {}, @@ -399,12 +399,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor up command: {}", .{input}); + log.warn("invalid cursor up command: {f}", .{input}); return; }, }, false, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI A with intermediates: {s}", @@ -419,12 +419,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor down command: {}", .{input}); + log.warn("invalid cursor down command: {f}", .{input}); return; }, }, false, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI B with intermediates: {s}", @@ -439,11 +439,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor right command: {}", .{input}); + log.warn("invalid cursor right command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI C with intermediates: {s}", @@ -458,11 +458,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor left command: {}", .{input}); + log.warn("invalid cursor left command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI D with intermediates: {s}", @@ -477,12 +477,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor up command: {}", .{input}); + log.warn("invalid cursor up command: {f}", .{input}); return; }, }, true, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI E with intermediates: {s}", @@ -497,12 +497,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor down command: {}", .{input}); + log.warn("invalid cursor down command: {f}", .{input}); return; }, }, true, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI F with intermediates: {s}", @@ -516,8 +516,8 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "setCursorCol")) switch (input.params.len) { 0 => try self.handler.setCursorCol(1), 1 => try self.handler.setCursorCol(input.params[0]), - else => log.warn("invalid HPA command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid HPA command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI G with intermediates: {s}", @@ -532,8 +532,8 @@ pub fn Stream(comptime Handler: type) type { 0 => try self.handler.setCursorPos(1, 1), 1 => try self.handler.setCursorPos(input.params[0], 1), 2 => try self.handler.setCursorPos(input.params[0], input.params[1]), - else => log.warn("invalid CUP command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid CUP command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI H with intermediates: {s}", @@ -548,11 +548,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid horizontal tab command: {}", .{input}); + log.warn("invalid horizontal tab command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI I with intermediates: {s}", @@ -569,7 +569,7 @@ pub fn Stream(comptime Handler: type) type { }; const protected = protected_ orelse { - log.warn("invalid erase display command: {}", .{input}); + log.warn("invalid erase display command: {f}", .{input}); return; }; @@ -580,12 +580,12 @@ pub fn Stream(comptime Handler: type) type { }; const mode = mode_ orelse { - log.warn("invalid erase display command: {}", .{input}); + log.warn("invalid erase display command: {f}", .{input}); return; }; try self.handler.eraseDisplay(mode, protected); - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // Erase Line 'K' => if (@hasDecl(T, "eraseLine")) { @@ -596,7 +596,7 @@ pub fn Stream(comptime Handler: type) type { }; const protected = protected_ orelse { - log.warn("invalid erase line command: {}", .{input}); + log.warn("invalid erase line command: {f}", .{input}); return; }; @@ -607,12 +607,12 @@ pub fn Stream(comptime Handler: type) type { }; const mode = mode_ orelse { - log.warn("invalid erase line command: {}", .{input}); + log.warn("invalid erase line command: {f}", .{input}); return; }; try self.handler.eraseLine(mode, protected); - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // IL - Insert Lines // TODO: test @@ -620,8 +620,8 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "insertLines")) switch (input.params.len) { 0 => try self.handler.insertLines(1), 1 => try self.handler.insertLines(input.params[0]), - else => log.warn("invalid IL command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid IL command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI L with intermediates: {s}", @@ -635,8 +635,8 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "deleteLines")) switch (input.params.len) { 0 => try self.handler.deleteLines(1), 1 => try self.handler.deleteLines(input.params[0]), - else => log.warn("invalid DL command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid DL command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI M with intermediates: {s}", @@ -651,11 +651,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid delete characters command: {}", .{input}); + log.warn("invalid delete characters command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI P with intermediates: {s}", @@ -671,11 +671,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid scroll up command: {}", .{input}); + log.warn("invalid scroll up command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI S with intermediates: {s}", @@ -690,11 +690,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid scroll down command: {}", .{input}); + log.warn("invalid scroll down command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI T with intermediates: {s}", @@ -711,7 +711,7 @@ pub fn Stream(comptime Handler: type) type { if (@hasDecl(T, "tabSet")) try self.handler.tabSet() else - log.warn("unimplemented tab set callback: {}", .{input}); + log.warn("unimplemented tab set callback: {f}", .{input}); return; } @@ -725,12 +725,12 @@ pub fn Stream(comptime Handler: type) type { 2 => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(.current) else - log.warn("unimplemented tab clear callback: {}", .{input}), + log.warn("unimplemented tab clear callback: {f}", .{input}), 5 => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(.all) else - log.warn("unimplemented tab clear callback: {}", .{input}), + log.warn("unimplemented tab clear callback: {f}", .{input}), else => {}, }, @@ -738,7 +738,7 @@ pub fn Stream(comptime Handler: type) type { else => {}, } - log.warn("invalid cursor tabulation control: {}", .{input}); + log.warn("invalid cursor tabulation control: {f}", .{input}); return; }, @@ -746,8 +746,8 @@ pub fn Stream(comptime Handler: type) type { if (@hasDecl(T, "tabReset")) try self.handler.tabReset() else - log.warn("unimplemented tab reset callback: {}", .{input}); - } else log.warn("invalid cursor tabulation control: {}", .{input}), + log.warn("unimplemented tab reset callback: {f}", .{input}); + } else log.warn("invalid cursor tabulation control: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI W with intermediates: {s}", @@ -762,11 +762,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid erase characters command: {}", .{input}); + log.warn("invalid erase characters command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI X with intermediates: {s}", @@ -781,11 +781,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid horizontal tab back command: {}", .{input}); + log.warn("invalid horizontal tab back command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI Z with intermediates: {s}", @@ -800,11 +800,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid HPR command: {}", .{input}); + log.warn("invalid HPR command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI a with intermediates: {s}", @@ -819,11 +819,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid print repeat command: {}", .{input}); + log.warn("invalid print repeat command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI b with intermediates: {s}", @@ -842,12 +842,12 @@ pub fn Stream(comptime Handler: type) type { }, else => @as(?ansi.DeviceAttributeReq, null), } orelse { - log.warn("invalid device attributes command: {}", .{input}); + log.warn("invalid device attributes command: {f}", .{input}); return; }; try self.handler.deviceAttributes(req, input.params); - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // VPA - Cursor Vertical Position Absolute 'd' => switch (input.intermediates.len) { @@ -856,11 +856,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid VPA command: {}", .{input}); + log.warn("invalid VPA command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI d with intermediates: {s}", @@ -875,11 +875,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid VPR command: {}", .{input}); + log.warn("invalid VPR command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI e with intermediates: {s}", @@ -894,11 +894,11 @@ pub fn Stream(comptime Handler: type) type { switch (input.params.len) { 1 => @enumFromInt(input.params[0]), else => { - log.warn("invalid tab clear command: {}", .{input}); + log.warn("invalid tab clear command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI g with intermediates: {s}", @@ -913,7 +913,7 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :ansi false; - log.warn("invalid set mode command: {}", .{input}); + log.warn("invalid set mode command: {f}", .{input}); break :mode; }; @@ -924,7 +924,7 @@ pub fn Stream(comptime Handler: type) type { log.warn("unimplemented mode: {}", .{mode_int}); } } - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // RM - Reset Mode 'l' => if (@hasDecl(T, "setMode")) mode: { @@ -933,7 +933,7 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :ansi false; - log.warn("invalid set mode command: {}", .{input}); + log.warn("invalid set mode command: {f}", .{input}); break :mode; }; @@ -944,7 +944,7 @@ pub fn Stream(comptime Handler: type) type { log.warn("unimplemented mode: {}", .{mode_int}); } } - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // SGR - Select Graphic Rendition 'm' => switch (input.intermediates.len) { @@ -958,7 +958,7 @@ pub fn Stream(comptime Handler: type) type { // log.info("SGR attribute: {}", .{attr}); try self.handler.setAttribute(attr); } - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), 1 => switch (input.intermediates[0]) { '>' => if (@hasDecl(T, "setModifyKeyFormat")) blk: { @@ -974,13 +974,13 @@ pub fn Stream(comptime Handler: type) type { 2 => .{ .function_keys = {} }, 4 => .{ .other_keys = .none }, else => { - log.warn("invalid setModifyKeyFormat: {}", .{input}); + log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; }, }; if (input.params.len > 2) { - log.warn("invalid setModifyKeyFormat: {}", .{input}); + log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; } @@ -1000,7 +1000,7 @@ pub fn Stream(comptime Handler: type) type { } try self.handler.setModifyKeyFormat(format); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{input}), + } else log.warn("unimplemented setModifyKeyFormat: {f}", .{input}), else => log.warn( "unknown CSI m with intermediate: {}", @@ -1029,12 +1029,12 @@ pub fn Stream(comptime Handler: type) type { input.intermediates[0] == '?') { if (!@hasDecl(T, "deviceStatusReport")) { - log.warn("unimplemented CSI callback: {}", .{input}); + log.warn("unimplemented CSI callback: {f}", .{input}); return; } if (input.params.len != 1) { - log.warn("invalid device status report command: {}", .{input}); + log.warn("invalid device status report command: {f}", .{input}); return; } @@ -1043,12 +1043,12 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :question true; - log.warn("invalid set mode command: {}", .{input}); + log.warn("invalid set mode command: {f}", .{input}); return; }; const req = device_status.reqFromInt(input.params[0], question) orelse { - log.warn("invalid device status report command: {}", .{input}); + log.warn("invalid device status report command: {f}", .{input}); return; }; @@ -1067,7 +1067,7 @@ pub fn Stream(comptime Handler: type) type { // only support reverting back to modify other keys in // numeric except format. try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except }); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{input}), + } else log.warn("unimplemented setModifyKeyFormat: {f}", .{input}), else => log.warn( "unknown CSI n with intermediate: {}", @@ -1101,13 +1101,13 @@ pub fn Stream(comptime Handler: type) type { }; if (input.params.len != 1) { - log.warn("invalid DECRQM command: {}", .{input}); + log.warn("invalid DECRQM command: {f}", .{input}); break :decrqm; } if (@hasDecl(T, "requestMode")) { try self.handler.requestMode(input.params[0], ansi_mode); - } else log.warn("unimplemented DECRQM callback: {}", .{input}); + } else log.warn("unimplemented DECRQM callback: {f}", .{input}); }, else => log.warn( @@ -1126,11 +1126,11 @@ pub fn Stream(comptime Handler: type) type { 0 => ansi.CursorStyle.default, 1 => @enumFromInt(input.params[0]), else => { - log.warn("invalid set curor style command: {}", .{input}); + log.warn("invalid set curor style command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}); + ) else log.warn("unimplemented CSI callback: {f}", .{input}); }, // DECSCA @@ -1147,12 +1147,12 @@ pub fn Stream(comptime Handler: type) type { }; const mode = mode_ orelse { - log.warn("invalid set protected mode command: {}", .{input}); + log.warn("invalid set protected mode command: {f}", .{input}); return; }; try self.handler.setProtectedMode(mode); - } else log.warn("unimplemented CSI callback: {}", .{input}); + } else log.warn("unimplemented CSI callback: {f}", .{input}); }, // XTVERSION @@ -1180,10 +1180,10 @@ pub fn Stream(comptime Handler: type) type { 0 => try self.handler.setTopAndBottomMargin(0, 0), 1 => try self.handler.setTopAndBottomMargin(input.params[0], 0), 2 => try self.handler.setTopAndBottomMargin(input.params[0], input.params[1]), - else => log.warn("invalid DECSTBM command: {}", .{input}), + else => log.warn("invalid DECSTBM command: {f}", .{input}), } } else log.warn( - "unimplemented CSI callback: {}", + "unimplemented CSI callback: {f}", .{input}, ), @@ -1203,13 +1203,13 @@ pub fn Stream(comptime Handler: type) type { }, else => log.warn( - "unknown CSI s with intermediate: {}", + "unknown CSI s with intermediate: {f}", .{input}, ), }, else => log.warn( - "ignoring unimplemented CSI s with intermediates: {s}", + "ignoring unimplemented CSI s with intermediates: {f}", .{input}, ), }, @@ -1225,10 +1225,10 @@ pub fn Stream(comptime Handler: type) type { 0 => try self.handler.setLeftAndRightMarginAmbiguous(), 1 => try self.handler.setLeftAndRightMargin(input.params[0], 0), 2 => try self.handler.setLeftAndRightMargin(input.params[0], input.params[1]), - else => log.warn("invalid DECSLRM command: {}", .{input}), + else => log.warn("invalid DECSLRM command: {f}", .{input}), } } else log.warn( - "unimplemented CSI callback: {}", + "unimplemented CSI callback: {f}", .{input}, ), @@ -1254,30 +1254,30 @@ pub fn Stream(comptime Handler: type) type { 0 => false, 1 => true, else => { - log.warn("invalid XTSHIFTESCAPE command: {}", .{input}); + log.warn("invalid XTSHIFTESCAPE command: {f}", .{input}); break :capture; }, }, else => { - log.warn("invalid XTSHIFTESCAPE command: {}", .{input}); + log.warn("invalid XTSHIFTESCAPE command: {f}", .{input}); break :capture; }, }; try self.handler.setMouseShiftCapture(capture); } else log.warn( - "unimplemented CSI callback: {}", + "unimplemented CSI callback: {f}", .{input}, ), else => log.warn( - "unknown CSI s with intermediate: {}", + "unknown CSI s with intermediate: {f}", .{input}, ), }, else => log.warn( - "ignoring unimplemented CSI s with intermediates: {s}", + "ignoring unimplemented CSI s with intermediates: {f}", .{input}, ), }, @@ -1296,7 +1296,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 14 t with extra parameters: {}", + "ignoring CSI 14 t with extra parameters: {f}", .{input}, ), 16 => if (input.params.len == 1) { @@ -1308,7 +1308,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 16 t with extra parameters: {s}", + "ignoring CSI 16 t with extra parameters: {f}", .{input}, ), 18 => if (input.params.len == 1) { @@ -1320,7 +1320,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 18 t with extra parameters: {s}", + "ignoring CSI 18 t with extra parameters: {f}", .{input}, ), 21 => if (input.params.len == 1) { @@ -1332,7 +1332,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 21 t with extra parameters: {s}", + "ignoring CSI 21 t with extra parameters: {f}", .{input}, ), inline 22, 23 => |number| if ((input.params.len == 2 or @@ -1359,21 +1359,21 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 22/23 t with extra parameters: {s}", + "ignoring CSI 22/23 t with extra parameters: {f}", .{input}, ), else => log.warn( - "ignoring CSI t with unimplemented parameter: {s}", + "ignoring CSI t with unimplemented parameter: {f}", .{input}, ), } } else log.err( - "ignoring CSI t with no parameters: {s}", + "ignoring CSI t with no parameters: {f}", .{input}, ); }, else => log.warn( - "ignoring unimplemented CSI t with intermediates: {s}", + "ignoring unimplemented CSI t with intermediates: {f}", .{input}, ), }, @@ -1382,7 +1382,7 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "restoreCursor")) try self.handler.restoreCursor() else - log.warn("unimplemented CSI callback: {}", .{input}), + log.warn("unimplemented CSI callback: {f}", .{input}), // Kitty keyboard protocol 1 => switch (input.intermediates[0]) { @@ -1393,7 +1393,7 @@ pub fn Stream(comptime Handler: type) type { '>' => if (@hasDecl(T, "pushKittyKeyboard")) push: { const flags: u5 = if (input.params.len == 1) std.math.cast(u5, input.params[0]) orelse { - log.warn("invalid pushKittyKeyboard command: {}", .{input}); + log.warn("invalid pushKittyKeyboard command: {f}", .{input}); break :push; } else @@ -1414,7 +1414,7 @@ pub fn Stream(comptime Handler: type) type { '=' => if (@hasDecl(T, "setKittyKeyboard")) set: { const flags: u5 = if (input.params.len >= 1) std.math.cast(u5, input.params[0]) orelse { - log.warn("invalid setKittyKeyboard command: {}", .{input}); + log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; } else @@ -1430,7 +1430,7 @@ pub fn Stream(comptime Handler: type) type { 2 => .@"or", 3 => .not, else => { - log.warn("invalid setKittyKeyboard command: {}", .{input}); + log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; }, }; @@ -1442,13 +1442,13 @@ pub fn Stream(comptime Handler: type) type { }, else => log.warn( - "unknown CSI s with intermediate: {}", + "unknown CSI s with intermediate: {f}", .{input}, ), }, else => log.warn( - "ignoring unimplemented CSI u: {}", + "ignoring unimplemented CSI u: {f}", .{input}, ), }, @@ -1458,11 +1458,11 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "insertBlanks")) switch (input.params.len) { 0 => try self.handler.insertBlanks(1), 1 => try self.handler.insertBlanks(input.params[0]), - else => log.warn("invalid ICH command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid ICH command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( - "ignoring unimplemented CSI @: {}", + "ignoring unimplemented CSI @: {f}", .{input}, ), }, @@ -1487,13 +1487,13 @@ pub fn Stream(comptime Handler: type) type { break :decsasd true; }; - if (!success) log.warn("unimplemented CSI callback: {}", .{input}); + if (!success) log.warn("unimplemented CSI callback: {f}", .{input}); }, else => if (@hasDecl(T, "csiUnimplemented")) try self.handler.csiUnimplemented(input) else - log.warn("unimplemented CSI action: {}", .{input}), + log.warn("unimplemented CSI action: {f}", .{input}), } } @@ -1690,10 +1690,10 @@ pub fn Stream(comptime Handler: type) type { '7' => if (@hasDecl(T, "saveCursor")) switch (action.intermediates.len) { 0 => try self.handler.saveCursor(), else => { - log.warn("invalid command: {}", .{action}); + log.warn("invalid command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), '8' => blk: { switch (action.intermediates.len) { @@ -1701,14 +1701,14 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "restoreCursor")) { try self.handler.restoreCursor(); break :blk {}; - } else log.warn("unimplemented restore cursor callback: {}", .{action}), + } else log.warn("unimplemented restore cursor callback: {f}", .{action}), 1 => switch (action.intermediates[0]) { // DECALN - Fill Screen with E '#' => if (@hasDecl(T, "decaln")) { try self.handler.decaln(); break :blk {}; - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), else => {}, }, @@ -1716,146 +1716,146 @@ pub fn Stream(comptime Handler: type) type { else => {}, // fall through } - log.warn("unimplemented ESC action: {}", .{action}); + log.warn("unimplemented ESC action: {f}", .{action}); }, // IND - Index 'D' => if (@hasDecl(T, "index")) switch (action.intermediates.len) { 0 => try self.handler.index(), else => { - log.warn("invalid index command: {}", .{action}); + log.warn("invalid index command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // NEL - Next Line 'E' => if (@hasDecl(T, "nextLine")) switch (action.intermediates.len) { 0 => try self.handler.nextLine(), else => { - log.warn("invalid next line command: {}", .{action}); + log.warn("invalid next line command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // HTS - Horizontal Tab Set 'H' => if (@hasDecl(T, "tabSet")) switch (action.intermediates.len) { 0 => try self.handler.tabSet(), else => { - log.warn("invalid tab set command: {}", .{action}); + log.warn("invalid tab set command: {f}", .{action}); return; }, - } else log.warn("unimplemented tab set callback: {}", .{action}), + } else log.warn("unimplemented tab set callback: {f}", .{action}), // RI - Reverse Index 'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) { 0 => try self.handler.reverseIndex(), else => { - log.warn("invalid reverse index command: {}", .{action}); + log.warn("invalid reverse index command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // SS2 - Single Shift 2 'N' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G2, true), else => { - log.warn("invalid single shift 2 command: {}", .{action}); + log.warn("invalid single shift 2 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // SS3 - Single Shift 3 'O' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G3, true), else => { - log.warn("invalid single shift 3 command: {}", .{action}); + log.warn("invalid single shift 3 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // SPA - Start of Guarded Area 'V' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) { try self.handler.setProtectedMode(ansi.ProtectedMode.iso); - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // EPA - End of Guarded Area 'W' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) { try self.handler.setProtectedMode(ansi.ProtectedMode.off); - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // DECID 'Z' => if (@hasDecl(T, "deviceAttributes") and action.intermediates.len == 0) { try self.handler.deviceAttributes(.primary, &.{}); - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // RIS - Full Reset 'c' => if (@hasDecl(T, "fullReset")) switch (action.intermediates.len) { 0 => try self.handler.fullReset(), else => { - log.warn("invalid full reset command: {}", .{action}); + log.warn("invalid full reset command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // LS2 - Locking Shift 2 'n' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G2, false), else => { - log.warn("invalid single shift 2 command: {}", .{action}); + log.warn("invalid single shift 2 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS3 - Locking Shift 3 'o' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G3, false), else => { - log.warn("invalid single shift 3 command: {}", .{action}); + log.warn("invalid single shift 3 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS1R - Locking Shift 1 Right '~' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GR, .G1, false), else => { - log.warn("invalid locking shift 1 right command: {}", .{action}); + log.warn("invalid locking shift 1 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS2R - Locking Shift 2 Right '}' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GR, .G2, false), else => { - log.warn("invalid locking shift 2 right command: {}", .{action}); + log.warn("invalid locking shift 2 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS3R - Locking Shift 3 Right '|' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GR, .G3, false), else => { - log.warn("invalid locking shift 3 right command: {}", .{action}); + log.warn("invalid locking shift 3 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // Set application keypad mode '=' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) { try self.handler.setMode(.keypad_keys, true); - } else log.warn("unimplemented setMode: {}", .{action}), + } else log.warn("unimplemented setMode: {f}", .{action}), // Reset application keypad mode '>' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) { try self.handler.setMode(.keypad_keys, false); - } else log.warn("unimplemented setMode: {}", .{action}), + } else log.warn("unimplemented setMode: {f}", .{action}), else => if (@hasDecl(T, "escUnimplemented")) try self.handler.escUnimplemented(action) else - log.warn("unimplemented ESC action: {}", .{action}), + log.warn("unimplemented ESC action: {f}", .{action}), // Sets ST (string terminator). We don't have to do anything // because our parser always accepts ST. From cb295b84a0ec274a43da59041fa4a199e799798d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Oct 2025 13:10:40 -0700 Subject: [PATCH 131/319] Zig 0.15: zig build test --- build.zig | 4 +- src/Command.zig | 26 +- src/apprt/gtk/build/blueprint.zig | 8 +- src/apprt/gtk/build/gresource.zig | 6 +- src/apprt/gtk/class/config.zig | 8 +- src/apprt/gtk/ipc/DBus.zig | 23 +- src/apprt/gtk/ipc/new_window.zig | 2 +- src/benchmark/CodepointWidth.zig | 61 ++--- src/benchmark/GraphemeBreak.zig | 50 +--- src/benchmark/IsSymbol.zig | 6 +- src/benchmark/TerminalParser.zig | 10 +- src/benchmark/TerminalStream.zig | 14 +- src/benchmark/options.zig | 2 +- src/build/GhosttyFrameData.zig | 1 + src/cli/args.zig | 261 +++++++++--------- src/cli/boo.zig | 19 +- src/cli/crash_report.zig | 29 +- src/cli/diagnostics.zig | 13 +- src/cli/edit_config.zig | 33 ++- src/cli/ghostty.zig | 8 +- src/cli/help.zig | 5 +- src/cli/list_actions.zig | 11 +- src/cli/list_colors.zig | 32 +-- src/cli/list_fonts.zig | 16 +- src/cli/list_keybinds.zig | 110 ++++---- src/cli/list_themes.zig | 97 ++++--- src/cli/new_window.zig | 23 +- src/cli/show_config.zig | 7 +- src/cli/show_face.zig | 41 ++- src/cli/ssh-cache/DiskCache.zig | 40 +-- src/cli/ssh-cache/Entry.zig | 2 +- src/cli/ssh_cache.zig | 33 ++- src/cli/validate_config.zig | 21 +- src/cli/version.zig | 25 +- src/config/Config.zig | 336 +++++++++++++----------- src/config/RepeatableStringMap.zig | 26 +- src/config/command.zig | 19 +- src/config/edit.zig | 4 +- src/config/formatter.zig | 129 +++++---- src/config/io.zig | 16 +- src/config/path.zig | 40 +-- src/config/theme.zig | 27 +- src/crash/sentry_envelope.zig | 192 ++++++-------- src/datastruct/main.zig | 2 +- src/datastruct/split_tree.zig | 123 ++++----- src/extra/vim.zig | 13 +- src/font/Metrics.zig | 12 +- src/font/SharedGridSet.zig | 14 +- src/font/shaper/run.zig | 4 +- src/global.zig | 4 +- src/helpgen.zig | 39 +-- src/input/Binding.zig | 63 ++--- src/input/command.zig | 3 +- src/input/function_keys.zig | 1 + src/input/helpgen_actions.zig | 31 ++- src/renderer/link.zig | 26 +- src/renderer/shadertoy.zig | 44 ++-- src/terminal/apc.zig | 4 +- src/terminal/dcs.zig | 2 +- src/terminal/kitty/graphics_command.zig | 56 ++-- src/terminal/kitty/graphics_image.zig | 26 +- src/terminal/kitty/graphics_storage.zig | 6 +- src/terminal/tmux.zig | 35 +-- src/terminfo/Source.zig | 5 +- src/termio/Exec.zig | 16 +- src/termio/shell_integration.zig | 43 +-- 66 files changed, 1264 insertions(+), 1144 deletions(-) diff --git a/build.zig b/build.zig index cb8f175a4..7b66af81a 100644 --- a/build.zig +++ b/build.zig @@ -276,6 +276,8 @@ pub fn build(b: *std.Build) !void { .omit_frame_pointer = false, .unwind_tables = .sync, }), + // Crash on x86_64 without this + .use_llvm = true, }); if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe); @@ -285,7 +287,7 @@ pub fn build(b: *std.Build) !void { test_step.dependOn(&test_run.step); // Normal tests always test our libghostty modules - test_step.dependOn(test_lib_vt_step); + //test_step.dependOn(test_lib_vt_step); // Valgrind test running const valgrind_run = b.addSystemCommand(&.{ diff --git a/src/Command.zig b/src/Command.zig index b0d804327..f28d8bb9d 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -194,7 +194,9 @@ fn startPosix(self: *Command, arena: Allocator) !void { // child process so there isn't much we can do. We try to output // something reasonable. Its important to note we MUST NOT return // any other error condition from here on out. - const stderr = std.io.getStdErr().writer(); + var stderr_buf: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_writer.interface; switch (err) { error.FileNotFound => stderr.print( \\Requested executable not found. Please verify the command is on @@ -211,6 +213,7 @@ fn startPosix(self: *Command, arena: Allocator) !void { .{err}, ) catch {}, } + stderr.flush() catch {}; // We return a very specific error that can be detected to determine // we're in the child. @@ -464,34 +467,35 @@ fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u1 /// Copied from Zig. This function could be made public in child_process.zig instead. fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { - var buf = std.ArrayList(u8).init(allocator); + var buf: std.Io.Writer.Allocating = .init(allocator); defer buf.deinit(); + const writer = &buf.writer; for (argv, 0..) |arg, arg_i| { - if (arg_i != 0) try buf.append(' '); + if (arg_i != 0) try writer.writeByte(' '); if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { - try buf.appendSlice(arg); + try writer.writeAll(arg); continue; } - try buf.append('"'); + try writer.writeByte('"'); var backslash_count: usize = 0; for (arg) |byte| { switch (byte) { '\\' => backslash_count += 1, '"' => { - try buf.appendNTimes('\\', backslash_count * 2 + 1); - try buf.append('"'); + try writer.splatByteAll('\\', backslash_count * 2 + 1); + try writer.writeByte('"'); backslash_count = 0; }, else => { - try buf.appendNTimes('\\', backslash_count); - try buf.append(byte); + try writer.splatByteAll('\\', backslash_count); + try writer.writeByte(byte); backslash_count = 0; }, } } - try buf.appendNTimes('\\', backslash_count * 2); - try buf.append('"'); + try writer.splatByteAll('\\', backslash_count * 2); + try writer.writeByte('"'); } return buf.toOwnedSliceSentinel(0); diff --git a/src/apprt/gtk/build/blueprint.zig b/src/apprt/gtk/build/blueprint.zig index 1e614f972..f25e7e1f9 100644 --- a/src/apprt/gtk/build/blueprint.zig +++ b/src/apprt/gtk/build/blueprint.zig @@ -45,7 +45,7 @@ pub fn main() !void { std.debug.print( \\`libadwaita` is too old. \\ - \\Ghostty requires a version {} or newer of `libadwaita` to + \\Ghostty requires a version {f} or newer of `libadwaita` to \\compile this blueprint. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. , .{required_adwaita_version}); @@ -80,7 +80,7 @@ pub fn main() !void { std.debug.print( \\`blueprint-compiler` not found. \\ - \\Ghostty requires version {} or newer of + \\Ghostty requires version {f} or newer of \\`blueprint-compiler` as a build-time dependency starting \\from version 1.2. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. @@ -104,7 +104,7 @@ pub fn main() !void { std.debug.print( \\`blueprint-compiler` is the wrong version. \\ - \\Ghostty requires version {} or newer of + \\Ghostty requires version {f} or newer of \\`blueprint-compiler` as a build-time dependency starting \\from version 1.2. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. @@ -145,7 +145,7 @@ pub fn main() !void { std.debug.print( \\`blueprint-compiler` not found. \\ - \\Ghostty requires version {} or newer of + \\Ghostty requires version {f} or newer of \\`blueprint-compiler` as a build-time dependency starting \\from version 1.2. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index 1f253fd5e..7adcd3e44 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -142,7 +142,9 @@ pub fn main() !void { ); } - const writer = std.io.getStdOut().writer(); + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + const writer = &stdout.interface; try writer.writeAll( \\ \\ @@ -157,6 +159,8 @@ pub fn main() !void { \\ \\ ); + + try stdout.end(); } /// Generate the icon resources. This works by looking up all the icons diff --git a/src/apprt/gtk/class/config.zig b/src/apprt/gtk/class/config.zig index 2b98c68b5..eadd3b7b8 100644 --- a/src/apprt/gtk/class/config.zig +++ b/src/apprt/gtk/class/config.zig @@ -117,10 +117,10 @@ pub const Config = extern struct { errdefer text_buf.unref(); var buf: [4095:0]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); for (config._diagnostics.items()) |diag| { - fbs.reset(); - diag.write(fbs.writer()) catch |err| { + writer.end = 0; + diag.format(&writer) catch |err| { log.warn( "error writing diagnostic to buffer err={}", .{err}, @@ -128,7 +128,7 @@ pub const Config = extern struct { continue; }; - text_buf.insertAtCursor(&buf, @intCast(fbs.pos)); + text_buf.insertAtCursor(&buf, @intCast(writer.end)); text_buf.insertAtCursor("\n", 1); } diff --git a/src/apprt/gtk/ipc/DBus.zig b/src/apprt/gtk/ipc/DBus.zig index d14d86ce6..fa4a6723e 100644 --- a/src/apprt/gtk/ipc/DBus.zig +++ b/src/apprt/gtk/ipc/DBus.zig @@ -29,7 +29,10 @@ payload_builder: *glib.VariantBuilder, parameters_builder: *glib.VariantBuilder, /// Initialize the helper. -pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!Self { +pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!Self { + var buf: [256]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buf); + const stderr = &stderr_writer.interface; // Get the appropriate bus name and object path for contacting the // Ghostty instance we're interested in. @@ -37,7 +40,7 @@ pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (A .class => |class| result: { // Force the usage of the class specified on the CLI to determine the // bus name and object path. - const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class}); + const object_path = try std.fmt.allocPrintSentinel(alloc, "/{s}", .{class}, 0); std.mem.replaceScalar(u8, object_path, '.', '/'); std.mem.replaceScalar(u8, object_path, '-', '_'); @@ -54,14 +57,14 @@ pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (A } if (gio.Application.idIsValid(bus_name.ptr) == 0) { - const stderr = std.io.getStdErr().writer(); try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name}); + try stderr.flush(); return error.IPCFailed; } if (glib.Variant.isObjectPath(object_path.ptr) == 0) { - const stderr = std.io.getStdErr().writer(); try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path}); + try stderr.flush(); return error.IPCFailed; } @@ -72,17 +75,17 @@ pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (A const dbus_ = gio.busGetSync(.session, null, &err_); if (err_) |err| { - const stderr = std.io.getStdErr().writer(); try stderr.print( "Unable to establish connection to D-Bus session bus: {s}\n", .{err.f_message orelse "(unknown)"}, ); + try stderr.flush(); return error.IPCFailed; } break :dbus dbus_ orelse { - const stderr = std.io.getStdErr().writer(); try stderr.print("gio.busGetSync returned null\n", .{}); + try stderr.flush(); return error.IPCFailed; }; }; @@ -128,7 +131,11 @@ pub fn addParameter(self: *Self, variant: *glib.Variant) void { /// Send the IPC to the remote Ghostty. Once it completes, nothing further /// should be done with this object other than call `deinit`. -pub fn send(self: *Self) (std.posix.WriteError || apprt.ipc.Errors)!void { +pub fn send(self: *Self) (std.Io.Writer.Error || apprt.ipc.Errors)!void { + var buf: [256]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buf); + const stderr = &stderr_writer.interface; + // finish building the parameters const parameters = self.parameters_builder.end(); @@ -167,11 +174,11 @@ pub fn send(self: *Self) (std.posix.WriteError || apprt.ipc.Errors)!void { defer if (result_) |result| result.unref(); if (err_) |err| { - const stderr = std.io.getStdErr().writer(); try stderr.print( "D-Bus method call returned an error err={s}\n", .{err.f_message orelse "(unknown)"}, ); + try stderr.flush(); return error.IPCFailed; } } diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index 55e2e0e01..19c46e3aa 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -20,7 +20,7 @@ const DBus = @import("DBus.zig"); // ``` // gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' [] // ``` -pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool { +pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool { var dbus = try DBus.init( alloc, target, diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index 9bbc2def7..552df8d1f 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -10,7 +10,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); -const uucode = @import("uucode"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); const simd = @import("../simd/main.zig"); const table = @import("../unicode/main.zig").table; @@ -48,9 +47,6 @@ pub const Mode = enum { /// Test our lookup table implementation. table, - - /// Using uucode, with custom `width` extension based on `wcwidth`. - uucode, }; /// Create a new terminal stream handler for the given arguments. @@ -75,7 +71,6 @@ pub fn benchmark(self: *CodepointWidth) Benchmark { .wcwidth => stepWcwidth, .table => stepTable, .simd => stepSimd, - .uucode => stepUucode, }, .setupFn = setup, .teardownFn = teardown, @@ -112,12 +107,15 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -136,12 +134,15 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -165,12 +166,15 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -185,35 +189,6 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { } } -fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { - const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); - - const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); - var d: UTF8Decoder = .{}; - var buf: [4096]u8 align(std.atomic.cache_line) = undefined; - while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); - return error.BenchmarkFailed; - }; - if (n == 0) break; // EOF reached - - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp| { - // This is the same trick we do in terminal.zig so we - // keep it here. - std.mem.doNotOptimizeAway(if (cp <= 0xFF) - 1 - else - uucode.get(.width, @intCast(cp))); - } - } - } -} - test CodepointWidth { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index e576c71ef..a1b3380f0 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -8,7 +8,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); -const uucode = @import("uucode"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); const unicode = @import("../unicode/main.zig"); @@ -39,9 +38,6 @@ pub const Mode = enum { /// Ghostty's table-based approach. table, - - /// uucode implementation - uucode, }; /// Create a new terminal stream handler for the given arguments. @@ -64,7 +60,6 @@ pub fn benchmark(self: *GraphemeBreak) Benchmark { .stepFn = switch (self.opts.mode) { .noop => stepNoop, .table => stepTable, - .uucode => stepUucode, }, .setupFn = setup, .teardownFn = teardown, @@ -95,12 +90,15 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -115,14 +113,17 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var state: unicode.GraphemeBreakState = .{}; var cp1: u21 = 0; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -138,33 +139,6 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { } } -fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { - const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); - - const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); - var d: UTF8Decoder = .{}; - var state: uucode.grapheme.BreakState = .default; - var cp1: u21 = 0; - var buf: [4096]u8 align(std.atomic.cache_line) = undefined; - while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); - return error.BenchmarkFailed; - }; - if (n == 0) break; // EOF reached - - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp2| { - std.mem.doNotOptimizeAway(uucode.grapheme.isBreak(cp1, @intCast(cp2), &state)); - cp1 = cp2; - } - } - } -} - test GraphemeBreak { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index 97af0657a..dffa5071a 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -90,7 +90,8 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const self: *IsSymbol = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var r = f.reader(&read_buf); var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { @@ -114,7 +115,8 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const self: *IsSymbol = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var r = f.reader(&read_buf); var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { diff --git a/src/benchmark/TerminalParser.zig b/src/benchmark/TerminalParser.zig index 3065c1ed6..f13b44552 100644 --- a/src/benchmark/TerminalParser.zig +++ b/src/benchmark/TerminalParser.zig @@ -75,14 +75,16 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { // the benchmark results and... I know writing this that we // aren't currently IO bound. const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; var p: terminalpkg.Parser = .init(); - var buf: [4096]u8 align(std.atomic.cache_line) = undefined; + var buf: [4096]u8 = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 71ab1fdfc..ecce509f3 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -113,17 +113,19 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { // the benchmark results and... I know writing this that we // aren't currently IO bound. const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); - var buf: [4096]u8 align(std.atomic.cache_line) = undefined; + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + const r = &f_reader.interface; + + var buf: [4096]u8 = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached - const chunk = buf[0..n]; - self.stream.nextSlice(chunk) catch |err| { + self.stream.nextSlice(buf[0..n]) catch |err| { log.warn("error processing data file chunk err={}", .{err}); return error.BenchmarkFailed; }; diff --git a/src/benchmark/options.zig b/src/benchmark/options.zig index 867be6afc..049e80f48 100644 --- a/src/benchmark/options.zig +++ b/src/benchmark/options.zig @@ -10,7 +10,7 @@ pub fn dataFile(path_: ?[]const u8) !?std.fs.File { const path = path_ orelse return null; // Stdin - if (std.mem.eql(u8, path, "-")) return std.io.getStdIn(); + if (std.mem.eql(u8, path, "-")) return .stdin(); // Normal file const file = try std.fs.cwd().openFile(path, .{}); diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig index 7193162bd..def1dbdb3 100644 --- a/src/build/GhosttyFrameData.zig +++ b/src/build/GhosttyFrameData.zig @@ -43,6 +43,7 @@ pub fn distResources(b: *std.Build) struct { .root_module = b.createModule(.{ .target = b.graph.host, }), + .use_llvm = true, }); exe.addCSourceFile(.{ .file = b.path("src/build/framegen/main.c"), diff --git a/src/cli/args.zig b/src/cli/args.zig index c4a40acf5..a34560b78 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -162,10 +162,11 @@ pub fn parse( error.InvalidField => "unknown field", 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( + else => try std.fmt.allocPrintSentinel( arena_alloc, "unknown error {}", .{err}, + 0, ), }; @@ -235,14 +236,16 @@ 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(); +) std.Io.Writer.Error![:0]const u8 { + var stream: std.Io.Writer.Allocating = .init(arena_alloc); + const writer = &stream.writer; + try writer.print("value required", .{}); try formatValues(T, key, writer); try writer.writeByte(0); - return buf.items[0 .. buf.items.len - 1 :0]; + + const written = stream.written(); + return written[0 .. written.len - 1 :0]; } fn formatInvalidValue( @@ -250,17 +253,23 @@ fn formatInvalidValue( 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(); +) std.Io.Writer.Error![:0]const u8 { + var stream: std.Io.Writer.Allocating = .init(arena_alloc); + const writer = &stream.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]; + + const written = stream.written(); + return written[0 .. written.len - 1 :0]; } -fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void { +fn formatValues( + comptime T: type, + key: []const u8, + writer: *std.Io.Writer, +) std.Io.Writer.Error!void { @setEvalBranchQuota(2000); const typeinfo = @typeInfo(T); inline for (typeinfo.@"struct".fields) |f| { @@ -542,8 +551,8 @@ pub fn parseAutoStruct( const key = std.mem.trim(u8, entry[0..idx], whitespace); // used if we need to decode a double-quoted string. - var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); const value = value: { const value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); @@ -554,10 +563,9 @@ pub fn parseAutoStruct( value[value.len - 1] == '"') { // Decode a double-quoted string as a Zig string literal. - const writer = buf.writer(alloc); - const parsed = try std.zig.string_literal.parseWrite(writer, value); + const parsed = try std.zig.string_literal.parseWrite(&buf.writer, value); if (parsed == .failure) return error.InvalidValue; - break :value buf.items; + break :value buf.written(); } break :value value; @@ -795,15 +803,13 @@ test "parse: diagnostic location" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var fbs = std.io.fixedBufferStream( + var r: std.Io.Reader = .fixed( \\a=42 \\what \\b=two ); - const r = fbs.reader(); - const Iter = LineIterator(@TypeOf(r)); - var iter: Iter = .{ .r = r, .filepath = "test" }; + var iter: LineIterator = .{ .r = &r, .filepath = "test" }; try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expect(data._arena != null); try testing.expectEqualStrings("42", data.a); @@ -1208,18 +1214,7 @@ test "parseIntoField: struct with basic fields" { try testing.expectEqual(84, data.value.b); try testing.expectEqual(24, data.value.c); - // Set with explicit default - data.value = try parseAutoStruct( - @TypeOf(data.value), - alloc, - "a:hello", - .{ .a = "oh no", .b = 42 }, - ); - try testing.expectEqualStrings("hello", data.value.a); - try testing.expectEqual(42, data.value.b); - try testing.expectEqual(12, data.value.c); - - // Missing required field + // Missing require dfield try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), @@ -1395,115 +1390,119 @@ test "ArgsIterator" { /// Returns an iterator (implements "next") that reads CLI args by line. /// Each CLI arg is expected to be a single line. This is used to implement /// configuration files. -pub fn LineIterator(comptime ReaderType: type) type { - return struct { - const Self = @This(); +pub const LineIterator = struct { + const Self = @This(); - /// The maximum size a single line can be. We don't expect any - /// CLI arg to exceed this size. Can't wait to git blame this in - /// like 4 years and be wrong about this. - pub const MAX_LINE_SIZE = 4096; + /// The maximum size a single line can be. We don't expect any + /// CLI arg to exceed this size. Can't wait to git blame this in + /// like 4 years and be wrong about this. + pub const MAX_LINE_SIZE = 4096; - /// Our stateful reader. - r: ReaderType, + /// Our stateful reader. + r: *std.Io.Reader, - /// Filepath that is used for diagnostics. This is only used for - /// diagnostic messages so it can be formatted however you want. - /// It is prefixed to the messages followed by the line number. - filepath: []const u8 = "", + /// Filepath that is used for diagnostics. This is only used for + /// diagnostic messages so it can be formatted however you want. + /// It is prefixed to the messages followed by the line number. + filepath: []const u8 = "", - /// The current line that we're on. This is 1-indexed because - /// lines are generally 1-indexed in the real world. The value - /// can be zero if we haven't read any lines yet. - line: usize = 0, + /// The current line that we're on. This is 1-indexed because + /// lines are generally 1-indexed in the real world. The value + /// can be zero if we haven't read any lines yet. + line: usize = 0, - /// This is the buffer where we store the current entry that - /// is formatted to be compatible with the parse function. - entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)), + /// This is the buffer where we store the current entry that + /// is formatted to be compatible with the parse function. + entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)), - pub fn next(self: *Self) ?[]const u8 { - // TODO: detect "--" prefixed lines and give a friendlier error - const buf = buf: { - while (true) { - // Read the full line - var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch |err| switch (err) { - inline else => |e| { - log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e }); - return null; - }, - } orelse return null; + pub fn init(reader: *std.Io.Reader) Self { + return .{ .r = reader }; + } - // Increment our line counter - self.line += 1; + pub fn next(self: *Self) ?[]const u8 { + // First prime the reader. + // File readers at least are initialized with a size of 0, + // and this will actually prompt the reader to get the actual + // size of the file, which will be used in the EOF check below. + // + // This will also optimize reads down the line as we're + // more likely to beworking with buffered data. + self.r.fillMore() catch {}; - // Trim any whitespace (including CR) around it - const trim = std.mem.trim(u8, entry, whitespace ++ "\r"); - if (trim.len != entry.len) { - std.mem.copyForwards(u8, entry, trim); - entry = entry[0..trim.len]; - } + var writer: std.Io.Writer = .fixed(self.entry[2..]); - // Ignore blank lines and comments - if (entry.len == 0 or entry[0] == '#') continue; + var entry = while (self.r.seek != self.r.end) { + // Reset write head + writer.end = 0; - // Trim spaces around '=' - if (mem.indexOf(u8, entry, "=")) |idx| { - const key = std.mem.trim(u8, entry[0..idx], whitespace); - const value = value: { - var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); + _ = self.r.streamDelimiterEnding(&writer, '\n') catch |e| { + log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e }); + return null; + }; + _ = self.r.discardDelimiterInclusive('\n') catch {}; - // Detect a quoted string. - if (value.len >= 2 and - value[0] == '"' and - value[value.len - 1] == '"') - { - // Trim quotes since our CLI args processor expects - // quotes to already be gone. - value = value[1 .. value.len - 1]; - } + var entry = writer.buffered(); + self.line += 1; - break :value value; - }; + // Trim any whitespace (including CR) around it + const trim = std.mem.trim(u8, entry, whitespace ++ "\r"); + if (trim.len != entry.len) { + std.mem.copyForwards(u8, entry, trim); + entry = entry[0..trim.len]; + } - const len = key.len + value.len + 1; - if (entry.len != len) { - std.mem.copyForwards(u8, entry, key); - entry[key.len] = '='; - std.mem.copyForwards(u8, entry[key.len + 1 ..], value); - entry = entry[0..len]; - } - } + // Ignore blank lines and comments + if (entry.len == 0 or entry[0] == '#') continue; + break entry; + } else return null; - break :buf entry; + if (mem.indexOf(u8, entry, "=")) |idx| { + const key = std.mem.trim(u8, entry[0..idx], whitespace); + const value = value: { + var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); + + // Detect a quoted string. + if (value.len >= 2 and + value[0] == '"' and + value[value.len - 1] == '"') + { + // Trim quotes since our CLI args processor expects + // quotes to already be gone. + value = value[1 .. value.len - 1]; } + + break :value value; }; - // We need to reslice so that we include our '--' at the beginning - // of our buffer so that we can trick the CLI parser to treat it - // as CLI args. - return self.entry[0 .. buf.len + 2]; + const len = key.len + value.len + 1; + if (entry.len != len) { + std.mem.copyForwards(u8, entry, key); + entry[key.len] = '='; + std.mem.copyForwards(u8, entry[key.len + 1 ..], value); + entry = entry[0..len]; + } } - /// Returns a location for a diagnostic message. - pub fn location( - self: *const Self, - alloc: Allocator, - ) Allocator.Error!?diags.Location { - // If we have no filepath then we have no location. - if (self.filepath.len == 0) return null; + // We need to reslice so that we include our '--' at the beginning + // of our buffer so that we can trick the CLI parser to treat it + // as CLI args. + return self.entry[0 .. entry.len + 2]; + } - return .{ .file = .{ - .path = try alloc.dupe(u8, self.filepath), - .line = self.line, - } }; - } - }; -} + /// Returns a location for a diagnostic message. + pub fn location( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!?diags.Location { + // If we have no filepath then we have no location. + if (self.filepath.len == 0) return null; -// Constructs a LineIterator (see docs for that). -fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) { - return .{ .r = reader }; -} + return .{ .file = .{ + .path = try alloc.dupe(u8, self.filepath), + .line = self.line, + } }; + } +}; /// An iterator valid for arg parsing from a slice. pub const SliceIterator = struct { @@ -1526,7 +1525,7 @@ pub fn sliceIterator(slice: []const []const u8) SliceIterator { test "LineIterator" { const testing = std.testing; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\A \\B=42 \\C @@ -1541,7 +1540,7 @@ test "LineIterator" { \\F= "value " ); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=42", iter.next().?); try testing.expectEqualStrings("--C", iter.next().?); @@ -1554,9 +1553,9 @@ test "LineIterator" { test "LineIterator end in newline" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A\n\n"); + var reader: std.Io.Reader = .fixed("A\n\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); @@ -1564,9 +1563,9 @@ test "LineIterator end in newline" { test "LineIterator spaces around '='" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A = B\n\n"); + var reader: std.Io.Reader = .fixed("A = B\n\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A=B", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); @@ -1574,18 +1573,18 @@ test "LineIterator spaces around '='" { test "LineIterator no value" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A = \n\n"); + var reader: std.Io.Reader = .fixed("A = \n\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A=", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } test "LineIterator with CRLF line endings" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A\r\nB = C\r\n"); + var reader: std.Io.Reader = .fixed("A\r\nB = C\r\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=C", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); diff --git a/src/cli/boo.zig b/src/cli/boo.zig index 72b282ef6..756b6d77a 100644 --- a/src/cli/boo.zig +++ b/src/cli/boo.zig @@ -6,7 +6,7 @@ const Allocator = std.mem.Allocator; const help_strings = @import("help_strings"); const vaxis = @import("vaxis"); -const framedata = @import("framedata"); +const framedata = @embedFile("framedata"); const vxfw = vaxis.vxfw; @@ -218,17 +218,20 @@ var frames: []const []const u8 = undefined; /// Decompress the frames into a slice of individual frames fn decompressFrames(gpa: Allocator) !void { - var fbs = std.io.fixedBufferStream(framedata.compressed); - var list = std.ArrayList(u8).init(gpa); + var src: std.Io.Reader = .fixed(framedata); - try std.compress.flate.decompress(fbs.reader(), list.writer()); - decompressed_data = try list.toOwnedSlice(); + // var buf: [std.compress.flate.max_window_len]u8 = undefined; + var decompress: std.compress.flate.Decompress = .init(&src, .raw, &.{}); - var frame_list = try std.ArrayList([]const u8).initCapacity(gpa, 235); + var out: std.Io.Writer.Allocating = .init(gpa); + _ = try decompress.reader.streamRemaining(&out.writer); + decompressed_data = try out.toOwnedSlice(); + + var frame_list: std.ArrayList([]const u8) = try .initCapacity(gpa, 235); var frame_iter = std.mem.splitScalar(u8, decompressed_data, '\x01'); while (frame_iter.next()) |frame| { - try frame_list.append(frame); + try frame_list.append(gpa, frame); } - frames = try frame_list.toOwnedSlice(); + frames = try frame_list.toOwnedSlice(gpa); } diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index c6a383563..f0940fdab 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -38,21 +38,35 @@ pub fn run(alloc_gpa: Allocator) !u8 { try args.parse(Options, alloc_gpa, &opts, &iter); } + var buffer: [1024]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&buffer); + const stdout = &stdout_writer.interface; + + const result = runInner(alloc, &stdout_file, stdout); + stdout.flush() catch {}; + return result; +} + +fn runInner( + alloc: Allocator, + stdout_file: *std.fs.File, + stdout: *std.Io.Writer, +) !u8 { const crash_dir = try crash.defaultDir(alloc); - var reports = std.ArrayList(crash.Report).init(alloc); + var reports: std.ArrayList(crash.Report) = .empty; + errdefer reports.deinit(alloc); var it = try crash_dir.iterator(); - while (try it.next()) |report| try reports.append(.{ + while (try it.next()) |report| try reports.append(alloc, .{ .name = try alloc.dupe(u8, report.name), .mtime = report.mtime, }); - const stdout = std.io.getStdOut(); - // If we have no reports, then we're done. If we have a tty then we // print a message, otherwise we do nothing. if (reports.items.len == 0) { - if (std.posix.isatty(stdout.handle)) { + if (std.posix.isatty(stdout_file.handle)) { try stdout.writeAll("No crash reports! 👻\n"); } return 0; @@ -60,16 +74,15 @@ pub fn run(alloc_gpa: Allocator) !u8 { std.mem.sort(crash.Report, reports.items, {}, lt); - const writer = stdout.writer(); for (reports.items) |report| { var buf: [128]u8 = undefined; const now = std.time.nanoTimestamp(); const diff = now - report.mtime; const since = if (diff <= 0) "now" else s: { const d = Config.Duration{ .duration = @intCast(diff) }; - break :s try std.fmt.bufPrint(&buf, "{s} ago", .{d.round(std.time.ns_per_s)}); + break :s try std.fmt.bufPrint(&buf, "{f} ago", .{d.round(std.time.ns_per_s)}); }; - try writer.print("{s} ({s})\n", .{ report.name, since }); + try stdout.print("{s} ({s})\n", .{ report.name, since }); } return 0; diff --git a/src/cli/diagnostics.zig b/src/cli/diagnostics.zig index 2c6cb3b30..2af8bb4f8 100644 --- a/src/cli/diagnostics.zig +++ b/src/cli/diagnostics.zig @@ -16,7 +16,7 @@ pub const Diagnostic = struct { message: [:0]const u8, /// Write the full user-friendly diagnostic message to the writer. - pub fn write(self: *const Diagnostic, writer: anytype) !void { + pub fn format(self: *const Diagnostic, writer: *std.Io.Writer) !void { switch (self.location) { .none => {}, .cli => |index| try writer.print("cli:{}:", .{index}), @@ -157,11 +157,14 @@ pub const DiagnosticList = struct { errdefer _ = self.list.pop(); if (comptime precompute_enabled) { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - try diag.write(buf.writer()); + var stream: std.Io.Writer.Allocating = .init(alloc); + defer stream.deinit(); + diag.format(&stream.writer) catch |err| switch (err) { + // WriteFailed in this instance can only mean an OOM + error.WriteFailed => return error.OutOfMemory, + }; - const owned: [:0]const u8 = try buf.toOwnedSliceSentinel(0); + const owned: [:0]const u8 = try stream.toOwnedSliceSentinel(0); errdefer alloc.free(owned); try self.precompute.messages.append(alloc, owned); diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index 116843037..f103ca4a0 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -47,7 +47,9 @@ pub fn run(alloc: Allocator) !u8 { // not using `exec` anymore and because this command isn't performance // critical where setting up the defer cleanup is a problem. - const stderr = std.io.getStdErr().writer(); + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; var opts: Options = .{}; defer opts.deinit(); @@ -58,6 +60,13 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } + const result = runInner(alloc, stderr); + // Flushing *shouldn't* fail but... + stderr.flush() catch {}; + return result; +} + +fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { // We load the configuration once because that will write our // default configuration files to disk. We don't use the config. var config = try Config.load(alloc); @@ -133,23 +142,13 @@ pub fn run(alloc: Allocator) !u8 { // so this is not a big deal. comptime assert(builtin.link_libc); - var buf: std.ArrayListUnmanaged(u8) = .empty; - errdefer buf.deinit(alloc); - - const writer = buf.writer(alloc); - var shellescape: internal_os.ShellEscapeWriter(std.ArrayListUnmanaged(u8).Writer) = .init(writer); - var shellescapewriter = shellescape.writer(); - - try writer.writeAll(editor); - try writer.writeByte(' '); - try shellescapewriter.writeAll(path); - - const command = try buf.toOwnedSliceSentinel(alloc, 0); - defer alloc.free(command); - + const editorZ = try alloc.dupeZ(u8, editor); + defer alloc.free(editorZ); + const pathZ = try alloc.dupeZ(u8, path); + defer alloc.free(pathZ); const err = std.posix.execvpeZ( - "sh", - &.{ "sh", "-c", command }, + editorZ, + &.{ editorZ, pathZ }, std.c.environ, ); diff --git a/src/cli/ghostty.zig b/src/cli/ghostty.zig index adb715d68..f6ac7d93d 100644 --- a/src/cli/ghostty.zig +++ b/src/cli/ghostty.zig @@ -107,12 +107,18 @@ pub const Action = enum { // for all commands by just changing this one place. if (std.mem.eql(u8, field.name, @tagName(self))) { - const stdout = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; const text = @field(help_strings.Action, field.name) ++ "\n"; stdout.writeAll(text) catch |write_err| { std.log.warn("failed to write help text: {}\n", .{write_err}); break :err 1; }; + stdout.flush() catch |flush_err| { + std.log.warn("failed to flush help text: {}\n", .{flush_err}); + break :err 1; + }; break :err 0; } diff --git a/src/cli/help.zig b/src/cli/help.zig index 0528dc1c2..a2b4dde80 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -30,7 +30,9 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; try stdout.writeAll( \\Usage: ghostty [+action] [options] \\ @@ -70,6 +72,7 @@ pub fn run(alloc: Allocator) !u8 { \\where `` is one of actions listed above. \\ ); + try stdout.flush(); return 0; } diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index 6f5ce06a2..682eed251 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -37,8 +37,15 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); - try helpgen_actions.generate(stdout, .plaintext, opts.docs, std.heap.page_allocator); + var stdout: std.fs.File = .stdout(); + var buffer: [4096]u8 = undefined; + var stdout_writer = stdout.writer(&buffer); + try helpgen_actions.generate( + &stdout_writer.interface, + .plaintext, + opts.docs, + std.heap.page_allocator, + ); return 0; } diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig index 63945de99..50c12a693 100644 --- a/src/cli/list_colors.zig +++ b/src/cli/list_colors.zig @@ -39,11 +39,9 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut(); - - var keys = std.ArrayList([]const u8).init(alloc); - defer keys.deinit(); - for (x11_color.map.keys()) |key| try keys.append(key); + var keys: std.ArrayList([]const u8) = .empty; + defer keys.deinit(alloc); + for (x11_color.map.keys()) |key| try keys.append(alloc, key); std.mem.sortUnstable([]const u8, keys.items, {}, struct { fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool { @@ -52,12 +50,15 @@ pub fn run(alloc: Allocator) !u8 { }.lessThan); // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 + var stdout: std.fs.File = .stdout(); if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), keys.items); } else { - const writer = stdout.writer(); + var buffer: [4096]u8 = undefined; + var stdout_writer = stdout.writer(&buffer); + const writer = &stdout_writer.interface; for (keys.items) |name| { const rgb = x11_color.map.get(name).?; try writer.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ @@ -74,19 +75,17 @@ pub fn run(alloc: Allocator) !u8 { fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { // Set up vaxis - var tty = try vaxis.Tty.init(); + var buf: [1024]u8 = undefined; + var tty = try vaxis.Tty.init(&buf); defer tty.deinit(); var vx = try vaxis.init(alloc, .{}); - defer vx.deinit(alloc, tty.anyWriter()); + defer vx.deinit(alloc, tty.writer()); // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an // event loop to auto-enable it. vx.caps.unicode = .unicode; - try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set); - defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; - - var buf_writer = tty.bufferedWriter(); - const writer = buf_writer.writer().any(); + try tty.writer().writeAll(vaxis.ctlseqs.unicode_set); + defer tty.writer().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; const winsize: vaxis.Winsize = switch (builtin.os.tag) { // We use some default, it doesn't really matter for what @@ -100,7 +99,7 @@ fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { else => try vaxis.Tty.getWinsize(tty.fd), }; - try vx.resize(alloc, tty.anyWriter(), winsize); + try vx.resize(alloc, tty.writer(), winsize); const win = vx.window(); @@ -203,11 +202,8 @@ fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { } // output the data - try vx.prettyPrint(writer); + try vx.prettyPrint(tty.writer()); } - // be sure to flush! - try buf_writer.flush(); - return 0; } diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index 58246d3ad..396c4e8a6 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -77,7 +77,9 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { // Its possible to build Ghostty without font discovery! if (comptime font.Discover == void) { - const stderr = std.io.getStdErr().writer(); + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; try stderr.print( \\Ghostty was built without a font discovery mechanism. This is a compile-time \\option. Please review how Ghostty was built from source, contact the @@ -85,15 +87,18 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { , .{}, ); + try stderr.flush(); return 1; } - const stdout = std.io.getStdOut().writer(); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; // We'll be putting our fonts into a list categorized by family // so it is easier to read the output. - var families = std.ArrayList([]const u8).init(alloc); - var map = std.StringHashMap(std.ArrayListUnmanaged([]const u8)).init(alloc); + var families: std.ArrayList([]const u8) = .empty; + var map: std.StringHashMap(std.ArrayListUnmanaged([]const u8)) = .init(alloc); // Look up all available fonts var disco = font.Discover.init(); @@ -123,7 +128,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { const gop = try map.getOrPut(family); if (!gop.found_existing) { - try families.append(family); + try families.append(alloc, family); gop.value_ptr.* = .{}; } try gop.value_ptr.append(alloc, full_name); @@ -155,5 +160,6 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { try stdout.print("\n", .{}); } + try stdout.flush(); return 0; } diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index 94f445eea..a8899a4f5 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -64,27 +64,38 @@ pub fn run(alloc: Allocator) !u8 { var config = if (opts.default) try Config.default(alloc) else try Config.load(alloc); defer config.deinit(); - const stdout = std.io.getStdOut(); + var buffer: [1024]u8 = undefined; + const stdout: std.fs.File = .stdout(); + var stdout_writer = stdout.writer(&buffer); + const writer = &stdout_writer.interface; - // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 - if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { + if (tui.can_pretty_print and !opts.plain and stdout.isTty()) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), config.keybind); } else { try config.keybind.formatEntryDocs( - configpkg.entryFormatter("keybind", stdout.writer()), + configpkg.entryFormatter("keybind", writer), opts.docs, ); } + // Don't forget to flush! + try writer.flush(); return 0; } -const TriggerList = std.SinglyLinkedList(Binding.Trigger); +const TriggerNode = struct { + data: Binding.Trigger, + node: std.SinglyLinkedList.Node = .{}, + + pub fn get(node: *std.SinglyLinkedList.Node) *TriggerNode { + return @fieldParentPtr("node", node); + } +}; const ChordBinding = struct { - triggers: TriggerList, + triggers: std.SinglyLinkedList, action: Binding.Action, // Order keybinds based on various properties @@ -109,7 +120,8 @@ const ChordBinding = struct { const lhs_count: usize = blk: { var count: usize = 0; var maybe_trigger = lhs.triggers.first; - while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) { + while (maybe_trigger) |node| : (maybe_trigger = node.next) { + const trigger: *TriggerNode = .get(node); if (trigger.data.mods.super) count += 1; if (trigger.data.mods.ctrl) count += 1; if (trigger.data.mods.shift) count += 1; @@ -120,7 +132,8 @@ const ChordBinding = struct { const rhs_count: usize = blk: { var count: usize = 0; var maybe_trigger = rhs.triggers.first; - while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) { + while (maybe_trigger) |node| : (maybe_trigger = node.next) { + const trigger: *TriggerNode = .get(node); if (trigger.data.mods.super) count += 1; if (trigger.data.mods.ctrl) count += 1; if (trigger.data.mods.shift) count += 1; @@ -137,8 +150,8 @@ const ChordBinding = struct { var l_trigger = lhs.triggers.first; var r_trigger = rhs.triggers.first; while (l_trigger != null and r_trigger != null) { - const l_int = l_trigger.?.data.mods.int(); - const r_int = r_trigger.?.data.mods.int(); + const l_int = TriggerNode.get(l_trigger.?).data.mods.int(); + const r_int = TriggerNode.get(r_trigger.?).data.mods.int(); if (l_int != r_int) { return l_int > r_int; @@ -154,13 +167,13 @@ const ChordBinding = struct { while (l_trigger != null and r_trigger != null) { const lhs_key: c_int = blk: { - switch (l_trigger.?.data.key) { + switch (TriggerNode.get(l_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), } }; const rhs_key: c_int = blk: { - switch (r_trigger.?.data.key) { + switch (TriggerNode.get(r_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), } @@ -186,19 +199,18 @@ const ChordBinding = struct { fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { // Set up vaxis - var tty = try vaxis.Tty.init(); + var buf: [1024]u8 = undefined; + var tty = try vaxis.Tty.init(&buf); defer tty.deinit(); var vx = try vaxis.init(alloc, .{}); - defer vx.deinit(alloc, tty.anyWriter()); + const writer = tty.writer(); + defer vx.deinit(alloc, writer); // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an // event loop to auto-enable it. vx.caps.unicode = .unicode; - try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set); - defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; - - var buf_writer = tty.bufferedWriter(); - const writer = buf_writer.writer().any(); + try writer.writeAll(vaxis.ctlseqs.unicode_set); + defer writer.writeAll(vaxis.ctlseqs.unicode_reset) catch {}; const winsize: vaxis.Winsize = switch (builtin.os.tag) { // We use some default, it doesn't really matter for what @@ -212,7 +224,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { else => try vaxis.Tty.getWinsize(tty.fd), }; - try vx.resize(alloc, tty.anyWriter(), winsize); + try vx.resize(alloc, writer, winsize); const win = vx.window(); @@ -234,7 +246,9 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; var maybe_trigger = bind.triggers.first; - while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) { + while (maybe_trigger) |node| : (maybe_trigger = node.next) { + const trigger: *TriggerNode = .get(node); + if (trigger.data.mods.super) { result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); @@ -252,18 +266,18 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } const key = switch (trigger.data.key) { - .physical => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}), + .physical => |k| try std.fmt.allocPrint(alloc, "{t}", .{k}), .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}), }; result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col }); // Print a separator between chorded keys - if (trigger.next != null) { + if (trigger.node.next != null) { result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col }); } } - const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action}); + const action = try std.fmt.allocPrint(alloc, "{f}", .{bind.action}); // If our action has an argument, we print the argument in a different color if (std.mem.indexOfScalar(u8, action, ':')) |idx| { _ = win.print(&.{ @@ -276,29 +290,33 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { } try vx.prettyPrint(writer); } - try buf_writer.flush(); + try writer.flush(); return 0; } -fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !struct { []ChordBinding, u16 } { +fn iterateBindings( + alloc: Allocator, + iter: anytype, + win: *const vaxis.Window, +) !struct { []ChordBinding, u16 } { var widest_chord: u16 = 0; - var bindings = std.ArrayList(ChordBinding).init(alloc); + var bindings: std.ArrayList(ChordBinding) = .empty; while (iter.next()) |bind| { const width = blk: { - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); const t = bind.key_ptr.*; - if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{}); - if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl + ", .{}); - if (t.mods.alt) try std.fmt.format(buf.writer(), "alt + ", .{}); - if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{}); + if (t.mods.super) try buf.writer.print("super + ", .{}); + if (t.mods.ctrl) try buf.writer.print("ctrl + ", .{}); + if (t.mods.alt) try buf.writer.print("alt + ", .{}); + if (t.mods.shift) try buf.writer.print("shift + ", .{}); switch (t.key) { - .physical => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}), - .unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}), + .physical => |k| try buf.writer.print("{t}", .{k}), + .unicode => |c| try buf.writer.print("{u}", .{c}), } - break :blk win.gwidth(buf.items); + break :blk win.gwidth(buf.written()); }; switch (bind.value_ptr.*) { @@ -310,28 +328,28 @@ fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !s // Prepend the current keybind onto the list of sub-binds for (sub_bindings) |*nb| { - const prepend_node = try alloc.create(TriggerList.Node); - prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* }; - nb.triggers.prepend(prepend_node); + const prepend_node = try alloc.create(TriggerNode); + prepend_node.* = .{ .data = bind.key_ptr.* }; + nb.triggers.prepend(&prepend_node.node); } // Add the longest sub-bind width to the current bind width along with a padding // of 5 for the ' > ' spacer widest_chord = @max(widest_chord, width + max_width + 5); - try bindings.appendSlice(sub_bindings); + try bindings.appendSlice(alloc, sub_bindings); }, .leaf => |leaf| { - const node = try alloc.create(TriggerList.Node); - node.* = TriggerList.Node{ .data = bind.key_ptr.* }; - const triggers = TriggerList{ - .first = node, - }; + const node = try alloc.create(TriggerNode); + node.* = .{ .data = bind.key_ptr.* }; widest_chord = @max(widest_chord, width); - try bindings.append(.{ .triggers = triggers, .action = leaf.action }); + try bindings.append(alloc, .{ + .triggers = .{ .first = &node.node }, + .action = leaf.action, + }); }, } } - return .{ try bindings.toOwnedSlice(), widest_chord }; + return .{ try bindings.toOwnedSlice(alloc), widest_chord }; } diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 0c0acfe84..cc6cfaf3e 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -57,9 +57,12 @@ const ThemeListElement = struct { .host = .{ .raw = "" }, .path = .{ .raw = self.path }, }; - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); errdefer buf.deinit(); - try uri.writeToStream(.{ .scheme = true, .authority = true, .path = true }, buf.writer()); + try uri.writeToStream( + &buf.writer, + .{ .scheme = true, .authority = true, .path = true }, + ); return buf.toOwnedSlice(); } }; @@ -114,8 +117,14 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var arena = std.heap.ArenaAllocator.init(gpa_alloc); const alloc = arena.allocator(); - const stderr = std.io.getStdErr().writer(); - const stdout = std.io.getStdOut().writer(); + var stdout_buf: [4096]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&stdout_buf); + const stdout = &stdout_writer.interface; + + var stderr_buf: [4096]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_writer.interface; const resources_dir = global_state.resources_dir.app(); if (resources_dir == null) @@ -124,9 +133,9 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var count: usize = 0; - var themes = std.ArrayList(ThemeListElement).init(alloc); + var themes: std.ArrayList(ThemeListElement) = .empty; - var it = themepkg.LocationIterator{ .arena_alloc = arena.allocator() }; + var it: themepkg.LocationIterator = .{ .arena_alloc = arena.allocator() }; while (try it.next()) |loc| { var dir = std.fs.cwd().openDir(loc.dir, .{ .iterate = true }) catch |err| switch (err) { @@ -148,7 +157,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { count += 1; const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }); - try themes.append(.{ + try themes.append(alloc, .{ .path = path, .location = loc.location, .theme = try alloc.dupe(u8, entry.name), @@ -166,18 +175,20 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan); - if (tui.can_pretty_print and !opts.plain and std.posix.isatty(std.io.getStdOut().handle)) { + if (tui.can_pretty_print and !opts.plain and stdout_file.isTty()) { try preview(gpa_alloc, themes.items, opts.color); return 0; } for (themes.items) |theme| { if (opts.path) - try stdout.print("{s} ({s}) {s}\n", .{ theme.theme, @tagName(theme.location), theme.path }) + try stdout.print("{s} ({t}) {s}\n", .{ theme.theme, theme.location, theme.path }) else - try stdout.print("{s} ({s})\n", .{ theme.theme, @tagName(theme.location) }); + try stdout.print("{s} ({t})\n", .{ theme.theme, theme.location }); } + // Don't forget to flush! + try stdout.flush(); return 0; } @@ -209,23 +220,28 @@ const Preview = struct { text_input: vaxis.widgets.TextInput, theme_filter: ColorScheme, - pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !*Preview { + pub fn init( + allocator: std.mem.Allocator, + themes: []ThemeListElement, + theme_filter: ColorScheme, + buf: []u8, + ) !*Preview { const self = try allocator.create(Preview); self.* = .{ .allocator = allocator, .should_quit = false, - .tty = try vaxis.Tty.init(), + .tty = try .init(buf), .vx = try vaxis.init(allocator, .{}), .mouse = null, .themes = themes, - .filtered = try std.ArrayList(usize).initCapacity(allocator, themes.len), + .filtered = try .initCapacity(allocator, themes.len), .current = 0, .window = 0, .hex = false, .mode = .normal, .color_scheme = .light, - .text_input = vaxis.widgets.TextInput.init(allocator, &self.vx.unicode), + .text_input = .init(allocator, &self.vx.unicode), .theme_filter = theme_filter, }; @@ -236,9 +252,9 @@ const Preview = struct { pub fn deinit(self: *Preview) void { const allocator = self.allocator; - self.filtered.deinit(); + self.filtered.deinit(allocator); self.text_input.deinit(); - self.vx.deinit(allocator, self.tty.anyWriter()); + self.vx.deinit(allocator, self.tty.writer()); self.tty.deinit(); allocator.destroy(self); } @@ -251,12 +267,14 @@ const Preview = struct { try loop.init(); try loop.start(); - try self.vx.enterAltScreen(self.tty.anyWriter()); - try self.vx.setTitle(self.tty.anyWriter(), "👻 Ghostty Theme Preview 👻"); - try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s); - try self.vx.setMouseMode(self.tty.anyWriter(), true); + const writer = self.tty.writer(); + + try self.vx.enterAltScreen(writer); + try self.vx.setTitle(writer, "👻 Ghostty Theme Preview 👻"); + try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s); + try self.vx.setMouseMode(writer, true); if (self.vx.caps.color_scheme_updates) - try self.vx.subscribeToColorSchemeUpdates(self.tty.anyWriter()); + try self.vx.subscribeToColorSchemeUpdates(writer); while (!self.should_quit) { var arena = std.heap.ArenaAllocator.init(self.allocator); @@ -269,9 +287,8 @@ const Preview = struct { } try self.draw(alloc); - var buffered = self.tty.bufferedWriter(); - try self.vx.render(buffered.writer().any()); - try buffered.flush(); + try self.vx.render(writer); + try writer.flush(); } } @@ -308,11 +325,11 @@ const Preview = struct { const string = try std.ascii.allocLowerString(self.allocator, buffer); defer self.allocator.free(string); - var tokens = std.ArrayList([]const u8).init(self.allocator); - defer tokens.deinit(); + var tokens: std.ArrayList([]const u8) = .empty; + defer tokens.deinit(self.allocator); var it = std.mem.tokenizeScalar(u8, string, ' '); - while (it.next()) |token| try tokens.append(token); + while (it.next()) |token| try tokens.append(self.allocator, token); for (self.themes, 0..) |*theme, i| { try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path); @@ -322,13 +339,13 @@ const Preview = struct { .to_lower = true, .plain = true, }); - if (theme.rank != null) try self.filtered.append(i); + if (theme.rank != null) try self.filtered.append(self.allocator, i); } } else { for (self.themes, 0..) |*theme, i| { try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path); if (shouldIncludeTheme(self.theme_filter, theme_config)) { - try self.filtered.append(i); + try self.filtered.append(self.allocator, i); theme.rank = null; } } @@ -421,13 +438,13 @@ const Preview = struct { self.hex = false; if (key.matches('c', .{})) try self.vx.copyToSystemClipboard( - self.tty.anyWriter(), + self.tty.writer(), self.themes[self.filtered.items[self.current]].theme, alloc, ) else if (key.matches('c', .{ .shift = true })) try self.vx.copyToSystemClipboard( - self.tty.anyWriter(), + self.tty.writer(), self.themes[self.filtered.items[self.current]].path, alloc, ); @@ -471,7 +488,7 @@ const Preview = struct { }, .color_scheme => |color_scheme| self.color_scheme = color_scheme, .mouse => |mouse| self.mouse = mouse, - .winsize => |ws| try self.vx.resize(self.allocator, self.tty.anyWriter(), ws), + .winsize => |ws| try self.vx.resize(self.allocator, self.tty.writer(), ws), } } @@ -1044,14 +1061,14 @@ const Preview = struct { ); } - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); for (config._diagnostics.items(), 0..) |diag, captured_i| { const i: u16 = @intCast(captured_i); - try diag.write(buf.writer()); + try diag.format(&buf.writer); _ = child.printSegment( .{ - .text = buf.items, + .text = buf.written(), .style = self.ui_err(), }, .{ @@ -1319,7 +1336,7 @@ const Preview = struct { .{ .text = "const ", .style = color5 }, .{ .text = "stdout ", .style = standard }, .{ .text = "=", .style = color5 }, - .{ .text = " std.io.getStdOut().writer();", .style = standard }, + .{ .text = " std.Io.getStdOut().writer();", .style = standard }, }, .{ .row_offset = 7, @@ -1651,7 +1668,13 @@ fn color(config: Config, palette: usize) vaxis.Color { const lorem_ipsum = @embedFile("lorem_ipsum.txt"); fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !void { - var app = try Preview.init(allocator, themes, theme_filter); + var buf: [4096]u8 = undefined; + var app = try Preview.init( + allocator, + themes, + theme_filter, + &buf, + ); defer app.deinit(); try app.run(); } diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 343175b4e..f3f4740d1 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -26,7 +26,7 @@ pub const Options = struct { // If it's not `-e` continue with the standard argument parsning. if (!std.mem.eql(u8, arg, "-e")) return true; - var arguments: std.ArrayListUnmanaged([:0]const u8) = .empty; + var arguments: std.ArrayList([:0]const u8) = .empty; errdefer { for (arguments.items) |argument| alloc.free(argument); arguments.deinit(alloc); @@ -99,12 +99,21 @@ pub const Options = struct { pub fn run(alloc: Allocator) !u8 { var iter = try args.argsIterator(alloc); defer iter.deinit(); - return try runArgs(alloc, &iter); + + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; + + const result = runArgs(alloc, &iter, stderr); + stderr.flush() catch {}; + return result; } -fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { - const stderr = std.io.getStdErr().writer(); - +fn runArgs( + alloc_gpa: Allocator, + argsIter: anytype, + stderr: *std.Io.Writer, +) !u8 { var opts: Options = .{}; defer opts.deinit(); @@ -126,9 +135,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { inner: inline for (@typeInfo(Options).@"struct".fields) |field| { if (field.name[0] == '_') continue :inner; if (std.mem.eql(u8, field.name, diagnostic.key)) { - try stderr.writeAll("config error: "); - try diagnostic.write(stderr); - try stderr.writeAll("\n"); + try stderr.print("config error: {f}\n", .{diagnostic}); exit = true; } } diff --git a/src/cli/show_config.zig b/src/cli/show_config.zig index 3f22c75c2..1b73b77c1 100644 --- a/src/cli/show_config.zig +++ b/src/cli/show_config.zig @@ -77,7 +77,10 @@ pub fn run(alloc: Allocator) !u8 { // For some reason `std.fmt.format` isn't working here but it works in // tests so we just do configfmt.format. - const stdout = std.io.getStdOut().writer(); - try configfmt.format("", .{}, stdout); + var stdout: std.fs.File = .stdout(); + var buffer: [4096]u8 = undefined; + var stdout_writer = stdout.writer(&buffer); + try configfmt.format(&stdout_writer.interface); + try stdout_writer.end(); return 0; } diff --git a/src/cli/show_face.zig b/src/cli/show_face.zig index e3b596bcd..9dee777b3 100644 --- a/src/cli/show_face.zig +++ b/src/cli/show_face.zig @@ -64,13 +64,32 @@ pub const Options = struct { pub fn run(alloc: Allocator) !u8 { var iter = try args.argsIterator(alloc); defer iter.deinit(); - return try runArgs(alloc, &iter); + + var stdout_buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + + var stderr_buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stdout().writer(&stderr_buffer); + const stderr = &stderr_writer.interface; + + const result = runArgs( + alloc, + &iter, + stdout, + stderr, + ); + stdout.flush() catch {}; + stderr.flush() catch {}; + return result; } -fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - +fn runArgs( + alloc_gpa: Allocator, + argsIter: anytype, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, +) !u8 { // Its possible to build Ghostty without font discovery! if (comptime font.Discover == void) { try stderr.print( @@ -104,9 +123,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { inner: inline for (@typeInfo(Options).@"struct".fields) |field| { if (field.name[0] == '_') continue :inner; if (std.mem.eql(u8, field.name, diagnostic.key)) { - try stderr.writeAll("config error: "); - try diagnostic.write(stderr); - try stderr.writeAll("\n"); + try stderr.print("config error: {f}\n", .{diagnostic}); exit = true; } } @@ -138,9 +155,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { if (field.name[0] == '_') continue :inner; if (std.mem.eql(u8, field.name, diagnostic.key) and (diagnostic.location == .none or diagnostic.location == .cli)) continue :outer; } - try stderr.writeAll("config error: "); - try diagnostic.write(stderr); - try stderr.writeAll("\n"); + try stderr.print("config error: {f}\n", .{diagnostic}); } } @@ -189,8 +204,8 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { fn lookup( alloc: std.mem.Allocator, - stdout: anytype, - stderr: anytype, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, font_grid: *font.SharedGrid, style: font.Style, presentation: ?font.Presentation, diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index db138cf37..608155dfd 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -57,8 +57,6 @@ pub fn clear(self: DiskCache) !void { pub const AddResult = enum { added, updated }; -pub const AddError = std.fs.Dir.MakeError || std.fs.File.OpenError || std.fs.File.LockError || std.fs.File.ReadError || std.fs.File.WriteError || std.posix.RealPathError || std.posix.RenameError || Allocator.Error || error{ HostnameIsInvalid, CacheIsLocked }; - /// Add or update a hostname entry in the cache. /// Returns AddResult.added for new entries or AddResult.updated for existing ones. /// The cache file is created if it doesn't exist with secure permissions (0600). @@ -66,7 +64,7 @@ pub fn add( self: DiskCache, alloc: Allocator, hostname: []const u8, -) AddError!AddResult { +) !AddResult { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Create cache directory if needed @@ -130,15 +128,13 @@ pub fn add( return result; } -pub const RemoveError = std.fs.Dir.OpenError || std.fs.File.OpenError || std.fs.File.ReadError || std.fs.File.WriteError || std.posix.RealPathError || std.posix.RenameError || Allocator.Error || error{ HostnameIsInvalid, CacheIsLocked }; - /// Remove a hostname entry from the cache. /// No error is returned if the hostname doesn't exist or the cache file is missing. pub fn remove( self: DiskCache, alloc: Allocator, hostname: []const u8, -) RemoveError!void { +) !void { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Open our file @@ -199,7 +195,7 @@ pub fn contains( return entries.contains(hostname); } -fn fixupPermissions(file: std.fs.File) !void { +fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.ChmodError)!void { // Windows does not support chmod if (comptime builtin.os.tag == .windows) return; @@ -211,14 +207,12 @@ fn fixupPermissions(file: std.fs.File) !void { } } -pub const WriteCacheFileError = std.fs.Dir.OpenError || std.fs.File.OpenError || std.fs.File.WriteError || std.fs.Dir.RealPathAllocError || std.posix.RealPathError || std.posix.RenameError || error{FileTooBig}; - fn writeCacheFile( self: DiskCache, alloc: Allocator, entries: std.StringHashMap(Entry), expire_days: ?u32, -) WriteCacheFileError!void { +) !void { var td: TempDir = try .init(); defer td.deinit(); @@ -227,14 +221,18 @@ fn writeCacheFile( const tmp_path = try td.dir.realpathAlloc(alloc, "ssh-cache"); defer alloc.free(tmp_path); - const writer = tmp_file.writer(); + var buf: [1024]u8 = undefined; + var writer = tmp_file.writer(&buf); var iter = entries.iterator(); while (iter.next()) |kv| { // Only write non-expired entries if (kv.value_ptr.isExpired(expire_days)) continue; - try kv.value_ptr.format(writer); + try kv.value_ptr.format(&writer.interface); } + // Don't forget to flush!! + try writer.interface.flush(); + // Atomic replace try std.fs.renameAbsolute(tmp_path, self.path); } @@ -278,8 +276,12 @@ pub fn deinitEntries( fn readEntries( alloc: Allocator, file: std.fs.File, -) (std.fs.File.ReadError || Allocator.Error || error{FileTooBig})!std.StringHashMap(Entry) { - const content = try file.readToEndAlloc(alloc, MAX_CACHE_SIZE); +) !std.StringHashMap(Entry) { + var reader = file.reader(&.{}); + const content = try reader.interface.allocRemaining( + alloc, + .limited(MAX_CACHE_SIZE), + ); defer alloc.free(content); var entries = std.StringHashMap(Entry).init(alloc); @@ -403,10 +405,12 @@ test "disk cache clear" { // Create our path var td: TempDir = try .init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("cache", .{}); defer file.close(); - try file.writer().writeAll("HELLO!"); + var file_writer = file.writer(&buf); + try file_writer.interface.writeAll("HELLO!"); } const path = try td.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); @@ -429,10 +433,14 @@ test "disk cache operations" { // Create our path var td: TempDir = try .init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("cache", .{}); defer file.close(); - try file.writer().writeAll("HELLO!"); + var file_writer = file.writer(&buf); + const writer = &file_writer.interface; + try writer.writeAll("HELLO!"); + try writer.flush(); } const path = try td.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); diff --git a/src/cli/ssh-cache/Entry.zig b/src/cli/ssh-cache/Entry.zig index 3a691be80..f3403dbd4 100644 --- a/src/cli/ssh-cache/Entry.zig +++ b/src/cli/ssh-cache/Entry.zig @@ -33,7 +33,7 @@ pub fn parse(line: []const u8) ?Entry { }; } -pub fn format(self: Entry, writer: anytype) !void { +pub fn format(self: Entry, writer: *std.Io.Writer) !void { try writer.print( "{s}|{d}|{s}\n", .{ self.hostname, self.timestamp, self.terminfo_version }, diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 1099f0112..9434e9771 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -61,9 +61,30 @@ pub fn run(alloc_gpa: Allocator) !u8 { try args.parse(Options, alloc_gpa, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); + var stdout_buffer: [1024]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + var stderr_buffer: [1024]u8 = undefined; + var stderr_file: std.fs.File = .stderr(); + var stderr_writer = stderr_file.writer(&stderr_buffer); + const stderr = &stderr_writer.interface; + + const result = runInner(alloc, opts, stdout, stderr); + + // Flushing *shouldn't* fail but... + stdout.flush() catch {}; + stderr.flush() catch {}; + return result; +} + +pub fn runInner( + alloc: Allocator, + opts: Options, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, +) !u8 { // Setup our disk cache to the standard location const cache_path = try DiskCache.defaultPath(alloc, "ghostty"); const cache: DiskCache = .{ .path = cache_path }; @@ -165,7 +186,7 @@ pub fn run(alloc_gpa: Allocator) !u8 { fn listEntries( alloc: Allocator, entries: *const std.StringHashMap(Entry), - writer: anytype, + writer: *std.Io.Writer, ) !void { if (entries.count() == 0) { try writer.print("No hosts in cache.\n", .{}); @@ -173,12 +194,12 @@ fn listEntries( } // Sort entries by hostname for consistent output - var items = std.ArrayList(Entry).init(alloc); - defer items.deinit(); + var items: std.ArrayList(Entry) = .empty; + defer items.deinit(alloc); var iter = entries.iterator(); while (iter.next()) |kv| { - try items.append(kv.value_ptr.*); + try items.append(alloc, kv.value_ptr.*); } std.mem.sort(Entry, items.items, {}, struct { diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig index 114843e9a..55d861402 100644 --- a/src/cli/validate_config.zig +++ b/src/cli/validate_config.zig @@ -40,8 +40,19 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + const result = runInner(alloc, opts, stdout); + try stdout_writer.end(); + return result; +} +fn runInner( + alloc: std.mem.Allocator, + opts: Options, + stdout: *std.Io.Writer, +) !u8 { var cfg = try Config.default(alloc); defer cfg.deinit(); @@ -58,15 +69,9 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try cfg.finalize(); if (cfg._diagnostics.items().len > 0) { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - for (cfg._diagnostics.items()) |diag| { - try diag.write(buf.writer()); - try stdout.print("{s}\n", .{buf.items}); - buf.clearRetainingCapacity(); + try stdout.print("{f}\n", .{diag}); } - return 1; } diff --git a/src/cli/version.zig b/src/cli/version.zig index 22608fa88..cf8e66fa6 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -15,8 +15,12 @@ pub const Options = struct {}; /// The `version` command is used to display information about Ghostty. Recognized as /// either `+version` or `--version`. pub fn run(alloc: Allocator) !u8 { - const stdout = std.io.getStdOut().writer(); - const tty = std.io.getStdOut().isTty(); + var buffer: [1024]u8 = undefined; + const stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&buffer); + + const stdout = &stdout_writer.interface; + const tty = stdout_file.isTty(); if (tty) if (build_config.version.build) |commit_hash| { try stdout.print( @@ -29,7 +33,7 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print("Version\n", .{}); try stdout.print(" - version: {s}\n", .{build_config.version_string}); - try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)}); + try stdout.print(" - channel: {t}\n", .{build_config.release_channel}); try stdout.print("Build Config\n", .{}); try stdout.print(" - Zig version : {s}\n", .{builtin.zig_version_string}); @@ -37,20 +41,20 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print(" - app runtime : {}\n", .{build_config.app_runtime}); try stdout.print(" - font engine : {}\n", .{build_config.font_backend}); try stdout.print(" - renderer : {}\n", .{renderer.Renderer}); - try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)}); + try stdout.print(" - libxev : {t}\n", .{xev.backend}); if (comptime build_config.app_runtime == .gtk) { if (comptime builtin.os.tag == .linux) { const kernel_info = internal_os.getKernelInfo(alloc); defer if (kernel_info) |k| alloc.free(k); try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"}); } - try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())}); + try stdout.print(" - desktop env : {t}\n", .{internal_os.desktopEnvironment()}); try stdout.print(" - GTK version :\n", .{}); - try stdout.print(" build : {}\n", .{gtk_version.comptime_version}); - try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()}); + try stdout.print(" build : {f}\n", .{gtk_version.comptime_version}); + try stdout.print(" runtime : {f}\n", .{gtk_version.getRuntimeVersion()}); try stdout.print(" - libadwaita : enabled\n", .{}); - try stdout.print(" build : {}\n", .{adw_version.comptime_version}); - try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()}); + try stdout.print(" build : {f}\n", .{adw_version.comptime_version}); + try stdout.print(" runtime : {f}\n", .{adw_version.getRuntimeVersion()}); if (comptime build_options.x11) { try stdout.print(" - libX11 : enabled\n", .{}); } else { @@ -65,5 +69,8 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print(" - libwayland : disabled\n", .{}); } } + + // Don't forget to flush! + try stdout.flush(); return 0; } diff --git a/src/config/Config.zig b/src/config/Config.zig index 8f811e9a4..caaf5feb8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3417,10 +3417,10 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { defer file.close(); std.log.info("reading configuration file path={s}", .{path}); - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - const Iter = cli.args.LineIterator(@TypeOf(reader)); - var iter: Iter = .{ .r = reader, .filepath = path }; + var buf: [2048]u8 = undefined; + var file_reader = file.reader(&buf); + const reader = &file_reader.interface; + var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try self.loadIter(alloc, &iter); try self.expandPaths(std.fs.path.dirname(path).?); } @@ -3457,8 +3457,10 @@ fn writeConfigTemplate(path: []const u8) !void { } const file = try std.fs.createFileAbsolute(path, .{}); defer file.close(); - try std.fmt.format( - file.writer(), + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + const writer = &file_writer.interface; + try writer.print( @embedFile("./config-template"), .{ .path = path }, ); @@ -3628,17 +3630,17 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { // Next, take all remaining args and use that to build up // a command to execute. - var builder = std.ArrayList([:0]const u8).init(arena_alloc); - errdefer builder.deinit(); + var builder: std.ArrayList([:0]const u8) = .empty; + errdefer builder.deinit(arena_alloc); for (args) |arg_raw| { const arg = std.mem.sliceTo(arg_raw, 0); const copy = try arena_alloc.dupeZ(u8, arg); try self._replay_steps.append(arena_alloc, .{ .arg = copy }); - try builder.append(copy); + try builder.append(arena_alloc, copy); } self.@"_xdg-terminal-exec" = true; - self.@"initial-command" = .{ .direct = try builder.toOwnedSlice() }; + self.@"initial-command" = .{ .direct = try builder.toOwnedSlice(arena_alloc) }; return; } } @@ -3710,13 +3712,13 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { // PRIOR to the "-e" in our replay steps, since everything // after "-e" becomes an "initial-command". To do this, we // dupe the values if we find it. - var replay_suffix = std.ArrayList(Replay.Step).init(alloc_gpa); - defer replay_suffix.deinit(); + var replay_suffix: std.ArrayList(Replay.Step) = .empty; + defer replay_suffix.deinit(alloc_gpa); for (self._replay_steps.items, 0..) |step, i| if (step == .@"-e") { // We don't need to clone the steps because they should // all be allocated in our arena and we're keeping our // arena. - try replay_suffix.appendSlice(self._replay_steps.items[i..]); + try replay_suffix.appendSlice(alloc_gpa, self._replay_steps.items[i..]); // Remove our old values. Again, don't need to free any // memory here because its all part of our arena. @@ -3744,10 +3746,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { // We must only load a unique file once if (try loaded.fetchPut(path, {}) != null) { const diag: cli.Diagnostic = .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "config-file {s}: cycle detected", .{path}, + 0, ), }; @@ -3759,10 +3762,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { var file = std.fs.openFileAbsolute(path, .{}) catch |err| { if (err != error.FileNotFound or !optional) { const diag: cli.Diagnostic = .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "error opening config-file {s}: {}", .{ path, err }, + 0, ), }; @@ -3778,10 +3782,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { .file => {}, else => |kind| { const diag: cli.Diagnostic = .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "config-file {s}: not reading because file type is {s}", .{ path, @tagName(kind) }, + 0, ), }; @@ -3792,10 +3797,10 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { } log.info("loading config-file path={s}", .{path}); - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - const Iter = cli.args.LineIterator(@TypeOf(reader)); - var iter: Iter = .{ .r = reader, .filepath = path }; + var buf: [2048]u8 = undefined; + var file_reader = file.reader(&buf); + const reader = &file_reader.interface; + var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try self.loadIter(alloc_gpa, &iter); try self.expandPaths(std.fs.path.dirname(path).?); } @@ -3944,10 +3949,10 @@ fn loadTheme(self: *Config, theme: Theme) !void { errdefer new_config.deinit(); // Load our theme - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - const Iter = cli.args.LineIterator(@TypeOf(reader)); - var iter: Iter = .{ .r = reader, .filepath = path }; + var buf: [2048]u8 = undefined; + var file_reader = file.reader(&buf); + const reader = &file_reader.interface; + var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try new_config.loadIter(alloc_gpa, &iter); // Setup our replay to be conditional. @@ -4190,7 +4195,7 @@ pub fn finalize(self: *Config) !void { if (self.@"quit-after-last-window-closed-delay") |duration| { if (duration.duration < 5 * std.time.ns_per_s) { log.warn( - "quit-after-last-window-closed-delay is set to a very short value ({}), which might cause problems", + "quit-after-last-window-closed-delay is set to a very short value ({f}), which might cause problems", .{duration}, ); } @@ -4221,22 +4226,23 @@ pub fn parseManuallyHook( // Build up the command. We don't clean this up because we take // ownership in our allocator. - var command: std.ArrayList([:0]const u8) = .init(alloc); - errdefer command.deinit(); + var command: std.ArrayList([:0]const u8) = .empty; + errdefer command.deinit(alloc); while (iter.next()) |param| { const copy = try alloc.dupeZ(u8, param); try self._replay_steps.append(alloc, .{ .arg = copy }); - try command.append(copy); + try command.append(alloc, copy); } if (command.items.len == 0) { try self._diagnostics.append(alloc, .{ .location = try cli.Location.fromIter(iter, alloc), - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( alloc, "missing command after {s}", .{arg}, + 0, ), }); @@ -4371,10 +4377,11 @@ pub fn addDiagnosticFmt( ) Allocator.Error!void { const alloc = self._arena.?.allocator(); try self._diagnostics.append(alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( alloc, fmt, args, + 0, ), }); } @@ -4892,7 +4899,7 @@ pub const Color = struct { } /// Used by Formatter - pub fn formatEntry(self: Color, formatter: anytype) !void { + pub fn formatEntry(self: Color, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; try formatter.formatEntry( []const u8, @@ -4959,12 +4966,12 @@ pub const Color = struct { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var color: Color = .{ .r = 10, .g = 11, .b = 12 }; - try color.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.items); + try color.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.written()); } test "parseCLI with whitespace" { @@ -4995,7 +5002,7 @@ pub const TerminalColor = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: TerminalColor, formatter: anytype) !void { + pub fn formatEntry(self: TerminalColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .color => try self.color.formatEntry(formatter), @@ -5030,12 +5037,12 @@ pub const TerminalColor = union(enum) { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var sc: TerminalColor = .@"cell-foreground"; - try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items); + try sc.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.written()); } }; @@ -5051,7 +5058,7 @@ pub const BoldColor = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: BoldColor, formatter: anytype) !void { + pub fn formatEntry(self: BoldColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .color => try self.color.formatEntry(formatter), .bright => try formatter.formatEntry( @@ -5082,12 +5089,12 @@ pub const BoldColor = union(enum) { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var sc: BoldColor = .bright; - try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try testing.expectEqualSlices(u8, "a = bright\n", buf.items); + try sc.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try testing.expectEqualSlices(u8, "a = bright\n", buf.written()); } }; @@ -5174,8 +5181,7 @@ pub const ColorList = struct { // Build up the value of our config. Our buffer size should be // sized to contain all possible maximum values. var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - var writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); for (self.colors.items, 0..) |color, i| { var color_buf: [128]u8 = undefined; const color_str = try color.formatBuf(&color_buf); @@ -5185,7 +5191,7 @@ pub const ColorList = struct { try formatter.formatEntry( []const u8, - fbs.getWritten(), + writer.buffered(), ); } @@ -5214,7 +5220,7 @@ pub const ColorList = struct { test "format" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5223,8 +5229,8 @@ pub const ColorList = struct { var p: Self = .{}; try p.parseCLI(alloc, "black,white"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.written()); } }; @@ -5285,7 +5291,7 @@ pub const Palette = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; for (0.., self.value) |k, v| { try formatter.formatEntry( @@ -5340,12 +5346,12 @@ pub const Palette = struct { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: Self = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.items[0..14]); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.written()[0..14]); } test "parseCLI with whitespace" { @@ -5439,7 +5445,7 @@ pub const RepeatableString = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // If no items, we want to render an empty field. if (self.list.items.len == 0) { try formatter.formatEntry(void, {}); @@ -5486,17 +5492,17 @@ pub const RepeatableString = struct { test "formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: Self = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5505,13 +5511,13 @@ pub const RepeatableString = struct { var list: Self = .{}; try list.parseCLI(alloc, "A"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); } test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5521,8 +5527,8 @@ pub const RepeatableString = struct { var list: Self = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "B"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.written()); } }; @@ -5638,7 +5644,7 @@ pub const RepeatableFontVariation = struct { test "formatConfig single" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5647,8 +5653,8 @@ pub const RepeatableFontVariation = struct { var list: Self = .{}; try list.parseCLI(alloc, "wght = 200"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.written()); } }; @@ -6449,7 +6455,7 @@ pub const Keybinds = struct { } /// Like formatEntry but has an option to include docs. - pub fn formatEntryDocs(self: Keybinds, formatter: anytype, docs: bool) !void { + pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void { if (self.set.bindings.size == 0) { try formatter.formatEntry(void, {}); return; @@ -6478,14 +6484,14 @@ pub const Keybinds = struct { } } - var buffer_stream = std.io.fixedBufferStream(&buf); - std.fmt.format(buffer_stream.writer(), "{}", .{k}) catch return error.OutOfMemory; - try v.formatEntries(&buffer_stream, formatter); + var writer: std.Io.Writer = .fixed(&buf); + writer.print("{f}", .{k}) catch return error.OutOfMemory; + try v.formatEntries(&writer, formatter); } } /// Used by Formatter - pub fn formatEntry(self: Keybinds, formatter: anytype) !void { + pub fn formatEntry(self: Keybinds, formatter: formatterpkg.EntryFormatter) !void { try self.formatEntryDocs(formatter, false); } @@ -6502,7 +6508,7 @@ pub const Keybinds = struct { test "formatConfig single" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6511,14 +6517,14 @@ pub const Keybinds = struct { var list: Keybinds = .{}; try list.parseCLI(alloc, "shift+a=csi:hello"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.written()); } // Regression test for https://github.com/ghostty-org/ghostty/issues/2734 test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6528,7 +6534,7 @@ pub const Keybinds = struct { var list: Keybinds = .{}; try list.parseCLI(alloc, "ctrl+z>1=goto_tab:1"); try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2"); - try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer())); + try list.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer)); // Note they turn into translated keys because they match // their ASCII mapping. @@ -6537,12 +6543,12 @@ pub const Keybinds = struct { \\keybind = ctrl+z>1=goto_tab:1 \\ ; - try std.testing.expectEqualStrings(want, buf.items); + try std.testing.expectEqualStrings(want, buf.written()); } test "formatConfig multiple items nested" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6554,7 +6560,7 @@ pub const Keybinds = struct { try list.parseCLI(alloc, "ctrl+a>ctrl+b>w=close_window"); try list.parseCLI(alloc, "ctrl+a>ctrl+c>t=new_tab"); try list.parseCLI(alloc, "ctrl+b>ctrl+d>a=previous_tab"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); // NB: This does not currently retain the order of the keybinds. const want = @@ -6564,7 +6570,7 @@ pub const Keybinds = struct { \\a = ctrl+b>ctrl+d>a=previous_tab \\ ; - try std.testing.expectEqualStrings(want, buf.items); + try std.testing.expectEqualStrings(want, buf.written()); } }; @@ -6790,7 +6796,7 @@ pub const RepeatableCodepointMap = struct { test "formatConfig single" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6799,13 +6805,13 @@ pub const RepeatableCodepointMap = struct { var list: Self = .{}; try list.parseCLI(alloc, "U+ABCD=Comic Sans"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.written()); } test "formatConfig range" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6814,13 +6820,13 @@ pub const RepeatableCodepointMap = struct { var list: Self = .{}; try list.parseCLI(alloc, "U+0001 - U+0005=Verdana"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.written()); } test "formatConfig multiple" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6829,12 +6835,12 @@ pub const RepeatableCodepointMap = struct { var list: Self = .{}; try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, \\a = U+0006-U+0009=Courier \\a = U+ABCD=Courier \\ - , buf.items); + , buf.written()); } }; @@ -6886,7 +6892,7 @@ pub const FontStyle = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .default, .false => try formatter.formatEntry( []const u8, @@ -6918,7 +6924,7 @@ pub const FontStyle = union(enum) { test "formatConfig default" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6927,13 +6933,13 @@ pub const FontStyle = union(enum) { var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "default"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = default\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = default\n", buf.written()); } test "formatConfig false" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6942,13 +6948,13 @@ pub const FontStyle = union(enum) { var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "false"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = false\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = false\n", buf.written()); } test "formatConfig named" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6957,8 +6963,8 @@ pub const FontStyle = union(enum) { var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "bold"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = bold\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = bold\n", buf.written()); } }; @@ -7018,7 +7024,7 @@ pub const RepeatableLink = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // This currently can't be set so we don't format anything. _ = self; _ = formatter; @@ -7128,7 +7134,10 @@ pub const RepeatableCommand = struct { } /// Used by Formatter - pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void { + pub fn formatEntry( + self: RepeatableCommand, + formatter: formatterpkg.EntryFormatter, + ) !void { if (self.value.items.len == 0) { try formatter.formatEntry(void, {}); return; @@ -7136,22 +7145,23 @@ pub const RepeatableCommand = struct { for (self.value.items) |item| { var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - var writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); - writer.writeAll("title:\"") catch return error.OutOfMemory; - std.zig.stringEscape(item.title, "", .{}, writer) catch return error.OutOfMemory; - writer.writeAll("\"") catch return error.OutOfMemory; + writer.print( + "title:\"{f}\"", + .{std.zig.fmtString(item.title)}, + ) catch return error.OutOfMemory; if (item.description.len > 0) { - writer.writeAll(",description:\"") catch return error.OutOfMemory; - std.zig.stringEscape(item.description, "", .{}, writer) catch return error.OutOfMemory; - writer.writeAll("\"") catch return error.OutOfMemory; + writer.print( + ",description:\"{f}\"", + .{std.zig.fmtString(item.description)}, + ) catch return error.OutOfMemory; } - writer.print(",action:\"{}\"", .{item.action}) catch return error.OutOfMemory; + writer.print(",action:\"{f}\"", .{item.action}) catch return error.OutOfMemory; - try formatter.formatEntry([]const u8, fbs.getWritten()); + try formatter.formatEntry([]const u8, writer.buffered()); } } @@ -7197,17 +7207,17 @@ pub const RepeatableCommand = struct { test "RepeatableCommand formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatableCommand = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "RepeatableCommand formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -7216,13 +7226,13 @@ pub const RepeatableCommand = struct { var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:Bober"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:Bober\"\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:Bober\"\n", buf.written()); } test "RepeatableCommand formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -7232,14 +7242,12 @@ pub const RepeatableCommand = struct { var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:kurwa"); try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:kurwa\"\na = title:\"Ja\",description:\"pierdole\",action:\"text:jakie bydle\"\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:kurwa\"\na = title:\"Ja\",description:\"pierdole\",action:\"text:jakie bydle\"\n", buf.written()); } test "RepeatableCommand parseCLI commas" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -7455,14 +7463,14 @@ pub const MouseScrollMultiplier = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { - var buf: [32]u8 = undefined; - const formatted = std.fmt.bufPrint( - &buf, + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + var buf: [4096]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + writer.print( "precision:{d},discrete:{d}", .{ self.precision, self.discrete }, ) catch return error.OutOfMemory; - try formatter.formatEntry([]const u8, formatted); + try formatter.formatEntry([]const u8, writer.buffered()); } test "parse" { @@ -7505,12 +7513,12 @@ pub const MouseScrollMultiplier = struct { test "format entry MouseScrollMultiplier" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var args: Self = .{ .precision = 1.5, .discrete = 2.5 }; - try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", buf.writer())); - try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.items); + try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", &buf.writer)); + try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.written()); } }; @@ -7627,7 +7635,7 @@ pub const QuickTerminalSize = struct { return error.MissingUnit; } - fn format(self: Size, writer: anytype) !void { + fn format(self: Size, writer: *std.Io.Writer) !void { switch (self) { .percentage => |v| try writer.print("{d}%", .{v}), .pixels => |v| try writer.print("{}px", .{v}), @@ -7745,20 +7753,19 @@ pub const QuickTerminalSize = struct { }; } - pub fn formatEntry(self: QuickTerminalSize, formatter: anytype) !void { + pub fn formatEntry(self: QuickTerminalSize, formatter: formatterpkg.EntryFormatter) !void { const primary = self.primary orelse return; var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); - primary.format(writer) catch return error.OutOfMemory; + primary.format(&writer) catch return error.OutOfMemory; if (self.secondary) |secondary| { writer.writeByte(',') catch return error.OutOfMemory; - secondary.format(writer) catch return error.OutOfMemory; + secondary.format(&writer) catch return error.OutOfMemory; } - try formatter.formatEntry([]const u8, fbs.getWritten()); + try formatter.formatEntry([]const u8, writer.buffered()); } test "parse QuickTerminalSize" { @@ -8318,15 +8325,17 @@ pub const Duration = struct { return if (value) |v| .{ .duration = v } else error.ValueRequired; } - pub fn formatEntry(self: Duration, formatter: anytype) !void { + pub fn formatEntry(self: Duration, formatter: formatterpkg.EntryFormatter) !void { var buf: [64]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - try self.format("", .{}, writer); - try formatter.formatEntry([]const u8, fbs.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + try self.format(&writer); + try formatter.formatEntry([]const u8, writer.buffered()); } - pub fn format(self: Duration, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format( + self: Duration, + writer: *std.Io.Writer, + ) !void { var value = self.duration; var i: usize = 0; for (units) |unit| { @@ -8393,7 +8402,7 @@ pub const WindowPadding = struct { } } - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; if (self.top_left == self.bottom_right) { try formatter.formatEntry( @@ -8555,7 +8564,7 @@ test "test format" { inline for (Duration.units) |unit| { const d: Duration = .{ .duration = unit.factor }; var actual_buf: [16]u8 = undefined; - const actual = try std.fmt.bufPrint(&actual_buf, "{}", .{d}); + const actual = try std.fmt.bufPrint(&actual_buf, "{f}", .{d}); var expected_buf: [16]u8 = undefined; const expected = if (!std.mem.eql(u8, unit.name, "us")) try std.fmt.bufPrint(&expected_buf, "1{s}", .{unit.name}) @@ -8566,12 +8575,12 @@ test "test format" { } test "test entryFormatter" { - var buf = std.ArrayList(u8).init(std.testing.allocator); + var buf: std.Io.Writer.Allocating = .init(std.testing.allocator); defer buf.deinit(); var p: Duration = .{ .duration = std.math.maxInt(u64) }; - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.written()); } const TestIterator = struct { @@ -8681,15 +8690,20 @@ test "clone can then change conditional state" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme_light", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_light")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_light")); + try writer.end(); } { var file = try td.dir.createFile("theme_dark", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_dark")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_dark")); + try writer.end(); } var light_buf: [std.fs.max_path_bytes]u8 = undefined; const light = try td.dir.realpath("theme_light", &light_buf); @@ -8815,10 +8829,13 @@ test "theme loading" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_simple")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_simple")); + try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf); @@ -8851,10 +8868,13 @@ test "theme loading preserves conditional state" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_simple")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_simple")); + try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf); @@ -8881,10 +8901,13 @@ test "theme priority is lower than config" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_simple")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_simple")); + try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf); @@ -8915,15 +8938,20 @@ test "theme loading correct light/dark" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme_light", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_light")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_light")); + try writer.end(); } { var file = try td.dir.createFile("theme_dark", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_dark")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_dark")); + try writer.end(); } var light_buf: [std.fs.max_path_bytes]u8 = undefined; const light = try td.dir.realpath("theme_light", &light_buf); diff --git a/src/config/RepeatableStringMap.zig b/src/config/RepeatableStringMap.zig index 6f143e95d..d5e634333 100644 --- a/src/config/RepeatableStringMap.zig +++ b/src/config/RepeatableStringMap.zig @@ -104,7 +104,7 @@ pub fn equal(self: RepeatableStringMap, other: RepeatableStringMap) bool { } /// Used by formatter -pub fn formatEntry(self: RepeatableStringMap, formatter: anytype) !void { +pub fn formatEntry(self: RepeatableStringMap, formatter: formatterpkg.EntryFormatter) !void { // If no items, we want to render an empty field. if (self.map.count() == 0) { try formatter.formatEntry(void, {}); @@ -146,12 +146,12 @@ test "RepeatableStringMap: parseCLI" { test "RepeatableStringMap: formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatableStringMap = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "RepeatableStringMap: formatConfig single item" { @@ -162,20 +162,20 @@ test "RepeatableStringMap: formatConfig single item" { const alloc = arena.allocator(); { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var map: RepeatableStringMap = .{}; try map.parseCLI(alloc, "A=B"); - try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items); + try map.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.written()); } { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var map: RepeatableStringMap = .{}; try map.parseCLI(alloc, " A = B "); - try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items); + try map.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.written()); } } @@ -187,12 +187,12 @@ test "RepeatableStringMap: formatConfig multiple items" { const alloc = arena.allocator(); { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatableStringMap = .{}; try list.parseCLI(alloc, "A=B"); try list.parseCLI(alloc, "B = C"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.written()); } } diff --git a/src/config/command.zig b/src/config/command.zig index 9efeb199e..e0cdc641b 100644 --- a/src/config/command.zig +++ b/src/config/command.zig @@ -166,21 +166,20 @@ pub const Command = union(enum) { }; } - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .shell => |v| try formatter.formatEntry([]const u8, v), .direct => |v| { var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); writer.writeAll("direct:") catch return error.OutOfMemory; for (v) |arg| { writer.writeAll(arg) catch return error.OutOfMemory; writer.writeByte(' ') catch return error.OutOfMemory; } - const written = fbs.getWritten(); + const written = writer.buffered(); try formatter.formatEntry( []const u8, written[0..@intCast(written.len - 1)], @@ -292,13 +291,13 @@ pub const Command = union(enum) { defer arena.deinit(); const alloc = arena.allocator(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); var v: Self = undefined; try v.parseCLI(alloc, "echo hello"); - try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.items); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.written()); } test "Command: formatConfig direct" { @@ -307,13 +306,13 @@ pub const Command = union(enum) { defer arena.deinit(); const alloc = arena.allocator(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); var v: Self = undefined; try v.parseCLI(alloc, "direct: echo hello"); - try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.items); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.written()); } }; diff --git a/src/config/edit.zig b/src/config/edit.zig index 38dc98169..07bb7ee5a 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -89,8 +89,8 @@ fn configPath(alloc_arena: Allocator) ![]const u8 { /// Returns a const list of possible paths the main config file could be /// in for the current OS. fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 { - var paths = try std.ArrayList([]const u8).initCapacity(alloc_arena, 2); - errdefer paths.deinit(); + var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 2); + errdefer paths.deinit(alloc_arena); if (comptime builtin.os.tag == .macos) { paths.appendAssumeCapacity(try internal_os.macos.appSupportDir( diff --git a/src/config/formatter.zig b/src/config/formatter.zig index a42395c19..dcf99167d 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -8,38 +8,36 @@ const Key = @import("key.zig").Key; /// Returns a single entry formatter for the given field name and writer. pub fn entryFormatter( name: []const u8, - writer: anytype, -) EntryFormatter(@TypeOf(writer)) { + writer: *std.Io.Writer, +) EntryFormatter { return .{ .name = name, .writer = writer }; } /// The entry formatter type for a given writer. -pub fn EntryFormatter(comptime WriterType: type) type { - return struct { - name: []const u8, - writer: WriterType, +pub const EntryFormatter = struct { + name: []const u8, + writer: *std.Io.Writer, - pub fn formatEntry( - self: @This(), - comptime T: type, - value: T, - ) !void { - return formatter.formatEntry( - T, - self.name, - value, - self.writer, - ); - } - }; -} + pub fn formatEntry( + self: @This(), + comptime T: type, + value: T, + ) !void { + return formatter.formatEntry( + T, + self.name, + value, + self.writer, + ); + } +}; /// Format a single type with the given name and value. pub fn formatEntry( comptime T: type, name: []const u8, value: T, - writer: anytype, + writer: *std.Io.Writer, ) !void { switch (@typeInfo(T)) { .bool, .int => { @@ -53,7 +51,7 @@ pub fn formatEntry( }, .@"enum" => { - try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); + try writer.print("{s} = {t}\n", .{ name, value }); return; }, @@ -143,19 +141,14 @@ pub const FileFormatter = struct { /// Implements std.fmt so it can be used directly with std.fmt. pub fn format( self: FileFormatter, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, - ) !void { + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { @setEvalBranchQuota(10_000); - _ = layout; - _ = opts; - // If we're change-tracking then we need the default config to // compare against. var default: ?Config = if (self.changed) - try .default(self.alloc) + Config.default(self.alloc) catch return error.WriteFailed else null; defer if (default) |*v| v.deinit(); @@ -179,12 +172,12 @@ pub const FileFormatter = struct { } } - try formatEntry( + formatEntry( field.type, field.name, value, writer, - ); + ) catch return error.WriteFailed; if (do_docs) try writer.print("\n", .{}); } @@ -198,7 +191,7 @@ test "format default config" { var cfg = try Config.default(alloc); defer cfg.deinit(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); // We just make sure this works without errors. We aren't asserting output. @@ -206,9 +199,9 @@ test "format default config" { .alloc = alloc, .config = &cfg, }; - try std.fmt.format(buf.writer(), "{}", .{fmt}); + try fmt.format(&buf.writer); - //std.log.warn("{s}", .{buf.items}); + //std.log.warn("{s}", .{buf.written()}); } test "format default config changed" { @@ -218,7 +211,7 @@ test "format default config changed" { defer cfg.deinit(); cfg.@"font-size" = 42; - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); // We just make sure this works without errors. We aren't asserting output. @@ -227,26 +220,26 @@ test "format default config changed" { .config = &cfg, .changed = true, }; - try std.fmt.format(buf.writer(), "{}", .{fmt}); + try fmt.format(&buf.writer); - //std.log.warn("{s}", .{buf.items}); + //std.log.warn("{s}", .{buf.written()}); } test "formatEntry bool" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(bool, "a", true, buf.writer()); - try testing.expectEqualStrings("a = true\n", buf.items); + try formatEntry(bool, "a", true, &buf.writer); + try testing.expectEqualStrings("a = true\n", buf.written()); } { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(bool, "a", false, buf.writer()); - try testing.expectEqualStrings("a = false\n", buf.items); + try formatEntry(bool, "a", false, &buf.writer); + try testing.expectEqualStrings("a = false\n", buf.written()); } } @@ -254,10 +247,10 @@ test "formatEntry int" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(u8, "a", 123, buf.writer()); - try testing.expectEqualStrings("a = 123\n", buf.items); + try formatEntry(u8, "a", 123, &buf.writer); + try testing.expectEqualStrings("a = 123\n", buf.written()); } } @@ -265,10 +258,10 @@ test "formatEntry float" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(f64, "a", 0.7, buf.writer()); - try testing.expectEqualStrings("a = 0.7\n", buf.items); + try formatEntry(f64, "a", 0.7, &buf.writer); + try testing.expectEqualStrings("a = 0.7\n", buf.written()); } } @@ -277,10 +270,10 @@ test "formatEntry enum" { const Enum = enum { one, two, three }; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(Enum, "a", .two, buf.writer()); - try testing.expectEqualStrings("a = two\n", buf.items); + try formatEntry(Enum, "a", .two, &buf.writer); + try testing.expectEqualStrings("a = two\n", buf.written()); } } @@ -288,10 +281,10 @@ test "formatEntry void" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(void, "a", {}, buf.writer()); - try testing.expectEqualStrings("a = \n", buf.items); + try formatEntry(void, "a", {}, &buf.writer); + try testing.expectEqualStrings("a = \n", buf.written()); } } @@ -299,17 +292,17 @@ test "formatEntry optional" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(?bool, "a", null, buf.writer()); - try testing.expectEqualStrings("a = \n", buf.items); + try formatEntry(?bool, "a", null, &buf.writer); + try testing.expectEqualStrings("a = \n", buf.written()); } { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(?bool, "a", false, buf.writer()); - try testing.expectEqualStrings("a = false\n", buf.items); + try formatEntry(?bool, "a", false, &buf.writer); + try testing.expectEqualStrings("a = false\n", buf.written()); } } @@ -317,10 +310,10 @@ test "formatEntry string" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry([]const u8, "a", "hello", buf.writer()); - try testing.expectEqualStrings("a = hello\n", buf.items); + try formatEntry([]const u8, "a", "hello", &buf.writer); + try testing.expectEqualStrings("a = hello\n", buf.written()); } } @@ -332,9 +325,9 @@ test "formatEntry packed struct" { }; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(Value, "a", .{}, buf.writer()); - try testing.expectEqualStrings("a = one,no-two\n", buf.items); + try formatEntry(Value, "a", .{}, &buf.writer); + try testing.expectEqualStrings("a = one,no-two\n", buf.written()); } } diff --git a/src/config/io.zig b/src/config/io.zig index 8be4be551..9d9a127e8 100644 --- a/src/config/io.zig +++ b/src/config/io.zig @@ -94,10 +94,9 @@ pub const ReadableIO = union(enum) { }; } - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); switch (self) { inline else => |v, tag| { writer.writeAll(@tagName(tag)) catch return error.OutOfMemory; @@ -106,10 +105,9 @@ pub const ReadableIO = union(enum) { }, } - const written = fbs.getWritten(); try formatter.formatEntry( []const u8, - written, + writer.buffered(), ); } @@ -144,13 +142,13 @@ pub const ReadableIO = union(enum) { defer arena.deinit(); const alloc = arena.allocator(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); var v: Self = undefined; try v.parseCLI(alloc, "raw:foo"); - try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.items); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.written()); } }; @@ -222,7 +220,7 @@ pub const RepeatableReadableIO = struct { /// Used by Formatter pub fn formatEntry( self: Self, - formatter: anytype, + formatter: formatterpkg.EntryFormatter, ) !void { if (self.list.items.len == 0) { try formatter.formatEntry(void, {}); diff --git a/src/config/path.zig b/src/config/path.zig index 651dbdb3a..aeba69b94 100644 --- a/src/config/path.zig +++ b/src/config/path.zig @@ -79,7 +79,7 @@ pub const Path = union(enum) { } /// Used by formatter. - pub fn formatEntry(self: *const Path, formatter: anytype) !void { + pub fn formatEntry(self: *const Path, formatter: formatterpkg.EntryFormatter) !void { var buf: [std.fs.max_path_bytes + 1]u8 = undefined; const value = switch (self.*) { .optional => |path| std.fmt.bufPrint( @@ -154,10 +154,11 @@ pub const Path = union(enum) { &buf, ) catch |err| { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "error expanding home directory for path {s}: {}", .{ path, err }, + 0, ), }); @@ -194,10 +195,11 @@ pub const Path = union(enum) { } try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "error resolving file path {s}: {}", .{ path, err }, + 0, ), }); @@ -306,7 +308,7 @@ pub const Path = union(enum) { test "formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -315,13 +317,13 @@ pub const Path = union(enum) { var item: Path = undefined; try item.parseCLI(alloc, "A"); - try item.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + try item.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); } test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -331,8 +333,8 @@ pub const Path = union(enum) { var item: Path = undefined; try item.parseCLI(alloc, "A"); try item.parseCLI(alloc, "?B"); - try item.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = ?B\n", buf.items); + try item.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = ?B\n", buf.written()); } }; @@ -382,7 +384,7 @@ pub const RepeatablePath = struct { } /// Used by Formatter - pub fn formatEntry(self: RepeatablePath, formatter: anytype) !void { + pub fn formatEntry(self: RepeatablePath, formatter: formatterpkg.EntryFormatter) !void { if (self.value.items.len == 0) { try formatter.formatEntry(void, {}); return; @@ -453,17 +455,17 @@ pub const RepeatablePath = struct { test "formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatablePath = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -472,13 +474,13 @@ pub const RepeatablePath = struct { var list: RepeatablePath = .{}; try list.parseCLI(alloc, "A"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); } test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -488,7 +490,7 @@ pub const RepeatablePath = struct { var list: RepeatablePath = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "?B"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.written()); } }; diff --git a/src/config/theme.zig b/src/config/theme.zig index 8fa7c93dc..b1188a5c4 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -125,10 +125,11 @@ pub fn open( ) orelse return null; const stat = file.stat() catch |err| { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": {}", .{ theme, err }, + 0, ), }); return null; @@ -137,10 +138,11 @@ pub fn open( .file => {}, else => { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": it is a {s}", .{ theme, @tagName(stat.kind) }, + 0, ), }); return null; @@ -152,10 +154,11 @@ pub fn open( const basename = std.fs.path.basename(theme); if (!std.mem.eql(u8, theme, basename)) { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "theme \"{s}\" cannot include path separators unless it is an absolute path", .{theme}, + 0, ), }); return null; @@ -170,10 +173,11 @@ pub fn open( if (cwd.openFile(path, .{})) |file| { const stat = file.stat() catch |err| { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": {}", .{ theme, err }, + 0, ), }); return null; @@ -182,10 +186,11 @@ pub fn open( .file => {}, else => { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": it is a {s}", .{ theme, @tagName(stat.kind) }, + 0, ), }); return null; @@ -202,10 +207,11 @@ pub fn open( // Anything else is an error we log and give up on. else => { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "failed to load theme \"{s}\" from the file \"{s}\": {}", .{ theme, path, err }, + 0, ), }); @@ -222,10 +228,11 @@ pub fn open( while (try it.next()) |loc| { const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme }); try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "theme \"{s}\" not found, tried path \"{s}\"", .{ theme, path }, + 0, ), }); } @@ -249,17 +256,19 @@ pub fn openAbsolute( return std.fs.openFileAbsolute(theme, .{}) catch |err| { switch (err) { error.FileNotFound => try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "failed to load theme from the path \"{s}\"", .{theme}, + 0, ), }), else => try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "failed to load theme from the path \"{s}\": {}", .{ theme, err }, + 0, ), }), } diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index 6b675554c..08573b739 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -26,7 +26,7 @@ pub const Envelope = struct { headers: std.json.ObjectMap, /// The items in the envelope in the order they're encoded. - items: std.ArrayListUnmanaged(Item), + items: std.ArrayList(Item), /// Parse an envelope from a reader. /// @@ -37,7 +37,7 @@ pub const Envelope = struct { /// parsing in our use case is not a hot path. pub fn parse( alloc_gpa: Allocator, - reader: anytype, + reader: *std.Io.Reader, ) !Envelope { // We use an arena allocator to read from reader. We pair this // with `alloc_if_needed` when parsing json to allow the json @@ -62,23 +62,24 @@ pub const Envelope = struct { fn parseHeader( alloc: Allocator, - reader: anytype, + reader: *std.Io.Reader, ) !std.json.ObjectMap { - var buf: std.ArrayListUnmanaged(u8) = .{}; - reader.streamUntilDelimiter( - buf.writer(alloc), + var buf: std.Io.Writer.Allocating = .init(alloc); + _ = try reader.streamDelimiterLimit( + &buf.writer, '\n', - 1024 * 1024, // 1MB, arbitrary choice - ) catch |err| switch (err) { - // Envelope can be header-only. + .limited(1024 * 1024), // 1MB, arbitrary choice + ); + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline error.EndOfStream => {}, - else => |v| return v, + else => return err, }; const value = try std.json.parseFromSliceLeaky( std.json.Value, alloc, - buf.items, + buf.written(), .{ .allocate = .alloc_if_needed }, ); @@ -90,9 +91,9 @@ pub const Envelope = struct { fn parseItems( alloc: Allocator, - reader: anytype, - ) !std.ArrayListUnmanaged(Item) { - var items: std.ArrayListUnmanaged(Item) = .{}; + reader: *std.Io.Reader, + ) !std.ArrayList(Item) { + var items: std.ArrayList(Item) = .{}; errdefer items.deinit(alloc); while (try parseOneItem(alloc, reader)) |item| { try items.append(alloc, item); @@ -103,22 +104,27 @@ pub const Envelope = struct { fn parseOneItem( alloc: Allocator, - reader: anytype, + reader: *std.Io.Reader, ) !?Item { // Get the next item which must start with a header. - var buf: std.ArrayListUnmanaged(u8) = .{}; - reader.streamUntilDelimiter( - buf.writer(alloc), + var buf: std.Io.Writer.Allocating = .init(alloc); + _ = reader.streamDelimiterLimit( + &buf.writer, '\n', - 1024 * 1024, // 1MB, arbitrary choice + .limited(1024 * 1024), // 1MB, arbitrary choice ) catch |err| switch (err) { - error.EndOfStream => return null, - else => |v| return v, + error.StreamTooLong => return null, + else => return err, + }; + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, }; // Parse the header JSON const headers: std.json.ObjectMap = headers: { - const line = std.mem.trim(u8, buf.items, " \t"); + const line = std.mem.trim(u8, buf.written(), " \t"); if (line.len == 0) return null; const value = try std.json.parseFromSliceLeaky( @@ -156,18 +162,16 @@ pub const Envelope = struct { // Get the payload const payload: []const u8 = if (len_) |len| payload: { // The payload length is specified so read the exact length. - var payload = std.ArrayList(u8).init(alloc); + var payload: std.Io.Writer.Allocating = .init(alloc); defer payload.deinit(); - for (0..len) |_| { - const byte = reader.readByte() catch |err| switch (err) { - error.EndOfStream => return error.EnvelopeItemPayloadTooShort, - else => return err, - }; - try payload.append(byte); - } + + reader.streamExact(&payload.writer, len) catch |err| switch (err) { + error.EndOfStream => return error.EnvelopeItemPayloadTooShort, + else => return err, + }; // The next byte must be a newline. - if (reader.readByte()) |byte| { + if (reader.takeByte()) |byte| { if (byte != '\n') return error.EnvelopeItemPayloadNoNewline; } else |err| switch (err) { error.EndOfStream => {}, @@ -177,16 +181,20 @@ pub const Envelope = struct { break :payload try payload.toOwnedSlice(); } else payload: { // The payload is the next line ending in `\n`. It is required. - var payload = std.ArrayList(u8).init(alloc); - defer payload.deinit(); - reader.streamUntilDelimiter( - payload.writer(), + var payload: std.Io.Writer.Allocating = .init(alloc); + _ = reader.streamDelimiterLimit( + &payload.writer, '\n', - 1024 * 1024 * 50, // 50MB, arbitrary choice + .limited(1024 * 1024), // 50MB, arbitrary choice ) catch |err| switch (err) { - error.EndOfStream => return error.EnvelopeItemPayloadTooShort, + error.StreamTooLong => return error.EnvelopeItemPayloadTooShort, else => |v| return v, }; + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, + }; break :payload try payload.toOwnedSlice(); }; @@ -212,15 +220,13 @@ pub const Envelope = struct { /// therefore may allocate. pub fn serialize( self: *Envelope, - writer: anytype, + writer: *std.Io.Writer, ) !void { // Header line first - try std.json.stringify( + try writer.print("{f}\n", .{std.json.fmt( std.json.Value{ .object = self.headers }, json_opts, - writer, - ); - try writer.writeByte('\n'); + )}); // Write each item const alloc = self.allocator(); @@ -230,13 +236,13 @@ pub const Envelope = struct { const encoded = try item.encode(alloc); assert(item.* == .encoded); - try std.json.stringify( - std.json.Value{ .object = encoded.headers }, - json_opts, - writer, - ); - try writer.writeByte('\n'); - try writer.writeAll(encoded.payload); + try writer.print("{f}\n{s}", .{ + std.json.fmt( + std.json.Value{ .object = encoded.headers }, + json_opts, + ), + encoded.payload, + }); } } }; @@ -425,7 +431,7 @@ pub const Attachment = struct { pub const ObjectMapUnmanaged = std.StringArrayHashMapUnmanaged(std.json.Value); /// The options we must use for serialization. -const json_opts: std.json.StringifyOptions = .{ +const json_opts: std.json.Stringify.Options = .{ // This is the default but I want to be explicit because its // VERY important for the correctness of the envelope. This is // the only whitespace type in std.json that doesn't emit newlines. @@ -437,10 +443,10 @@ test "Envelope parse" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); } @@ -448,12 +454,12 @@ test "Envelope parse session" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.items.len); @@ -464,14 +470,14 @@ test "Envelope parse multiple" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 2), v.items.items.len); @@ -483,14 +489,14 @@ test "Envelope parse multiple no length" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session"} \\{} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 2), v.items.items.len); @@ -502,13 +508,13 @@ test "Envelope parse end in new line" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} \\ ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.items.len); @@ -519,12 +525,12 @@ test "Envelope parse attachment" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.items.len); @@ -537,14 +543,14 @@ test "Envelope parse attachment" { // Serialization test { - var output = std.ArrayList(u8).init(alloc); + var output: std.Io.Writer.Allocating = .init(alloc); defer output.deinit(); - try v.serialize(output.writer()); + try v.serialize(&output.writer); try testing.expectEqualStrings( \\{} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD - , std.mem.trim(u8, output.items, "\n")); + , std.mem.trim(u8, output.written(), "\n")); } } @@ -552,76 +558,40 @@ test "Envelope serialize empty" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); - var output = std.ArrayList(u8).init(alloc); + var output: std.Io.Writer.Allocating = .init(alloc); defer output.deinit(); - try v.serialize(output.writer()); + try v.serialize(&output.writer); try testing.expectEqualStrings( \\{} - , std.mem.trim(u8, output.items, "\n")); + , std.mem.trim(u8, output.written(), "\n")); } test "Envelope serialize session" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); - var output = std.ArrayList(u8).init(alloc); + var output: std.Io.Writer.Allocating = .init(alloc); defer output.deinit(); - try v.serialize(output.writer()); + try v.serialize(&output.writer); try testing.expectEqualStrings( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} - , std.mem.trim(u8, output.items, "\n")); + , std.mem.trim(u8, output.written(), "\n")); } - -// // Uncomment this test if you want to extract a minidump file from an -// // existing envelope. This is useful for getting new test contents. -// test "Envelope extract mdmp" { -// const testing = std.testing; -// const alloc = testing.allocator; -// -// var fbs = std.io.fixedBufferStream(@embedFile("in.crash")); -// var v = try Envelope.parse(alloc, fbs.reader()); -// defer v.deinit(); -// -// try testing.expect(v.items.items.len > 0); -// for (v.items.items, 0..) |*item, i| { -// if (item.encoded.type != .attachment) { -// log.warn("ignoring item type={} i={}", .{ item.encoded.type, i }); -// continue; -// } -// -// try item.decode(v.allocator()); -// const attach = item.attachment; -// const attach_type = attach.type orelse { -// log.warn("attachment missing type i={}", .{i}); -// continue; -// }; -// if (!std.mem.eql(u8, attach_type, "event.minidump")) { -// log.warn("ignoring attachment type={s} i={}", .{ attach_type, i }); -// continue; -// } -// -// log.warn("found minidump i={}", .{i}); -// var f = try std.fs.cwd().createFile("out.mdmp", .{}); -// defer f.close(); -// try f.writer().writeAll(attach.payload); -// return; -// } -// } diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 5aa68555f..14ee0e504 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -14,7 +14,7 @@ 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; -//pub const SplitTree = split_tree.SplitTree; +pub const SplitTree = split_tree.SplitTree; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/datastruct/split_tree.zig b/src/datastruct/split_tree.zig index 28b45ceed..eb371187c 100644 --- a/src/datastruct/split_tree.zig +++ b/src/datastruct/split_tree.zig @@ -1023,45 +1023,33 @@ pub fn SplitTree(comptime V: type) type { } /// Format the tree in a human-readable format. By default this will - /// output a diagram followed by a textual representation. This can - /// be controlled via the formatting string: - /// - /// - `diagram` - Output a diagram of the split tree only. - /// - `text` - Output a textual representation of the split tree only. - /// - Empty - Output both a diagram and a textual representation. - /// + /// output a diagram followed by a textual representation. pub fn format( self: *const Self, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = options; - if (self.nodes.len == 0) { try writer.writeAll("empty"); return; } - - if (std.mem.eql(u8, fmt, "diagram")) { - self.formatDiagram(writer) catch - try writer.writeAll("failed to draw split tree diagram"); - } else if (std.mem.eql(u8, fmt, "text")) { - try self.formatText(writer, .root, 0); - } else if (fmt.len == 0) { - self.formatDiagram(writer) catch {}; - try self.formatText(writer, .root, 0); - } else { - return error.InvalidFormat; - } + self.formatDiagram(writer) catch {}; + try self.formatText(writer); } - fn formatText( - self: *const Self, - writer: anytype, + pub fn formatText(self: Self, writer: *std.Io.Writer) std.Io.Writer.Error!void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + try self.formatTextInner(writer, .root, 0); + } + + fn formatTextInner( + self: Self, + writer: *std.Io.Writer, current: Node.Handle, depth: usize, - ) !void { + ) std.Io.Writer.Error!void { for (0..depth) |_| try writer.writeAll(" "); if (self.zoomed) |zoomed| if (zoomed == current) { @@ -1075,20 +1063,25 @@ pub fn SplitTree(comptime V: type) type { try writer.print("leaf: {d}\n", .{current}), .split => |s| { - try writer.print("split (layout: {s}, ratio: {d:.2})\n", .{ - @tagName(s.layout), + try writer.print("split (layout: {t}, ratio: {d:.2})\n", .{ + s.layout, s.ratio, }); - try self.formatText(writer, s.left, depth + 1); - try self.formatText(writer, s.right, depth + 1); + try self.formatTextInner(writer, s.left, depth + 1); + try self.formatTextInner(writer, s.right, depth + 1); }, } } - fn formatDiagram( - self: *const Self, - writer: anytype, - ) !void { + pub fn formatDiagram( + self: Self, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + // Use our arena's GPA to allocate some intermediate memory. // Requiring allocation for formatting is nasty but this is really // only used for debugging and testing and shouldn't hit OOM @@ -1099,7 +1092,7 @@ pub fn SplitTree(comptime V: type) type { // Get our spatial representation. const sp = spatial: { - const sp = try self.spatial(alloc); + const sp = self.spatial(alloc) catch return error.WriteFailed; // Scale our spatial representation to have minimum width/height 1. var min_w: f16 = 1; @@ -1111,7 +1104,7 @@ pub fn SplitTree(comptime V: type) type { const ratio_w: f16 = 1 / min_w; const ratio_h: f16 = 1 / min_h; - const slots = try alloc.dupe(Spatial.Slot, sp.slots); + const slots = alloc.dupe(Spatial.Slot, sp.slots) catch return error.WriteFailed; for (slots) |*slot| { slot.x *= ratio_w; slot.y *= ratio_h; @@ -1168,9 +1161,9 @@ pub fn SplitTree(comptime V: type) type { width *= cell_width; height *= cell_height; - const rows = try alloc.alloc([]u8, height); + const rows = alloc.alloc([]u8, height) catch return error.WriteFailed; for (0..rows.len) |y| { - rows[y] = try alloc.alloc(u8, width + 1); + rows[y] = alloc.alloc(u8, width + 1) catch return error.WriteFailed; @memset(rows[y], ' '); rows[y][width] = '\n'; } @@ -1223,7 +1216,7 @@ pub fn SplitTree(comptime V: type) type { const label: []const u8 = if (@hasDecl(View, "splitTreeLabel")) node.leaf.splitTreeLabel() else - try std.fmt.bufPrint(&buf, "{d}", .{handle}); + std.fmt.bufPrint(&buf, "{d}", .{handle}) catch return error.WriteFailed; // Draw the handle in the center const x_mid = width / 2 + x; @@ -1231,7 +1224,7 @@ pub fn SplitTree(comptime V: type) type { const label_width = label.len; const label_start = x_mid - label_width / 2; const row = grid[y_mid][label_start..]; - _ = try std.fmt.bufPrint(row, "{s}", .{label}); + _ = std.fmt.bufPrint(row, "{s}", .{label}) catch return error.WriteFailed; } // Output every row @@ -1339,7 +1332,7 @@ test "SplitTree: empty tree" { var t: TestTree = .empty; defer t.deinit(); - const str = try std.fmt.allocPrint(alloc, "{}", .{t}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t}); defer alloc.free(str); try testing.expectEqualStrings(str, \\empty @@ -1353,7 +1346,7 @@ test "SplitTree: single node" { var t: TestTree = try .init(alloc, &v); defer t.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1383,7 +1376,7 @@ test "SplitTree: split horizontal" { defer t3.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t3}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t3}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1415,7 +1408,7 @@ test "SplitTree: split horizontal" { defer t4.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t4}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t4}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+--------++---++---+ @@ -1449,7 +1442,7 @@ test "SplitTree: split horizontal" { defer t5.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t5}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t5}); defer alloc.free(str); try testing.expectEqualStrings( \\+------------------++--------++---++---+ @@ -1547,7 +1540,7 @@ test "SplitTree: split vertical" { ); defer t3.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t3}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t3, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1583,7 +1576,7 @@ test "SplitTree: split horizontal with zero ratio" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1617,7 +1610,7 @@ test "SplitTree: split vertical with zero ratio" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1651,7 +1644,7 @@ test "SplitTree: split horizontal with full width" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1685,7 +1678,7 @@ test "SplitTree: split vertical with full width" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1727,7 +1720,7 @@ test "SplitTree: remove leaf" { ); defer t4.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t4}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t4, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1772,7 +1765,7 @@ test "SplitTree: split twice, remove intermediary" { defer split2.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split2}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split2, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1798,7 +1791,7 @@ test "SplitTree: split twice, remove intermediary" { defer split3.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split3}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split3, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1883,7 +1876,7 @@ test "SplitTree: spatial goto" { const split = splitBD; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1943,7 +1936,7 @@ test "SplitTree: spatial goto" { defer equal.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{equal}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(equal, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1979,7 +1972,7 @@ test "SplitTree: resize" { defer split.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -2005,7 +1998,7 @@ test "SplitTree: resize" { 0.25, ); defer resized.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{resized}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+-------------++---+ @@ -2026,7 +2019,7 @@ test "SplitTree: clone empty tree" { defer t2.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t2}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t2}); defer alloc.free(str); try testing.expectEqualStrings(str, \\empty @@ -2064,7 +2057,7 @@ test "SplitTree: zoom" { }); { - const str = try std.fmt.allocPrint(alloc, "{text}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\split (layout: horizontal, ratio: 0.50) @@ -2079,7 +2072,7 @@ test "SplitTree: zoom" { defer clone.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{text}", .{clone}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(clone, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\split (layout: horizontal, ratio: 0.50) @@ -2122,7 +2115,7 @@ test "SplitTree: split resets zoom" { defer split.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{text}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\split (layout: horizontal, ratio: 0.50) @@ -2178,7 +2171,7 @@ test "SplitTree: remove and zoom" { defer removed.deinit(); try testing.expect(removed.zoomed == null); - const str = try std.fmt.allocPrint(alloc, "{text}", .{removed}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\leaf: B @@ -2201,7 +2194,7 @@ test "SplitTree: remove and zoom" { ); defer removed.deinit(); - const str = try std.fmt.allocPrint(alloc, "{text}", .{removed}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\(zoomed) leaf: A diff --git a/src/extra/vim.zig b/src/extra/vim.zig index 4443fd168..ff85e820b 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -59,14 +59,15 @@ pub const compiler = /// Generates the syntax file at comptime. fn comptimeGenSyntax() []const u8 { comptime { - var counting_writer = std.io.countingWriter(std.io.null_writer); - try writeSyntax(&counting_writer.writer()); + @setEvalBranchQuota(50000); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeSyntax(&counter.writer); - var buf: [counting_writer.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeSyntax(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeSyntax(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index a0bc047c4..668b6f15f 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -471,23 +471,23 @@ pub const Modifier = union(enum) { test "formatConfig percent" { const configpkg = @import("../config.zig"); const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); const p = try parseCLI("24%"); - try p.formatEntry(configpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = 24%\n", buf.items); + try p.formatEntry(configpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = 24%\n", buf.written()); } test "formatConfig absolute" { const configpkg = @import("../config.zig"); const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); const p = try parseCLI("-30"); - try p.formatEntry(configpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = -30\n", buf.items); + try p.formatEntry(configpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = -30\n", buf.written()); } }; diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 813a8d6d0..4512e23cc 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -596,10 +596,10 @@ pub const Key = struct { // from DerivedConfig below. var config = try DerivedConfig.init(alloc, config_src); - var descriptors = std.ArrayList(discovery.Descriptor).init(alloc); - defer descriptors.deinit(); + var descriptors: std.ArrayList(discovery.Descriptor) = .empty; + defer descriptors.deinit(alloc); for (config.@"font-family".list.items) |family| { - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = config.@"font-style".nameValue(), .size = font_size.points, @@ -617,7 +617,7 @@ pub const Key = struct { // italic. for (config.@"font-family-bold".list.items) |family| { const style = config.@"font-style-bold".nameValue(); - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = style, .size = font_size.points, @@ -627,7 +627,7 @@ pub const Key = struct { } for (config.@"font-family-italic".list.items) |family| { const style = config.@"font-style-italic".nameValue(); - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = style, .size = font_size.points, @@ -637,7 +637,7 @@ pub const Key = struct { } for (config.@"font-family-bold-italic".list.items) |family| { const style = config.@"font-style-bold-italic".nameValue(); - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = style, .size = font_size.points, @@ -681,7 +681,7 @@ pub const Key = struct { return .{ .arena = arena, - .descriptors = try descriptors.toOwnedSlice(), + .descriptors = try descriptors.toOwnedSlice(alloc), .style_offsets = .{ regular_offset, bold_offset, diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 7bd019fd7..da3c51cee 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -356,8 +356,8 @@ pub const RunIterator = struct { // If this is a grapheme, we need to find a font that supports // all of the codepoints in the grapheme. const cps = self.opts.row.grapheme(cell) orelse return primary; - var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1); - defer candidates.deinit(); + var candidates: std.ArrayList(font.Collection.Index) = try .initCapacity(alloc, cps.len + 1); + defer candidates.deinit(alloc); candidates.appendAssumeCapacity(primary); for (cps) |cp| { diff --git a/src/global.zig b/src/global.zig index e68ec7f74..8034fabe0 100644 --- a/src/global.zig +++ b/src/global.zig @@ -140,7 +140,7 @@ pub const GlobalState = struct { std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); } std.log.info("renderer={}", .{renderer.Renderer}); - std.log.info("libxev default backend={s}", .{@tagName(xev.backend)}); + std.log.info("libxev default backend={t}", .{xev.backend}); // As early as possible, initialize our resource limits. self.rlimits = .init(); @@ -206,7 +206,7 @@ pub const GlobalState = struct { var sa: p.Sigaction = .{ .handler = .{ .handler = p.SIG.IGN }, - .mask = p.empty_sigset, + .mask = p.sigemptyset(), .flags = 0, }; diff --git a/src/helpgen.zig b/src/helpgen.zig index 57296fe86..fe30db10c 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -11,19 +11,22 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = gpa.allocator(); - const stdout = std.io.getStdOut().writer(); - try stdout.writeAll( + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + const writer = &stdout.interface; + try writer.writeAll( \\// THIS FILE IS AUTO GENERATED \\ \\ ); - try genConfig(alloc, stdout); - try genActions(alloc, stdout); - try genKeybindActions(alloc, stdout); + try genConfig(alloc, writer); + try genActions(alloc, writer); + try genKeybindActions(alloc, writer); + try stdout.end(); } -fn genConfig(alloc: std.mem.Allocator, writer: anytype) !void { +fn genConfig(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { var ast = try std.zig.Ast.parse(alloc, @embedFile("config/Config.zig"), .zig); defer ast.deinit(alloc); @@ -44,7 +47,7 @@ fn genConfig(alloc: std.mem.Allocator, writer: anytype) !void { fn genConfigField( alloc: std.mem.Allocator, - writer: anytype, + writer: *std.Io.Writer, ast: std.zig.Ast, comptime field: []const u8, ) !void { @@ -69,7 +72,7 @@ fn genConfigField( } } -fn genActions(alloc: std.mem.Allocator, writer: anytype) !void { +fn genActions(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { try writer.writeAll( \\ \\/// Actions help @@ -115,7 +118,7 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void { try writer.writeAll("};\n"); } -fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void { +fn genKeybindActions(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { var ast = try std.zig.Ast.parse(alloc, @embedFile("input/Binding.zig"), .zig); defer ast.deinit(alloc); @@ -149,24 +152,24 @@ fn extractDocComments( } else unreachable; // Go through and build up the lines. - var lines = std.ArrayList([]const u8).init(alloc); - defer lines.deinit(); + var lines: std.ArrayList([]const u8) = .empty; + defer lines.deinit(alloc); for (start_idx..index + 1) |i| { const token = tokens[i]; if (token != .doc_comment) break; - try lines.append(ast.tokenSlice(@intCast(i))[3..]); + try lines.append(alloc, ast.tokenSlice(@intCast(i))[3..]); } // Convert the lines to a multiline string. - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); + var buffer: std.Io.Writer.Allocating = .init(alloc); + defer buffer.deinit(); const prefix = findCommonPrefix(lines); for (lines.items) |line| { - try writer.writeAll(" \\\\"); - try writer.writeAll(line[@min(prefix, line.len)..]); - try writer.writeAll("\n"); + try buffer.writer.writeAll(" \\\\"); + try buffer.writer.writeAll(line[@min(prefix, line.len)..]); + try buffer.writer.writeAll("\n"); } - try writer.writeAll(";\n"); + try buffer.writer.writeAll(";\n"); return buffer.toOwnedSlice(); } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 642044067..c44fb0b09 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -7,6 +7,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const build_config = @import("../build_config.zig"); const uucode = @import("uucode"); +const EntryFormatter = @import("../config/formatter.zig").EntryFormatter; const key = @import("key.zig"); const KeyEvent = key.KeyEvent; @@ -1184,13 +1185,8 @@ pub const Action = union(enum) { /// action back into the format used by parse. pub fn format( self: Action, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - switch (self) { inline else => |value| { // All actions start with the tag. @@ -1206,16 +1202,16 @@ pub const Action = union(enum) { } fn formatValue( - writer: anytype, + writer: *std.Io.Writer, value: anytype, ) !void { const Value = @TypeOf(value); const value_info = @typeInfo(Value); switch (Value) { void => {}, - []const u8 => try std.zig.stringEscape(value, "", .{}, writer), + []const u8 => try std.zig.stringEscape(value, writer), else => switch (value_info) { - .@"enum" => try writer.print("{s}", .{@tagName(value)}), + .@"enum" => try writer.print("{t}", .{value}), .float => try writer.print("{d}", .{value}), .int => try writer.print("{d}", .{value}), .@"struct" => |info| if (!info.is_tuple) { @@ -1648,13 +1644,8 @@ pub const Trigger = struct { /// Format implementation for fmt package. pub fn format( self: Trigger, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - // Modifiers first if (self.mods.super) try writer.writeAll("super+"); if (self.mods.ctrl) try writer.writeAll("ctrl+"); @@ -1663,7 +1654,7 @@ pub const Trigger = struct { // Key switch (self.key) { - .physical => |k| try writer.print("{s}", .{@tagName(k)}), + .physical => |k| try writer.print("{t}", .{k}), .unicode => |c| try writer.print("{u}", .{c}), } } @@ -1721,13 +1712,8 @@ pub const Set = struct { /// action back into the format used by parse. pub fn format( self: Value, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - switch (self) { .leader => |set| { // the leader key was already printed. @@ -1758,26 +1744,34 @@ pub const Set = struct { /// that is shared between calls to nested levels of the set. /// For example, 'a>b>c=x' and 'a>b>d=y' will re-use the 'a>b' written /// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'. - pub fn formatEntries(self: Value, buffer_stream: anytype, formatter: anytype) !void { + pub fn formatEntries( + self: Value, + buffer: *std.Io.Writer, + formatter: EntryFormatter, + ) !void { switch (self) { .leader => |set| { // We'll rewind to this position after each sub-entry, // sharing the prefix between siblings. - const pos = try buffer_stream.getPos(); + const pos = buffer.end; var iter = set.bindings.iterator(); while (iter.next()) |binding| { - buffer_stream.seekTo(pos) catch unreachable; // can't fail - std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}) catch return error.OutOfMemory; - try binding.value_ptr.*.formatEntries(buffer_stream, formatter); + // I'm not exactly if this is safe for any arbitrary + // writer since the Writer interface does not have any + // rewind functions, but for our use case of a + // fixed-size buffer writer this should work just fine. + buffer.end = pos; + buffer.print(">{f}", .{binding.key_ptr.*}) catch return error.OutOfMemory; + try binding.value_ptr.*.formatEntries(buffer, formatter); } }, .leaf => |leaf| { // When we get to the leaf, the buffer_stream contains // the full sequence of keys needed to reach this action. - std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}) catch return error.OutOfMemory; - try formatter.formatEntry([]const u8, buffer_stream.getWritten()); + buffer.print("={f}", .{leaf.action}) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); }, } } @@ -3234,11 +3228,8 @@ test "action: format" { const a: Action = .{ .text = "👻" }; - var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(alloc); - - const writer = buf.writer(alloc); - try a.format("", .{}, writer); - - try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.items); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try a.format(&buf.writer); + try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written()); } diff --git a/src/input/command.zig b/src/input/command.zig index bf5061c12..ba55820fc 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -50,7 +50,7 @@ pub const Command = struct { return .{ .action_key = @tagName(self.action), - .action = std.fmt.comptimePrint("{s}", .{self.action}), + .action = std.fmt.comptimePrint("{t}", .{self.action}), .title = self.title, .description = self.description, }; @@ -94,6 +94,7 @@ pub const defaults: []const Command = defaults: { /// Defaults in C-compatible form. pub const defaultsC: []const Command.C = defaults: { + @setEvalBranchQuota(100_000); var result: [defaults.len]Command.C = undefined; for (defaults, 0..) |cmd, i| result[i] = cmd.comptimeCval(); const final = result; diff --git a/src/input/function_keys.zig b/src/input/function_keys.zig index 33a5b89c0..8c89b39bd 100644 --- a/src/input/function_keys.zig +++ b/src/input/function_keys.zig @@ -278,6 +278,7 @@ fn pcStyle(comptime fmt: []const u8) []Entry { // The comptime {} wrapper is superfluous but it prevents us from // accidentally running this function at runtime. comptime { + @setEvalBranchQuota(500_000); var entries: [modifiers.len]Entry = undefined; for (modifiers, 2.., 0..) |mods, code, i| { entries[i] = .{ diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig index 1382bbe95..4210f1f91 100644 --- a/src/input/helpgen_actions.zig +++ b/src/input/helpgen_actions.zig @@ -13,7 +13,7 @@ pub const Format = enum { /// Markdown formatted output markdown, - fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void { + fn formatFieldName(self: Format, writer: *std.Io.Writer, field_name: []const u8) !void { switch (self) { .plaintext => { try writer.writeAll(field_name); @@ -27,16 +27,16 @@ pub const Format = enum { } } - fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void { + fn formatDocLine(self: Format, writer: *std.Io.Writer, line: []const u8) !void { switch (self) { .plaintext => { - try writer.appendSlice(" "); - try writer.appendSlice(line); - try writer.appendSlice("\n"); + try writer.writeAll(" "); + try writer.writeAll(line); + try writer.writeAll("\n"); }, .markdown => { - try writer.appendSlice(line); - try writer.appendSlice("\n"); + try writer.writeAll(line); + try writer.writeAll("\n"); }, } } @@ -61,7 +61,7 @@ pub const Format = enum { /// Generate keybind actions documentation with the specified format pub fn generate( - writer: anytype, + writer: *std.Io.Writer, format: Format, show_docs: bool, page_allocator: std.mem.Allocator, @@ -70,8 +70,8 @@ pub fn generate( try writer.writeAll(header); } - var buffer = std.ArrayList(u8).init(page_allocator); - defer buffer.deinit(); + var stream: std.Io.Writer.Allocating = .init(page_allocator); + defer stream.deinit(); const fields = @typeInfo(KeybindAction).@"union".fields; inline for (fields) |field| { @@ -79,10 +79,9 @@ pub fn generate( // Write previously stored doc comment below all related actions if (show_docs and @hasDecl(help_strings.KeybindAction, field.name)) { - try writer.writeAll(buffer.items); + try writer.writeAll(stream.written()); try writer.writeAll("\n"); - - buffer.clearRetainingCapacity(); + stream.clearRetainingCapacity(); } if (show_docs) { @@ -101,13 +100,13 @@ pub fn generate( while (iter.next()) |s| { // If it is the last line and empty, then skip it. if (iter.peek() == null and s.len == 0) continue; - try format.formatDocLine(&buffer, s); + try format.formatDocLine(&stream.writer, s); } } } // Write any remaining buffered documentation - if (buffer.items.len > 0) { - try writer.writeAll(buffer.items); + if (stream.written().len > 0) { + try writer.writeAll(stream.written()); } } diff --git a/src/renderer/link.zig b/src/renderer/link.zig index 410fb8632..9f489ed48 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -34,19 +34,19 @@ pub const Set = struct { alloc: Allocator, config: []const inputpkg.Link, ) !Set { - var links = std.ArrayList(Link).init(alloc); - defer links.deinit(); + var links: std.ArrayList(Link) = .empty; + defer links.deinit(alloc); for (config) |link| { var regex = try link.oniRegex(); errdefer regex.deinit(); - try links.append(.{ + try links.append(alloc, .{ .regex = regex, .highlight = link.highlight, }); } - return .{ .links = try links.toOwnedSlice() }; + return .{ .links = try links.toOwnedSlice(alloc) }; } pub fn deinit(self: *Set, alloc: Allocator) void { @@ -77,8 +77,8 @@ pub const Set = struct { // as selections which contain the start and end points of // the match. There is no way to map these back to the link // configuration right now because we don't need to. - var matches = std.ArrayList(terminal.Selection).init(alloc); - defer matches.deinit(); + var matches: std.ArrayList(terminal.Selection) = .empty; + defer matches.deinit(alloc); // If our mouse is over an OSC8 link, then we can skip the regex // matches below since OSC8 takes priority. @@ -101,7 +101,7 @@ pub const Set = struct { ); } - return .{ .matches = try matches.toOwnedSlice() }; + return .{ .matches = try matches.toOwnedSlice(alloc) }; } fn matchSetFromOSC8( @@ -112,8 +112,6 @@ pub const Set = struct { mouse_pin: terminal.Pin, mouse_mods: inputpkg.Mods, ) !void { - _ = alloc; - // If the right mods aren't pressed, then we can't match. if (!mouse_mods.equal(inputpkg.ctrlOrSuper(.{}))) return; @@ -135,6 +133,7 @@ pub const Set = struct { if (link.id == .implicit) { const uri = link.uri.offset.ptr(page.memory)[0..link.uri.len]; return try self.matchSetFromOSC8Implicit( + alloc, matches, mouse_pin, uri, @@ -154,7 +153,7 @@ pub const Set = struct { // building our matching selection. if (!row.hyperlink) { if (current) |sel| { - try matches.append(sel); + try matches.append(alloc, sel); current = null; } @@ -191,7 +190,7 @@ pub const Set = struct { // No match, if we have a current selection then complete it. if (current) |sel| { - try matches.append(sel); + try matches.append(alloc, sel); current = null; } } @@ -203,6 +202,7 @@ pub const Set = struct { /// around the mouse pin. fn matchSetFromOSC8Implicit( self: *const Set, + alloc: Allocator, matches: *std.ArrayList(terminal.Selection), mouse_pin: terminal.Pin, uri: []const u8, @@ -264,7 +264,7 @@ pub const Set = struct { sel.endPtr().* = cell_pin; } - try matches.append(sel); + try matches.append(alloc, sel); } /// Fills matches with the matches from regex link matches. @@ -334,7 +334,7 @@ pub const Set = struct { => if (!sel.contains(screen, mouse_pin)) continue, } - try matches.append(sel); + try matches.append(alloc, sel); } } } diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 576237587..a8f62cdea 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -129,7 +129,7 @@ pub fn loadFromFile( /// mainImage function and don't define any of the uniforms. This function /// will convert the ShaderToy shader into a valid GLSL shader that can be /// compiled and linked. -pub fn glslFromShader(writer: anytype, src: []const u8) !void { +pub fn glslFromShader(writer: *std.Io.Writer, src: []const u8) !void { const prefix = @embedFile("shaders/shadertoy_prefix.glsl"); try writer.writeAll(prefix); try writer.writeAll("\n\n"); @@ -138,7 +138,7 @@ pub fn glslFromShader(writer: anytype, src: []const u8) !void { /// Convert a GLSL shader into SPIR-V assembly. pub fn spirvFromGlsl( - writer: anytype, + writer: *std.Io.Writer, errlog: ?*SpirvLog, src: [:0]const u8, ) !void { @@ -331,10 +331,10 @@ fn spvCross( /// Convert ShaderToy shader to null-terminated glsl for testing. fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 { - var list = std.ArrayList(u8).init(alloc); - defer list.deinit(); - try glslFromShader(list.writer(), src); - return try list.toOwnedSliceSentinel(0); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try glslFromShader(&buf.writer, src); + return try buf.toOwnedSliceSentinel(0); } test "spirv" { @@ -345,9 +345,8 @@ test "spirv" { defer alloc.free(src); var buf: [4096 * 4]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - const writer = buf_stream.writer(); - try spirvFromGlsl(writer, null, src); + var writer: std.Io.Writer = .fixed(&buf); + try spirvFromGlsl(&writer, null, src); } test "spirv invalid" { @@ -358,12 +357,11 @@ test "spirv invalid" { defer alloc.free(src); var buf: [4096 * 4]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - const writer = buf_stream.writer(); + var writer: std.Io.Writer = .fixed(&buf); var errlog: SpirvLog = .{ .alloc = alloc }; defer errlog.deinit(); - try testing.expectError(error.GlslangFailed, spirvFromGlsl(writer, &errlog, src)); + try testing.expectError(error.GlslangFailed, spirvFromGlsl(&writer, &errlog, src)); try testing.expect(errlog.info.len > 0); } @@ -374,9 +372,14 @@ test "shadertoy to msl" { const src = try testGlslZ(alloc, test_crt); defer alloc.free(src); - var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); - defer spvlist.deinit(); - try spirvFromGlsl(spvlist.writer(), null, src); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try spirvFromGlsl(&buf.writer, null, src); + + // TODO: Replace this with an aligned version of Writer.Allocating + var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty; + defer spvlist.deinit(alloc); + try spvlist.appendSlice(alloc, buf.written()); const msl = try mslFromSpv(alloc, spvlist.items); defer alloc.free(msl); @@ -389,9 +392,14 @@ test "shadertoy to glsl" { const src = try testGlslZ(alloc, test_crt); defer alloc.free(src); - var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); - defer spvlist.deinit(); - try spirvFromGlsl(spvlist.writer(), null, src); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try spirvFromGlsl(&buf.writer, null, src); + + // TODO: Replace this with an aligned version of Writer.Allocating + var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty; + defer spvlist.deinit(alloc); + try spvlist.appendSlice(alloc, buf.written()); const glsl = try glslFromSpv(alloc, spvlist.items); defer alloc.free(glsl); diff --git a/src/terminal/apc.zig b/src/terminal/apc.zig index a168da4a1..704c3fbe3 100644 --- a/src/terminal/apc.zig +++ b/src/terminal/apc.zig @@ -65,7 +65,9 @@ pub const Handler = struct { .kitty => |*p| kitty: { if (comptime !build_options.kitty_graphics) unreachable; - const command = p.complete() catch |err| { + // Use the same allocator that was used to create the parser. + const alloc = p.arena.child_allocator; + const command = p.complete(alloc) catch |err| { log.warn("kitty graphics protocol error: {}", .{err}); break :kitty null; }; diff --git a/src/terminal/dcs.zig b/src/terminal/dcs.zig index 4694fc457..971ea13a0 100644 --- a/src/terminal/dcs.zig +++ b/src/terminal/dcs.zig @@ -64,7 +64,7 @@ pub const Handler = struct { .state = .{ .tmux = .{ .max_bytes = self.max_bytes, - .buffer = try std.ArrayList(u8).initCapacity( + .buffer = try .initCapacity( alloc, 128, // Arbitrary choice to limit initial reallocs ), diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index dcb4850c9..2ce01f83a 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -59,7 +59,7 @@ pub const Parser = struct { errdefer arena.deinit(); var result: Parser = .{ .arena = arena, - .data = std.ArrayList(u8).init(alloc), + .data = .empty, .kv = .{}, .kv_temp_len = 0, .kv_current = 0, @@ -77,8 +77,8 @@ pub const Parser = struct { pub fn deinit(self: *Parser) void { // We don't free the hash map because its in the arena + self.data.deinit(self.arena.child_allocator); self.arena.deinit(); - self.data.deinit(); } /// Parse a complete command string. @@ -86,7 +86,7 @@ pub const Parser = struct { var parser = init(alloc); defer parser.deinit(); for (data) |c| try parser.feed(c); - return try parser.complete(); + return try parser.complete(alloc); } /// Feed a single byte to the parser. @@ -136,7 +136,7 @@ pub const Parser = struct { else => {}, }, - .data => try self.data.append(c), + .data => try self.data.append(self.arena.child_allocator, c), } } @@ -145,7 +145,7 @@ pub const Parser = struct { /// /// The allocator given will be used for the long-lived data /// of the final command. - pub fn complete(self: *Parser) !Command { + pub fn complete(self: *Parser, alloc: Allocator) !Command { switch (self.state) { // We can't ever end in the control key state and be valid. // This means the command looked something like "a=1,b" @@ -194,14 +194,14 @@ pub const Parser = struct { return .{ .control = control, .quiet = quiet, - .data = try self.decodeData(), + .data = try self.decodeData(alloc), }; } /// Decodes the payload data from base64 and returns it as a slice. /// This function will destroy the contents of self.data, it should /// only be used once we are done collecting payload bytes. - fn decodeData(self: *Parser) ![]const u8 { + fn decodeData(self: *Parser, alloc: Allocator) ![]const u8 { if (self.data.items.len == 0) { return ""; } @@ -225,7 +225,7 @@ pub const Parser = struct { // Remove the extra bytes. self.data.items.len = decoded.len; - return try self.data.toOwnedSlice(); + return try self.data.toOwnedSlice(alloc); } fn accumulateValue(self: *Parser, c: u8, overflow_state: State) !void { @@ -969,7 +969,7 @@ test "transmission command" { const input = "f=24,s=10,v=20"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -987,7 +987,7 @@ test "transmission ignores 'm' if medium is not direct" { const input = "a=t,t=t,m=1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1004,7 +1004,7 @@ test "transmission respects 'm' if medium is direct" { const input = "a=t,t=d,m=1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1021,7 +1021,7 @@ test "query command" { const input = "i=31,s=1,v=1,a=q,t=d,f=24;QUFBQQ"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .query); @@ -1041,7 +1041,7 @@ test "display command" { const input = "a=p,U=1,i=31,c=80,r=120"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1059,7 +1059,7 @@ test "delete command" { const input = "a=d,d=p,x=3,y=4"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .delete); @@ -1079,7 +1079,7 @@ test "no control data" { const input = ";QUFBQQ"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1094,7 +1094,7 @@ test "ignore unknown keys (long)" { const input = "f=24,s=10,v=20,hello=world"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1112,7 +1112,7 @@ test "ignore very long values" { const input = "f=24,s=10,v=2000000000000000000000000000000000000000"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1130,7 +1130,7 @@ test "ensure very large negative values don't get skipped" { const input = "a=p,i=1,z=-2000000000"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1147,7 +1147,7 @@ test "ensure proper overflow error for u32" { const input = "a=p,i=10000000000"; for (input) |c| try p.feed(c); - try testing.expectError(error.Overflow, p.complete()); + try testing.expectError(error.Overflow, p.complete(alloc)); } test "ensure proper overflow error for i32" { @@ -1158,7 +1158,7 @@ test "ensure proper overflow error for i32" { const input = "a=p,i=1,z=-9999999999"; for (input) |c| try p.feed(c); - try testing.expectError(error.Overflow, p.complete()); + try testing.expectError(error.Overflow, p.complete(alloc)); } test "all i32 values" { @@ -1171,7 +1171,7 @@ test "all i32 values" { defer p.deinit(); const input = "a=p,i=1,z=-1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1186,7 +1186,7 @@ test "all i32 values" { defer p.deinit(); const input = "a=p,i=1,H=-1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1201,7 +1201,7 @@ test "all i32 values" { defer p.deinit(); const input = "a=p,i=1,V=-1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1259,7 +1259,7 @@ test "delete range command 1" { const input = "a=d,d=r,x=3,y=4"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .delete); @@ -1279,7 +1279,7 @@ test "delete range command 2" { const input = "a=d,d=R,x=5,y=11"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .delete); @@ -1299,7 +1299,7 @@ test "delete range command 3" { const input = "a=d,d=R,x=5,y=4"; for (input) |c| try p.feed(c); - try testing.expectError(error.InvalidFormat, p.complete()); + try testing.expectError(error.InvalidFormat, p.complete(alloc)); } test "delete range command 4" { @@ -1310,7 +1310,7 @@ test "delete range command 4" { const input = "a=d,d=R,x=5"; for (input) |c| try p.feed(c); - try testing.expectError(error.InvalidFormat, p.complete()); + try testing.expectError(error.InvalidFormat, p.complete(alloc)); } test "delete range command 5" { @@ -1321,5 +1321,5 @@ test "delete range command 5" { const input = "a=d,d=R,y=5"; for (input) |c| try p.feed(c); - try testing.expectError(error.InvalidFormat, p.complete()); + try testing.expectError(error.InvalidFormat, p.complete(alloc)); } diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index f32b70be2..268f71601 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -259,15 +259,16 @@ pub const LoadingImage = struct { }; } - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); + var buf: [4096]u8 = undefined; + var buf_reader = file.reader(&buf); + const reader = &buf_reader.interface; // Read the file - var managed = std.ArrayList(u8).init(alloc); - errdefer managed.deinit(); + var managed: std.ArrayList(u8) = .empty; + errdefer managed.deinit(alloc); const size: usize = if (t.size > 0) @min(t.size, max_size) else max_size; - reader.readAllArrayList(&managed, size) catch |err| { - log.warn("failed to read temporary file: {}", .{err}); + reader.appendRemaining(alloc, &managed, .limited(size)) catch { + log.warn("failed to read temporary file: {?}", .{buf_reader.err}); return error.InvalidData; }; @@ -402,14 +403,15 @@ pub const LoadingImage = struct { fn decompressZlib(self: *LoadingImage, alloc: Allocator) !void { // Open our zlib stream - var fbs = std.io.fixedBufferStream(self.data.items); - var stream = std.compress.zlib.decompressor(fbs.reader()); + var buf: [std.compress.flate.max_window_len]u8 = undefined; + var reader: std.Io.Reader = .fixed(self.data.items); + var stream: std.compress.flate.Decompress = .init(&reader, .zlib, &buf); // Write it to an array list - var list = std.ArrayList(u8).init(alloc); - errdefer list.deinit(); - stream.reader().readAllArrayList(&list, max_size) catch |err| { - log.warn("failed to read decompressed data: {}", .{err}); + var list: std.ArrayList(u8) = .empty; + errdefer list.deinit(alloc); + stream.reader.appendRemaining(alloc, &list, .limited(max_size)) catch { + log.warn("failed to read decompressed data: {?}", .{stream.err}); return error.DecompressionFailed; }; diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 0c3022e4a..8aef0ece5 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -526,8 +526,8 @@ pub const ImageStorage = struct { used: bool, }; - var candidates = std.ArrayList(Candidate).init(alloc); - defer candidates.deinit(); + var candidates: std.ArrayList(Candidate) = .empty; + defer candidates.deinit(alloc); var it = self.images.iterator(); while (it.next()) |kv| { @@ -548,7 +548,7 @@ pub const ImageStorage = struct { break :used false; }; - try candidates.append(.{ + try candidates.append(alloc, .{ .id = img.id, .time = img.transmit_time, .used = used, diff --git a/src/terminal/tmux.zig b/src/terminal/tmux.zig index 1ea9f8c39..67c5a979c 100644 --- a/src/terminal/tmux.zig +++ b/src/terminal/tmux.zig @@ -17,7 +17,7 @@ pub const Client = struct { state: State = .idle, /// The buffer used to store in-progress notifications, output, etc. - buffer: std.ArrayList(u8), + buffer: std.Io.Writer.Allocating, /// The maximum size in bytes of the buffer. This is used to limit /// memory usage. If the buffer exceeds this size, the client will @@ -49,7 +49,7 @@ pub const Client = struct { // Handle a byte of input. pub fn put(self: *Client, byte: u8) !?Notification { - if (self.buffer.items.len >= self.max_bytes) { + if (self.buffer.written().len >= self.max_bytes) { self.broken(); return error.OutOfMemory; } @@ -81,18 +81,19 @@ pub const Client = struct { // If we're in a block then we accumulate until we see a newline // and then we check to see if that line ended the block. .block => if (byte == '\n') { + const written = self.buffer.written(); const idx = if (std.mem.lastIndexOfScalar( u8, - self.buffer.items, + written, '\n', )) |v| v + 1 else 0; - const line = self.buffer.items[idx..]; + const line = written[idx..]; if (std.mem.startsWith(u8, line, "%end") or std.mem.startsWith(u8, line, "%error")) { const err = std.mem.startsWith(u8, line, "%error"); - const output = std.mem.trimRight(u8, self.buffer.items[0..idx], "\r\n"); + const output = std.mem.trimRight(u8, written[0..idx], "\r\n"); // If it is an error then log it. if (err) log.warn("tmux control mode error={s}", .{output}); @@ -107,7 +108,7 @@ pub const Client = struct { }, } - try self.buffer.append(byte); + try self.buffer.writer.writeByte(byte); return null; } @@ -116,7 +117,7 @@ pub const Client = struct { assert(self.state == .notification); const line = line: { - var line = self.buffer.items; + var line = self.buffer.written(); if (line[line.len - 1] == '\r') line = line[0 .. line.len - 1]; break :line line; }; @@ -274,7 +275,7 @@ pub const Client = struct { // Mark the tmux state as broken. fn broken(self: *Client) void { self.state = .broken; - self.buffer.clearAndFree(); + self.buffer.deinit(); } }; @@ -313,7 +314,7 @@ test "tmux begin/end empty" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null); for ("%end 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null); @@ -326,7 +327,7 @@ test "tmux begin/error empty" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null); for ("%error 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null); @@ -339,7 +340,7 @@ test "tmux begin/end data" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null); for ("hello\nworld\n") |byte| try testing.expect(try c.put(byte) == null); @@ -353,7 +354,7 @@ test "tmux output" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%output %42 foo bar baz") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -366,7 +367,7 @@ test "tmux session-changed" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%session-changed $42 foo") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -379,7 +380,7 @@ test "tmux sessions-changed" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%sessions-changed") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -390,7 +391,7 @@ test "tmux sessions-changed carriage return" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%sessions-changed\r") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -401,7 +402,7 @@ test "tmux window-add" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%window-add @14") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -413,7 +414,7 @@ test "tmux window-renamed" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%window-renamed @42 bar") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig index 7692e6f54..d5c3f427a 100644 --- a/src/terminfo/Source.zig +++ b/src/terminfo/Source.zig @@ -115,9 +115,10 @@ pub fn xtgettcapMap(comptime self: Source) std.StaticStringMap([]const u8) { }, .numeric => |v| numeric: { var buf: [10]u8 = undefined; - const num_len = std.fmt.formatIntBuf(&buf, v, 10, .upper, .{}); + var writer: std.Io.Writer = .fixed(&buf); + writer.printInt(v, 10, .upper, .{}) catch unreachable; const final = buf; - break :numeric final[0..num_len]; + break :numeric final[0..writer.end]; }, }, }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 77fd2cc68..8c729fddc 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1518,7 +1518,7 @@ fn execCommand( .shell => |v| shell: { var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); - defer args.deinit(); + defer args.deinit(alloc); if (comptime builtin.os.tag == .windows) { // We run our shell wrapped in `cmd.exe` so that we don't have @@ -1539,21 +1539,21 @@ fn execCommand( "cmd.exe", }); - try args.append(cmd); - try args.append("/C"); + try args.append(alloc, cmd); + try args.append(alloc, "/C"); } else { // We run our shell wrapped in `/bin/sh` so that we don't have // to parse the command line ourselves if it has arguments. // Additionally, some environments (NixOS, I found) use /bin/sh // to setup some environment variables that are important to // have set. - try args.append("/bin/sh"); - if (internal_os.isFlatpak()) try args.append("-l"); - try args.append("-c"); + try args.append(alloc, "/bin/sh"); + if (internal_os.isFlatpak()) try args.append(alloc, "-l"); + try args.append(alloc, "-c"); } - try args.append(v); - break :shell try args.toOwnedSlice(); + try args.append(alloc, v); + break :shell try args.toOwnedSlice(alloc); }, }; } diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 90a697409..8b2648dbd 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -175,7 +175,9 @@ pub fn setupFeatures( inline for (fields) |field| n += field.name.len; break :capacity n; }; - var buffer = try std.BoundedArray(u8, capacity).init(0); + + var buf: [capacity]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); // Sort the fields so that the output is deterministic. This is // done at comptime so it has no runtime cost @@ -197,13 +199,13 @@ pub fn setupFeatures( inline for (fields_sorted) |name| { if (@field(features, name)) { - if (buffer.len > 0) try buffer.append(','); - try buffer.appendSlice(name); + if (writer.end > 0) try writer.writeByte(','); + try writer.writeAll(name); } } - if (buffer.len > 0) { - try env.put("GHOSTTY_SHELL_FEATURES", buffer.slice()); + if (writer.end > 0) { + try env.put("GHOSTTY_SHELL_FEATURES", buf[0..writer.end]); } } @@ -257,8 +259,8 @@ fn setupBash( resource_dir: []const u8, env: *EnvMap, ) !?config.Command { - var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 3); - defer args.deinit(); + var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 3); + defer args.deinit(alloc); // Iterator that yields each argument in the original command line. // This will allocate once proportionate to the command line length. @@ -267,21 +269,22 @@ fn setupBash( // Start accumulating arguments with the executable and initial flags. if (iter.next()) |exe| { - try args.append(try alloc.dupeZ(u8, exe)); + try args.append(alloc, try alloc.dupeZ(u8, exe)); } else return null; - try args.append("--posix"); + try args.append(alloc, "--posix"); // On macOS, we request a login shell to match that platform's norms. if (comptime builtin.target.os.tag.isDarwin()) { - try args.append("--login"); + try args.append(alloc, "--login"); } // Stores the list of intercepted command line flags that will be passed // to our shell integration script: --norc --noprofile // We always include at least "1" so the script can differentiate between // being manually sourced or automatically injected (from here). - var inject = try std.BoundedArray(u8, 32).init(0); - try inject.appendSlice("1"); + var buf: [32]u8 = undefined; + var inject: std.Io.Writer = .fixed(&buf); + try inject.writeAll("1"); // Walk through the rest of the given arguments. If we see an option that // would require complex or unsupported integration behavior, we bail out @@ -296,9 +299,9 @@ fn setupBash( if (std.mem.eql(u8, arg, "--posix")) { return null; } else if (std.mem.eql(u8, arg, "--norc")) { - try inject.appendSlice(" --norc"); + try inject.writeAll(" --norc"); } else if (std.mem.eql(u8, arg, "--noprofile")) { - try inject.appendSlice(" --noprofile"); + try inject.writeAll(" --noprofile"); } else if (std.mem.eql(u8, arg, "--rcfile") or std.mem.eql(u8, arg, "--init-file")) { rcfile = iter.next(); } else if (arg.len > 1 and arg[0] == '-' and arg[1] != '-') { @@ -306,20 +309,20 @@ fn setupBash( if (std.mem.indexOfScalar(u8, arg, 'c') != null) { return null; } - try args.append(try alloc.dupeZ(u8, arg)); + try args.append(alloc, try alloc.dupeZ(u8, arg)); } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) { // All remaining arguments should be passed directly to the shell // command. We shouldn't perform any further option processing. - try args.append(try alloc.dupeZ(u8, arg)); + try args.append(alloc, try alloc.dupeZ(u8, arg)); while (iter.next()) |remaining_arg| { - try args.append(try alloc.dupeZ(u8, remaining_arg)); + try args.append(alloc, try alloc.dupeZ(u8, remaining_arg)); } break; } else { - try args.append(try alloc.dupeZ(u8, arg)); + try args.append(alloc, try alloc.dupeZ(u8, arg)); } } - try env.put("GHOSTTY_BASH_INJECT", inject.slice()); + try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]); if (rcfile) |v| { try env.put("GHOSTTY_BASH_RCFILE", v); } @@ -356,7 +359,7 @@ fn setupBash( // Since we built up a command line, we don't need to wrap it in // ANOTHER shell anymore and can do a direct command. - return .{ .direct = try args.toOwnedSlice() }; + return .{ .direct = try args.toOwnedSlice(alloc) }; } test "bash" { From d59d754e29857365940f5e5c2d732b52e8fc00e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 14:41:08 -0700 Subject: [PATCH 132/319] Zig 0.15: zig build GTK exe --- src/Surface.zig | 16 ++- src/apprt/action.zig | 6 +- src/apprt/gtk/adw_version.zig | 2 +- src/apprt/gtk/build/gresource.zig | 6 +- src/apprt/gtk/cgroup.zig | 12 +- src/apprt/gtk/class/application.zig | 8 +- src/apprt/gtk/class/command_palette.zig | 5 +- src/apprt/gtk/class/global_shortcuts.zig | 6 +- src/apprt/gtk/class/split_tree.zig | 2 +- src/apprt/gtk/class/surface.zig | 24 ++-- src/apprt/gtk/class/tab.zig | 11 +- src/apprt/gtk/gtk_version.zig | 2 +- src/apprt/gtk/key.zig | 23 ++-- src/build/GhosttyExe.zig | 2 + src/build/mdgen/main_ghostty_1.zig | 7 +- src/build/mdgen/main_ghostty_5.zig | 15 ++- src/build/mdgen/mdgen.zig | 8 +- src/extra/bash.zig | 14 +- src/extra/fish.zig | 14 +- src/extra/vim.zig | 2 +- src/extra/zsh.zig | 14 +- src/font/Atlas.zig | 2 +- src/font/opentype/sfnt.zig | 6 +- src/font/shaper/feature.zig | 4 +- src/inspector/Inspector.zig | 1 + src/inspector/termio.zig | 24 ++-- src/main_build_data.zig | 4 +- src/main_ghostty.zig | 13 +- src/os/cgroup.zig | 26 +++- src/os/shell.zig | 157 ++++++++++++----------- src/pty.zig | 2 +- src/renderer/shadertoy.zig | 50 ++++---- src/synthetic/cli/Ascii.zig | 2 +- src/synthetic/cli/Osc.zig | 2 +- src/synthetic/cli/Utf8.zig | 2 +- src/terminal/PageList.zig | 2 +- src/terminal/Terminal.zig | 7 +- src/terminal/kitty/graphics_command.zig | 2 +- src/terminal/osc.zig | 2 +- src/terminal/style.zig | 4 +- src/terminfo/Source.zig | 2 +- src/termio/Exec.zig | 17 ++- src/termio/stream_handler.zig | 47 +++---- 43 files changed, 321 insertions(+), 256 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3b4bf872f..686647a67 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -305,19 +305,19 @@ const DerivedConfig = struct { // Build all of our links const links = links: { - var links = std.ArrayList(Link).init(alloc); - defer links.deinit(); + var links: std.ArrayList(Link) = .empty; + defer links.deinit(alloc); for (config.link.links.items) |link| { var regex = try link.oniRegex(); errdefer regex.deinit(); - try links.append(.{ + try links.append(alloc, .{ .regex = regex, .action = link.action, .highlight = link.highlight, }); } - break :links try links.toOwnedSlice(); + break :links try links.toOwnedSlice(alloc); }; errdefer { for (links) |*link| link.regex.deinit(); @@ -2493,7 +2493,7 @@ fn maybeHandleBinding( self.keyboard.bindings = null; // Attempt to perform the action - log.debug("key event binding flags={} action={}", .{ + log.debug("key event binding flags={} action={f}", .{ leaf.flags, action, }); @@ -5119,7 +5119,9 @@ fn writeScreenFile( defer file.close(); // Screen.dumpString writes byte-by-byte, so buffer it - var buf_writer = std.io.bufferedWriter(file.writer()); + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + var buf_writer = &file_writer.interface; // Write the scrollback contents. This requires a lock. { @@ -5169,7 +5171,7 @@ fn writeScreenFile( const br = sel.bottomRight(&self.io.terminal.screen); try self.io.terminal.screen.dumpString( - buf_writer.writer(), + buf_writer, .{ .tl = tl, .br = br, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index b356ff32f..14a8165f2 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -578,7 +578,7 @@ pub const SetTitle = struct { value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.title }); } @@ -602,7 +602,7 @@ pub const Pwd = struct { value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.pwd }); } @@ -630,7 +630,7 @@ pub const DesktopNotification = struct { value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.print("{s}{{ title: {s}, body: {s} }}", .{ @typeName(@This()), diff --git a/src/apprt/gtk/adw_version.zig b/src/apprt/gtk/adw_version.zig index 7ce88f585..6f7be52da 100644 --- a/src/apprt/gtk/adw_version.zig +++ b/src/apprt/gtk/adw_version.zig @@ -27,7 +27,7 @@ pub fn getRuntimeVersion() std.SemanticVersion { } pub fn logVersion() void { - log.info("libadwaita version build={} runtime={}", .{ + log.info("libadwaita version build={f} runtime={f}", .{ comptime_version, getRuntimeVersion(), }); diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index 7adcd3e44..fabd5763e 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -166,7 +166,7 @@ pub fn main() !void { /// Generate the icon resources. This works by looking up all the icons /// specified by `icon_sizes` in `images/icons/`. They are asserted to exist /// by trying to access the file. -fn genIcons(writer: anytype) !void { +fn genIcons(writer: *std.Io.Writer) !void { try writer.print( \\ \\ @@ -208,7 +208,7 @@ fn genIcons(writer: anytype) !void { } /// Generate the resources at the root prefix. -fn genRoot(writer: anytype) !void { +fn genRoot(writer: *std.Io.Writer) !void { try writer.print( \\ \\ @@ -240,7 +240,7 @@ fn genRoot(writer: anytype) !void { /// assuming these will be fn genUi( alloc: Allocator, - writer: anytype, + writer: *std.Io.Writer, files: *const std.ArrayListUnmanaged([]const u8), ) !void { try writer.print( diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 23c4d545e..697126798 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -50,7 +50,7 @@ pub fn init( ) orelse ""; if (!std.mem.eql(u8, original, current)) break :transient current; alloc.free(current); - std.time.sleep(25 * std.time.ns_per_ms); + std.Thread.sleep(25 * std.time.ns_per_ms); }; errdefer alloc.free(transient); log.info("transient scope created cgroup={s}", .{transient}); @@ -101,21 +101,21 @@ fn enableControllers(alloc: Allocator, cgroup: []const u8) !void { defer alloc.free(raw); // Build our string builder for enabling all controllers - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); // Controllers are space-separated var it = std.mem.splitScalar(u8, raw, ' '); while (it.next()) |controller| { - try builder.append('+'); - try builder.appendSlice(controller); - if (it.rest().len > 0) try builder.append(' '); + try builder.writer.writeByte('+'); + try builder.writer.writeAll(controller); + if (it.rest().len > 0) try builder.writer.writeByte(' '); } // Enable them all try internal_os.cgroup.configureControllers( cgroup, - builder.items, + builder.written(), ); } diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 90c72681d..af56130d3 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1044,7 +1044,9 @@ pub const Application = extern struct { defer file.close(); log.info("loading gtk-custom-css path={s}", .{path}); - const contents = try file.reader().readAllAlloc( + var buf: [4096]u8 = undefined; + var reader = file.reader(&buf); + const contents = try reader.interface.readAlloc( alloc, 5 * 1024 * 1024, // 5MB, ); @@ -1115,8 +1117,8 @@ pub const Application = extern struct { // This should really never, never happen. Its not critical enough // to actually crash, but this is a bug somewhere. An accelerator // for a trigger can't possibly be more than 1024 bytes. - error.NoSpaceLeft => { - log.warn("accelerator somehow longer than 1024 bytes: {}", .{trigger}); + error.WriteFailed => { + log.warn("accelerator somehow longer than 1024 bytes: {f}", .{trigger}); return; }, }; diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 8b7bb328c..6da49115e 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -485,10 +485,11 @@ const Command = extern struct { const command = priv.command orelse return null; - priv.action_key = std.fmt.allocPrintZ( + priv.action_key = std.fmt.allocPrintSentinel( priv.arena.allocator(), - "{}", + "{f}", .{command.action}, + 0, ) catch null; return priv.action_key; diff --git a/src/apprt/gtk/class/global_shortcuts.zig b/src/apprt/gtk/class/global_shortcuts.zig index 18280cfe9..9c67be7c1 100644 --- a/src/apprt/gtk/class/global_shortcuts.zig +++ b/src/apprt/gtk/class/global_shortcuts.zig @@ -188,9 +188,9 @@ pub const GlobalShortcuts = extern struct { // If there isn't space to translate the trigger, then our // buffer might be too small (but 1024 is insane!). In any case // we don't want to stop registering globals. - error.NoSpaceLeft => { + error.WriteFailed => { log.warn( - "buffer too small to translate trigger, ignoring={}", + "buffer too small to translate trigger, ignoring={f}", .{entry.key_ptr.*}, ); continue; @@ -257,7 +257,7 @@ pub const GlobalShortcuts = extern struct { const trigger = entry.key_ptr.*.ptr; const action = std.fmt.bufPrintZ( &action_buf, - "{}", + "{f}", .{entry.value_ptr.*}, ) catch continue; diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 977a7eab2..a498ca5dc 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -268,7 +268,7 @@ pub const SplitTree = extern struct { ); defer new_tree.deinit(); log.debug( - "new split at={} direction={} old_tree={} new_tree={}", + "new split at={} direction={} old_tree={f} new_tree={f}", .{ handle, direction, old_tree, &new_tree }, ); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index d49885256..20b8b5cba 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -2117,13 +2117,11 @@ pub const Surface = extern struct { const alloc = Application.default().allocator(); if (ext.gValueHolds(value, gdk.FileList.getGObjectType())) { - var data = std.ArrayList(u8).init(alloc); - defer data.deinit(); + var stream: std.Io.Writer.Allocating = .init(alloc); + defer stream.deinit(); - var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ - .child_writer = data.writer(), - }; - const writer = shell_escape_writer.writer(); + var shell_escape_writer: internal_os.ShellEscapeWriter = .init(&stream.writer); + const writer = &shell_escape_writer.writer; const list: ?*glib.SList = list: { const unboxed = value.getBoxed() orelse return 0; @@ -2151,7 +2149,7 @@ pub const Surface = extern struct { } } - const string = data.toOwnedSliceSentinel(0) catch |err| { + const string = stream.toOwnedSliceSentinel(0) catch |err| { log.err("unable to convert to a slice: {}", .{err}); return 0; }; @@ -2164,13 +2162,11 @@ pub const Surface = extern struct { const object = value.getObject() orelse return 0; const file = gobject.ext.cast(gio.File, object) orelse return 0; const path = file.getPath() orelse return 0; - var data = std.ArrayList(u8).init(alloc); - defer data.deinit(); + var stream: std.Io.Writer.Allocating = .init(alloc); + defer stream.deinit(); - var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ - .child_writer = data.writer(), - }; - const writer = shell_escape_writer.writer(); + var shell_escape_writer: internal_os.ShellEscapeWriter = .init(&stream.writer); + const writer = &shell_escape_writer.writer; writer.writeAll(std.mem.span(path)) catch |err| { log.err("unable to write path to buffer: {}", .{err}); return 0; @@ -2180,7 +2176,7 @@ pub const Surface = extern struct { return 0; }; - const string = data.toOwnedSliceSentinel(0) catch |err| { + const string = stream.toOwnedSliceSentinel(0) catch |err| { log.err("unable to convert to a slice: {}", .{err}); return 0; }; diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 26b006bb6..941fa00a9 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -405,22 +405,21 @@ pub const Tab = extern struct { }; // Use an allocator to build up our string as we write it. - var buf: std.ArrayList(u8) = .init(Application.default().allocator()); + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); defer buf.deinit(); - const writer = buf.writer(); // If our bell is ringing, then we prefix the bell icon to the title. if (bell_ringing and config.@"bell-features".title) { - writer.writeAll("🔔 ") catch {}; + buf.writer.writeAll("🔔 ") catch {}; } // If we're zoomed, prefix with the magnifying glass emoji. if (zoomed) { - writer.writeAll("🔍 ") catch {}; + buf.writer.writeAll("🔍 ") catch {}; } - writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain); - return glib.ext.dupeZ(u8, buf.items); + buf.writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain); + return glib.ext.dupeZ(u8, buf.written()); } const C = Common(Self, Private); diff --git a/src/apprt/gtk/gtk_version.zig b/src/apprt/gtk/gtk_version.zig index 6f3d733a5..71edb076d 100644 --- a/src/apprt/gtk/gtk_version.zig +++ b/src/apprt/gtk/gtk_version.zig @@ -26,7 +26,7 @@ pub fn getRuntimeVersion() std.SemanticVersion { } pub fn logVersion() void { - log.info("GTK version build={} runtime={}", .{ + log.info("GTK version build={f} runtime={f}", .{ comptime_version, getRuntimeVersion(), }); diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index a00b0312e..bf0f0e2f6 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -12,9 +12,8 @@ const winproto = @import("winproto.zig"); pub fn accelFromTrigger( buf: []u8, trigger: input.Binding.Trigger, -) error{NoSpaceLeft}!?[:0]const u8 { - var buf_stream = std.io.fixedBufferStream(buf); - const writer = buf_stream.writer(); +) error{WriteFailed}!?[:0]const u8 { + var writer: std.Io.Writer = .fixed(buf); // Modifiers if (trigger.mods.shift) try writer.writeAll(""); @@ -23,11 +22,11 @@ pub fn accelFromTrigger( if (trigger.mods.super) try writer.writeAll(""); // Write our key - if (!try writeTriggerKey(writer, trigger)) return null; + if (!try writeTriggerKey(&writer, trigger)) return null; // We need to make the string null terminated. try writer.writeByte(0); - const slice = buf_stream.getWritten(); + const slice = writer.buffered(); return slice[0 .. slice.len - 1 :0]; } @@ -36,9 +35,8 @@ pub fn accelFromTrigger( pub fn xdgShortcutFromTrigger( buf: []u8, trigger: input.Binding.Trigger, -) error{NoSpaceLeft}!?[:0]const u8 { - var buf_stream = std.io.fixedBufferStream(buf); - const writer = buf_stream.writer(); +) error{WriteFailed}!?[:0]const u8 { + var writer: std.Io.Writer = .fixed(buf); // Modifiers if (trigger.mods.shift) try writer.writeAll("SHIFT+"); @@ -52,15 +50,18 @@ pub fn xdgShortcutFromTrigger( // to *X11's* keysyms (which I assume is a subset of libxkbcommon's). // I haven't been able to any evidence to back up that assumption but // this works for now - if (!try writeTriggerKey(writer, trigger)) return null; + if (!try writeTriggerKey(&writer, trigger)) return null; // We need to make the string null terminated. try writer.writeByte(0); - const slice = buf_stream.getWritten(); + const slice = writer.buffered(); return slice[0 .. slice.len - 1 :0]; } -fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) error{NoSpaceLeft}!bool { +fn writeTriggerKey( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) error{WriteFailed}!bool { switch (trigger.key) { .physical => |k| { const keyval = keyvalFromKey(k) orelse return false; diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index 083aecdb5..3e63b6026 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -21,6 +21,8 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty .omit_frame_pointer = cfg.strip, .unwind_tables = if (cfg.strip) .none else .sync, }), + // Crashes on x86_64 self-hosted on 0.15.1 + .use_llvm = true, }); const install_step = b.addInstallArtifact(exe, .{}); diff --git a/src/build/mdgen/main_ghostty_1.zig b/src/build/mdgen/main_ghostty_1.zig index b3663de8d..2bb413d93 100644 --- a/src/build/mdgen/main_ghostty_1.zig +++ b/src/build/mdgen/main_ghostty_1.zig @@ -2,12 +2,15 @@ const std = @import("std"); const gen = @import("mdgen.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; const alloc = gpa.allocator(); - const writer = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; try gen.substitute(alloc, @embedFile("ghostty_1_header.md"), writer); try gen.genActions(writer); try gen.genConfig(writer, true); try gen.substitute(alloc, @embedFile("ghostty_1_footer.md"), writer); + try writer.flush(); } diff --git a/src/build/mdgen/main_ghostty_5.zig b/src/build/mdgen/main_ghostty_5.zig index 77c72b946..2123b0bce 100644 --- a/src/build/mdgen/main_ghostty_5.zig +++ b/src/build/mdgen/main_ghostty_5.zig @@ -2,12 +2,15 @@ const std = @import("std"); const gen = @import("mdgen.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; const alloc = gpa.allocator(); - const output = std.io.getStdOut().writer(); - try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), output); - try gen.genConfig(output, false); - try gen.genKeybindActions(output); - try gen.substitute(alloc, @embedFile("ghostty_5_footer.md"), output); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; + try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), writer); + try gen.genConfig(writer, false); + try gen.genKeybindActions(writer); + try gen.substitute(alloc, @embedFile("ghostty_5_footer.md"), writer); + try writer.flush(); } diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index 53ed02067..530c8964f 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -5,7 +5,7 @@ const Config = @import("../../config/Config.zig"); const Action = @import("../../cli/ghostty.zig").Action; const KeybindAction = @import("../../input/Binding.zig").Action; -pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { +pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: *std.Io.Writer) !void { const output = try alloc.alloc(u8, std.mem.replacementSize( u8, input, @@ -18,7 +18,7 @@ pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) try writer.writeAll(output); } -pub fn genConfig(writer: anytype, cli: bool) !void { +pub fn genConfig(writer: *std.Io.Writer, cli: bool) !void { try writer.writeAll( \\ \\# CONFIGURATION OPTIONS @@ -48,7 +48,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void { } } -pub fn genActions(writer: anytype) !void { +pub fn genActions(writer: *std.Io.Writer) !void { try writer.writeAll( \\ \\# COMMAND LINE ACTIONS @@ -83,7 +83,7 @@ pub fn genActions(writer: anytype) !void { } } -pub fn genKeybindActions(writer: anytype) !void { +pub fn genKeybindActions(writer: *std.Io.Writer) !void { try writer.writeAll( \\ \\# KEYBIND ACTIONS diff --git a/src/extra/bash.zig b/src/extra/bash.zig index 536cadbc4..ee9a7895c 100644 --- a/src/extra/bash.zig +++ b/src/extra/bash.zig @@ -19,18 +19,18 @@ pub const completions = comptimeGenerateBashCompletions(); fn comptimeGenerateBashCompletions() []const u8 { comptime { @setEvalBranchQuota(50000); - var counter = std.io.countingWriter(std.io.null_writer); - try writeBashCompletions(&counter.writer()); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeBashCompletions(&counter.writer); - var buf: [counter.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeBashCompletions(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeBashCompletions(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } -fn writeBashCompletions(writer: anytype) !void { +fn writeBashCompletions(writer: *std.Io.Writer) !void { const pad1 = " "; const pad2 = pad1 ++ pad1; const pad3 = pad2 ++ pad1; diff --git a/src/extra/fish.zig b/src/extra/fish.zig index 5a4b38e32..7ffc23093 100644 --- a/src/extra/fish.zig +++ b/src/extra/fish.zig @@ -11,18 +11,18 @@ pub const completions = comptimeGenerateCompletions(); fn comptimeGenerateCompletions() []const u8 { comptime { @setEvalBranchQuota(50000); - var counter = std.io.countingWriter(std.io.null_writer); - try writeCompletions(&counter.writer()); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeCompletions(&counter.writer); - var buf: [counter.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeCompletions(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeCompletions(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } -fn writeCompletions(writer: anytype) !void { +fn writeCompletions(writer: *std.Io.Writer) !void { { try writer.writeAll("set -l commands \""); var count: usize = 0; diff --git a/src/extra/vim.zig b/src/extra/vim.zig index ff85e820b..2c0192d03 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -72,7 +72,7 @@ fn comptimeGenSyntax() []const u8 { } /// Writes the syntax file to the given writer. -fn writeSyntax(writer: anytype) !void { +fn writeSyntax(writer: *std.Io.Writer) !void { try writer.writeAll( \\" Vim syntax file \\" Language: Ghostty config file diff --git a/src/extra/zsh.zig b/src/extra/zsh.zig index 6bddcd285..2fad4234a 100644 --- a/src/extra/zsh.zig +++ b/src/extra/zsh.zig @@ -12,18 +12,18 @@ const equals_required = "=-:::"; fn comptimeGenerateZshCompletions() []const u8 { comptime { @setEvalBranchQuota(50000); - var counter = std.io.countingWriter(std.io.null_writer); - try writeZshCompletions(&counter.writer()); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeZshCompletions(&counter.writer); - var buf: [counter.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeZshCompletions(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeZshCompletions(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } -fn writeZshCompletions(writer: anytype) !void { +fn writeZshCompletions(writer: *std.Io.Writer) !void { try writer.writeAll( \\#compdef ghostty \\ diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 68ccaddcc..e2d9a5de2 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -355,7 +355,7 @@ pub fn clear(self: *Atlas) void { /// swapped because PPM expects RGB. This would be /// easy enough to fix so next time someone needs /// to debug a color atlas they should fix it. -pub fn dump(self: Atlas, writer: anytype) !void { +pub fn dump(self: Atlas, writer: *std.Io.Writer) !void { try writer.print( \\P{c} \\{d} {d} diff --git a/src/font/opentype/sfnt.zig b/src/font/opentype/sfnt.zig index 82c118bce..d97d9e2d5 100644 --- a/src/font/opentype/sfnt.zig +++ b/src/font/opentype/sfnt.zig @@ -106,7 +106,7 @@ fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type { self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; @@ -176,7 +176,7 @@ pub const SFNT = struct { self: OffsetSubtable, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; @@ -210,7 +210,7 @@ pub const SFNT = struct { self: TableRecord, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; diff --git a/src/font/shaper/feature.zig b/src/font/shaper/feature.zig index 5fce7d6eb..40770376b 100644 --- a/src/font/shaper/feature.zig +++ b/src/font/shaper/feature.zig @@ -201,7 +201,7 @@ pub const Feature = struct { self: Feature, comptime layout: []const u8, opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = layout; _ = opts; @@ -262,7 +262,7 @@ pub const FeatureList = struct { self: FeatureList, comptime layout: []const u8, opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { for (self.features.items, 0..) |feature, i| { try feature.format(layout, opts, writer); diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index d23510949..49b05bd7f 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -600,6 +600,7 @@ fn renderModesWindow(self: *Inspector) void { const t = self.surface.renderer_state.terminal; inline for (@typeInfo(terminal.Mode).@"enum".fields) |field| { + @setEvalBranchQuota(6000); const tag: terminal.modes.ModeTag = @bitCast(@as(terminal.modes.ModeTag.Backing, field.value)); cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 49ab00ecd..03a3b0375 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -43,9 +43,9 @@ pub const VTEvent = struct { ) !VTEvent { var md = Metadata.init(alloc); errdefer md.deinit(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); - try encodeAction(alloc, buf.writer(), &md, action); + try encodeAction(alloc, &buf.writer, &md, action); const str = try buf.toOwnedSliceSentinel(0); errdefer alloc.free(str); @@ -115,7 +115,7 @@ pub const VTEvent = struct { /// Encode a parser action as a string that we show in the logs. fn encodeAction( alloc: Allocator, - writer: anytype, + writer: *std.Io.Writer, md: *Metadata, action: terminal.Parser.Action, ) !void { @@ -125,16 +125,16 @@ pub const VTEvent = struct { .csi_dispatch => |v| try encodeCSI(writer, v), .esc_dispatch => |v| try encodeEsc(writer, v), .osc_dispatch => |v| try encodeOSC(alloc, writer, md, v), - else => try writer.print("{}", .{action}), + else => try writer.print("{f}", .{action}), } } - fn encodePrint(writer: anytype, action: terminal.Parser.Action) !void { + fn encodePrint(writer: *std.Io.Writer, action: terminal.Parser.Action) !void { const ch = action.print; try writer.print("'{u}' (U+{X})", .{ ch, ch }); } - fn encodeExecute(writer: anytype, action: terminal.Parser.Action) !void { + fn encodeExecute(writer: *std.Io.Writer, action: terminal.Parser.Action) !void { const ch = action.execute; switch (ch) { 0x00 => try writer.writeAll("NUL"), @@ -158,7 +158,7 @@ pub const VTEvent = struct { try writer.print(" (0x{X})", .{ch}); } - fn encodeCSI(writer: anytype, csi: terminal.Parser.Action.CSI) !void { + fn encodeCSI(writer: *std.Io.Writer, csi: terminal.Parser.Action.CSI) !void { for (csi.intermediates) |v| try writer.print("{c} ", .{v}); for (csi.params, 0..) |v, i| { if (i != 0) try writer.writeByte(';'); @@ -168,14 +168,14 @@ pub const VTEvent = struct { try writer.writeByte(csi.final); } - fn encodeEsc(writer: anytype, esc: terminal.Parser.Action.ESC) !void { + fn encodeEsc(writer: *std.Io.Writer, esc: terminal.Parser.Action.ESC) !void { for (esc.intermediates) |v| try writer.print("{c} ", .{v}); try writer.writeByte(esc.final); } fn encodeOSC( alloc: Allocator, - writer: anytype, + writer: *std.Io.Writer, md: *Metadata, osc: terminal.osc.Command, ) !void { @@ -265,10 +265,10 @@ pub const VTEvent = struct { const s = if (field.type == void) try alloc.dupeZ(u8, tag_name) else - try std.fmt.allocPrintZ(alloc, "{s}={}", .{ + try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{ tag_name, @field(value, field.name), - }); + }, 0); try md.put(key, s); } @@ -283,7 +283,7 @@ pub const VTEvent = struct { else => switch (Value) { u8, u16 => try md.put( key, - try std.fmt.allocPrintZ(alloc, "{}", .{value}), + try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0), ), []const u8, diff --git a/src/main_build_data.zig b/src/main_build_data.zig index 13e604389..9fffdd0d6 100644 --- a/src/main_build_data.zig +++ b/src/main_build_data.zig @@ -33,7 +33,9 @@ pub fn main() !void { const action = action_ orelse return error.NoAction; // Our output always goes to stdout. - const writer = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; switch (action) { .bash => try writer.writeAll(@import("extra/bash.zig").completions), .fish => try writer.writeAll(@import("extra/fish.zig").completions), diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 9c121b950..decfc609c 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -35,7 +35,9 @@ pub fn main() !MainReturn { // a global is because the C API needs to be able to access this state; // no other Zig code should EVER access the global state. state.init() catch |err| { - const stderr = std.io.getStdErr().writer(); + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; defer posix.exit(1); const ErrSet = @TypeOf(err) || error{Unknown}; switch (@as(ErrSet, @errorCast(err))) { @@ -54,6 +56,7 @@ pub fn main() !MainReturn { else => try stderr.print("invalid CLI invocation err={}\n", .{err}), } + try stderr.flush(); }; defer state.deinit(); const alloc = state.alloc; @@ -154,8 +157,12 @@ fn logFn( .stderr => { // Always try default to send to stderr - const stderr = std.io.getStdErr().writer(); - nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; + var buffer: [1024]u8 = undefined; + var stderr = std.fs.File.stderr().writer(&buffer); + const writer = &stderr.interface; + nosuspend writer.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; + // TODO: Do we want to use flushless stderr in the future? + writer.flush() catch {}; }, } } diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 4f13921c5..97c796f8b 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -19,8 +19,9 @@ pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { defer file.close(); // Read it all into memory -- we don't expect this file to ever be that large. - var buf_reader = std.io.bufferedReader(file.reader()); - const contents = try buf_reader.reader().readAllAlloc( + var reader_buf: [4096]u8 = undefined; + var reader = file.reader(&reader_buf); + const contents = try reader.interface.readAlloc( alloc, 1 * 1024 * 1024, // 1MB ); @@ -52,7 +53,11 @@ pub fn create( ); const file = try std.fs.cwd().openFile(pid_path, .{ .mode = .write_only }); defer file.close(); - try file.writer().print("{}", .{pid}); + + var file_buf: [64]u8 = undefined; + var writer = file.writer(&file_buf); + try writer.interface.print("{}", .{pid}); + try writer.interface.flush(); } } @@ -182,8 +187,9 @@ pub fn controllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { // Read it all into memory -- we don't expect this file to ever // be that large. - var buf_reader = std.io.bufferedReader(file.reader()); - const contents = try buf_reader.reader().readAllAlloc( + var reader_buf: [4096]u8 = undefined; + var reader = file.reader(&reader_buf); + const contents = try reader.interface.readAlloc( alloc, 1 * 1024 * 1024, // 1MB ); @@ -213,7 +219,10 @@ pub fn configureControllers( defer file.close(); // Write - try file.writer().writeAll(v); + var writer_buf: [4096]u8 = undefined; + var writer = file.writer(&writer_buf); + try writer.interface.writeAll(v); + try writer.interface.flush(); } pub const Limit = union(enum) { @@ -242,5 +251,8 @@ pub fn configureLimit(cgroup: []const u8, limit: Limit) !void { defer file.close(); // Write our limit in bytes - try file.writer().print("{}", .{size}); + var writer_buf: [4096]u8 = undefined; + var writer = file.writer(&writer_buf); + try writer.interface.print("{}", .{size}); + try writer.interface.flush(); } diff --git a/src/os/shell.zig b/src/os/shell.zig index 3e57031dd..a6f23e843 100644 --- a/src/os/shell.zig +++ b/src/os/shell.zig @@ -1,110 +1,121 @@ const std = @import("std"); const testing = std.testing; +const Writer = std.Io.Writer; /// Writer that escapes characters that shells treat specially to reduce the /// risk of injection attacks or other such weirdness. Specifically excludes /// linefeeds so that they can be used to delineate lists of file paths. /// -/// T should be a Zig type that follows the `std.io.Writer` interface. -pub fn ShellEscapeWriter(comptime T: type) type { - return struct { - child_writer: T, +/// T should be a Zig type that follows the `std.Io.Writer` interface. +pub const ShellEscapeWriter = struct { + writer: Writer, + child: *Writer, - fn write(self: *ShellEscapeWriter(T), data: []const u8) error{Error}!usize { - var count: usize = 0; - for (data) |byte| { - const buf = switch (byte) { - '\\', - '"', - '\'', - '$', - '`', - '*', - '?', - ' ', - '|', - '(', - ')', - => &[_]u8{ '\\', byte }, - else => &[_]u8{byte}, - }; - self.child_writer.writeAll(buf) catch return error.Error; - count += 1; - } - return count; + pub fn init(child: *Writer) ShellEscapeWriter { + return .{ + .writer = .{ + // TODO: Actually use a buffer here + .buffer = &.{}, + .vtable = &.{ .drain = ShellEscapeWriter.drain }, + }, + .child = child, + }; + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { + const self: *ShellEscapeWriter = @fieldParentPtr("writer", w); + + // TODO: This is a very naive implementation and does not really make + // full use of the post-Writergate API. However, since we know that + // this is going into an Allocating writer anyways, we can be a bit + // less strict here. + + var count: usize = 0; + for (data[0 .. data.len - 1]) |chunk| try self.writeEscaped(chunk, &count); + + for (0..splat) |_| try self.writeEscaped(data[data.len], &count); + return count; + } + + fn writeEscaped( + self: *ShellEscapeWriter, + s: []const u8, + count: *usize, + ) Writer.Error!void { + for (s) |byte| { + const buf = switch (byte) { + '\\', + '"', + '\'', + '$', + '`', + '*', + '?', + ' ', + '|', + '(', + ')', + => &[_]u8{ '\\', byte }, + else => &[_]u8{byte}, + }; + try self.child.writeAll(buf); + count.* += 1; } - - const Writer = std.io.Writer(*ShellEscapeWriter(T), error{Error}, write); - - pub fn init(child_writer: T) ShellEscapeWriter(T) { - return .{ .child_writer = child_writer }; - } - - pub fn writer(self: *ShellEscapeWriter(T)) Writer { - return .{ .context = self }; - } - }; -} + } +}; test "shell escape 1" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("abc"); - try testing.expectEqualStrings("abc", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("abc"); + try testing.expectEqualStrings("abc", writer.buffered()); } test "shell escape 2" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a c"); - try testing.expectEqualStrings("a\\ c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a c"); + try testing.expectEqualStrings("a\\ c", writer.buffered()); } test "shell escape 3" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a?c"); - try testing.expectEqualStrings("a\\?c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a?c"); + try testing.expectEqualStrings("a\\?c", writer.buffered()); } test "shell escape 4" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a\\c"); - try testing.expectEqualStrings("a\\\\c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a\\c"); + try testing.expectEqualStrings("a\\\\c", writer.buffered()); } test "shell escape 5" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a|c"); - try testing.expectEqualStrings("a\\|c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a|c"); + try testing.expectEqualStrings("a\\|c", writer.buffered()); } test "shell escape 6" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a\"c"); - try testing.expectEqualStrings("a\\\"c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a\"c"); + try testing.expectEqualStrings("a\\\"c", writer.buffered()); } test "shell escape 7" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a(1)"); - try testing.expectEqualStrings("a\\(1\\)", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a(1)"); + try testing.expectEqualStrings("a\\(1\\)", writer.buffered()); } diff --git a/src/pty.zig b/src/pty.zig index 02906b778..1ab88d40f 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -216,7 +216,7 @@ const PosixPty = struct { // Reset our signals var sa: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.DFL }, - .mask = posix.empty_sigset, + .mask = posix.sigemptyset(), .flags = 0, }; posix.sigaction(posix.SIG.ABRT, &sa, null); diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index a8f62cdea..d31c36dee 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -38,8 +38,8 @@ pub fn loadFromFiles( paths: configpkg.RepeatablePath, target: Target, ) ![]const [:0]const u8 { - var list = std.ArrayList([:0]const u8).init(alloc_gpa); - defer list.deinit(); + var list: std.ArrayList([:0]const u8) = .empty; + defer list.deinit(alloc_gpa); errdefer for (list.items) |shader| alloc_gpa.free(shader); for (paths.value.items) |item| { @@ -56,10 +56,10 @@ pub fn loadFromFiles( return err; }; log.info("loaded custom shader path={s}", .{path}); - try list.append(shader); + try list.append(alloc_gpa, shader); } - return try list.toOwnedSlice(); + return try list.toOwnedSlice(alloc_gpa); } /// Load a single shader from a file and convert it to the target language @@ -73,34 +73,35 @@ pub fn loadFromFile( defer arena.deinit(); const alloc = arena.allocator(); - // Load the shader file - const cwd = std.fs.cwd(); - const file = try cwd.openFile(path, .{}); - defer file.close(); - // Read it all into memory -- we don't expect shaders to be large. - var buf_reader = std.io.bufferedReader(file.reader()); - const src = try buf_reader.reader().readAllAlloc( - alloc, - 4 * 1024 * 1024, // 4MB - ); + const src = src: { + // Load the shader file + const cwd = std.fs.cwd(); + const file = try cwd.openFile(path, .{}); + defer file.close(); + + var buf: [4096]u8 = undefined; + var reader = file.reader(&buf); + break :src try reader.interface.readAlloc( + alloc, + 4 * 1024 * 1024, // 4MB + ); + }; // Convert to full GLSL const glsl: [:0]const u8 = glsl: { - var list = std.ArrayList(u8).init(alloc); - try glslFromShader(list.writer(), src); - try list.append(0); - break :glsl list.items[0 .. list.items.len - 1 :0]; + var stream: std.Io.Writer.Allocating = .init(alloc); + try glslFromShader(&stream.writer, src); + try stream.writer.writeByte(0); + break :glsl stream.written()[0 .. stream.written().len - 1 :0]; }; // Convert to SPIR-V const spirv: []const u8 = spirv: { - // SpirV pointer must be aligned to 4 bytes since we expect - // a slice of words. - var list = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); + var stream: std.Io.Writer.Allocating = .init(alloc); var errlog: SpirvLog = .{ .alloc = alloc }; defer errlog.deinit(); - spirvFromGlsl(list.writer(), &errlog, glsl) catch |err| { + spirvFromGlsl(&stream.writer, &errlog, glsl) catch |err| { if (errlog.info.len > 0 or errlog.debug.len > 0) { log.warn("spirv error path={s} info={s} debug={s}", .{ path, @@ -111,6 +112,11 @@ pub fn loadFromFile( return err; }; + + // SpirV pointer must be aligned to 4 bytes since we expect + // a slice of words. + var list: std.ArrayListAligned(u8, .of(u32)) = .empty; + try list.appendSlice(alloc, stream.written()); break :spirv list.items; }; diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index 25e5bb00b..14de5edb8 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -23,7 +23,7 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void { +pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { _ = self; var gen: synthetic.Bytes = .{ diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig index 4792cda6b..8dd3d46ed 100644 --- a/src/synthetic/cli/Osc.zig +++ b/src/synthetic/cli/Osc.zig @@ -29,7 +29,7 @@ pub fn destroy(self: *Osc, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Osc, writer: anytype, rand: std.Random) !void { +pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void { var gen: synthetic.Osc = .{ .rand = rand, .p_valid = self.opts.@"p-valid", diff --git a/src/synthetic/cli/Utf8.zig b/src/synthetic/cli/Utf8.zig index 28a11f891..65437d1ae 100644 --- a/src/synthetic/cli/Utf8.zig +++ b/src/synthetic/cli/Utf8.zig @@ -23,7 +23,7 @@ pub fn destroy(self: *Utf8, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Utf8, writer: anytype, rand: std.Random) !void { +pub fn run(self: *Utf8, writer: *std.Io.Writer, rand: std.Random) !void { _ = self; var gen: synthetic.Utf8 = .{ diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 2f6864784..9bf116598 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2723,7 +2723,7 @@ pub fn encodeUtf8( /// 1 | etc.| | 4 /// +-----+ : /// +--------+ -pub fn diagram(self: *const PageList, writer: anytype) !void { +pub fn diagram(self: *const PageList, writer: *std.Io.Writer) !void { const active_pin = self.getTopLeft(.active); var active = false; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d15f2deb3..69bcbcb84 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -239,6 +239,11 @@ pub fn deinit(self: *Terminal, alloc: Allocator) void { self.* = undefined; } +/// The general allocator we should use for this terminal. +fn gpa(self: *Terminal) Allocator { + return self.screen.alloc; +} + /// Print UTF-8 encoded string to the terminal. pub fn printString(self: *Terminal, str: []const u8) !void { const view = try std.unicode.Utf8View.init(str); @@ -2531,7 +2536,7 @@ pub fn resize( /// Set the pwd for the terminal. pub fn setPwd(self: *Terminal, pwd: []const u8) !void { self.pwd.clearRetainingCapacity(); - try self.pwd.appendSlice(pwd); + try self.pwd.appendSlice(self.gpa(), pwd); } /// Returns the pwd for the terminal, if any. The memory is owned by the diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index 2ce01f83a..08b2e378a 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -276,7 +276,7 @@ pub const Response = struct { placement_id: u32 = 0, message: []const u8 = "OK", - pub fn encode(self: Response, writer: anytype) !void { + pub fn encode(self: Response, writer: *std.Io.Writer) !void { // We only encode a result if we have either an id or an image number. if (self.id == 0 and self.image_number == 0) return; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index d244310bb..897a5ef0f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -274,7 +274,7 @@ pub const Terminator = enum { self: Terminator, comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.writeAll(self.string()); } diff --git a/src/terminal/style.zig b/src/terminal/style.zig index 4f51cbc71..eac577a53 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -60,7 +60,7 @@ pub const Style = struct { self: Color, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; @@ -228,7 +228,7 @@ pub const Style = struct { self: Style, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig index d5c3f427a..6a068c906 100644 --- a/src/terminfo/Source.zig +++ b/src/terminfo/Source.zig @@ -43,7 +43,7 @@ pub const Capability = struct { /// Encode as a terminfo source file. The encoding is always done in a /// human-readable format with whitespace. Fields are always written in the /// order of the slices on this struct; this will not do any reordering. -pub fn encode(self: Source, writer: anytype) !void { +pub fn encode(self: Source, writer: *std.Io.Writer) !void { // Encode the names in the order specified for (self.names, 0..) |name, i| { if (i != 0) try writer.writeAll("|"); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 8c729fddc..d822377c9 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -591,6 +591,17 @@ const Subprocess = struct { flatpak_command: ?FlatpakHostCommand = null, linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default, + const ArgsFormatter = struct { + args: []const [:0]const u8, + + pub fn format(this: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void { + for (this.args, 0..) |a, i| { + if (i > 0) try writer.writeAll(", "); + try writer.print("`{s}`", .{a}); + } + } + }; + /// Initialize the subprocess. This will NOT start it, this only sets /// up the internal state necessary to start it later. pub fn init(gpa: Allocator, cfg: Config) !Subprocess { @@ -897,7 +908,7 @@ const Subprocess = struct { self.pty = null; }; - log.debug("starting command command={s}", .{self.args}); + log.debug("starting command command={f}", .{ArgsFormatter{ .args = self.args }}); // If we can't access the cwd, then don't set any cwd and inherit. // This is important because our cwd can be set by the shell (OSC 7) @@ -1157,7 +1168,7 @@ const Subprocess = struct { const res = posix.waitpid(pid, std.c.W.NOHANG); log.debug("waitpid result={}", .{res.pid}); if (res.pid != 0) break; - std.time.sleep(10 * std.time.ns_per_ms); + std.Thread.sleep(10 * std.time.ns_per_ms); } }, } @@ -1180,7 +1191,7 @@ const Subprocess = struct { const pgid = c.getpgid(pid); if (pgid == my_pgid) { log.warn("pgid is our own, retrying", .{}); - std.time.sleep(10 * std.time.ns_per_ms); + std.Thread.sleep(10 * std.time.ns_per_ms); continue; } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 9a7e8b416..06ff29809 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -310,11 +310,11 @@ pub const StreamHandler = struct { .kitty => |*kitty_cmd| { if (self.terminal.kittyGraphics(self.alloc, kitty_cmd)) |resp| { var buf: [1024]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - try resp.encode(buf_stream.writer()); - const final = buf_stream.getWritten(); + var writer: std.Io.Writer = .fixed(&buf); + try resp.encode(&writer); + const final = writer.buffered(); if (final.len > 2) { - log.debug("kitty graphics response: {s}", .{std.fmt.fmtSliceHexLower(final)}); + log.debug("kitty graphics response: {x}", .{final}); self.messageWriter(try termio.Message.writeReq(self.alloc, final)); } } @@ -1141,7 +1141,7 @@ pub const StreamHandler = struct { // We need to unescape the path. We first try to unescape onto // the stack and fall back to heap allocation if we have to. - var pathBuf: [1024]u8 = undefined; + var path_buf: [1024]u8 = undefined; const path, const heap = path: { // 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 @@ -1156,15 +1156,16 @@ pub const StreamHandler = struct { break :path .{ path, false }; // First try to stack-allocate - var fba = std.heap.FixedBufferAllocator.init(&pathBuf); - if (std.fmt.allocPrint(fba.allocator(), "{raw}", .{uri.path})) |v| - break :path .{ v, false } - else |_| {} + var stack_writer: std.Io.Writer = .fixed(&path_buf); + if (uri.path.formatRaw(&stack_writer)) |_| { + break :path .{ stack_writer.buffered(), false }; + } else |_| {} // Fall back to heap - if (std.fmt.allocPrint(self.alloc, "{raw}", .{uri.path})) |v| - break :path .{ v, true } - else |_| {} + var alloc_writer: std.Io.Writer.Allocating = .init(self.alloc); + if (uri.path.formatRaw(&alloc_writer.writer)) |_| { + break :path .{ alloc_writer.written(), true }; + } else |_| {} // Fall back to using it directly... log.warn("failed to unescape OSC 7 path, using it directly path={s}", .{path}); @@ -1471,15 +1472,15 @@ pub const StreamHandler = struct { self: *StreamHandler, request: terminal.kitty.color.OSC, ) !void { - var buf = std.ArrayList(u8).init(self.alloc); - defer buf.deinit(); - const writer = buf.writer(); + var stream: std.Io.Writer.Allocating = .init(self.alloc); + defer stream.deinit(); + const writer = &stream.writer; for (request.list.items) |item| { switch (item) { .query => |key| { // If the writer buffer is empty, we need to write our prefix - if (buf.items.len == 0) try writer.writeAll("\x1b]21"); + if (stream.written().len == 0) try writer.writeAll("\x1b]21"); const color: terminal.color.RGB = switch (key) { .palette => |palette| self.terminal.color_palette.colors[palette], @@ -1488,17 +1489,17 @@ pub const StreamHandler = struct { .background => self.background_color orelse self.default_background_color, .cursor => self.cursor_color orelse self.default_cursor_color, else => { - log.warn("ignoring unsupported kitty color protocol key: {}", .{key}); + log.warn("ignoring unsupported kitty color protocol key: {f}", .{key}); continue; }, }, } orelse { - try writer.print(";{}=", .{key}); + try writer.print(";{f}=", .{key}); continue; }; try writer.print( - ";{}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", + ";{f}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", .{ key, color.r, color.g, color.b }, ); }, @@ -1525,7 +1526,7 @@ pub const StreamHandler = struct { }, else => { log.warn( - "ignoring unsupported kitty color protocol key: {}", + "ignoring unsupported kitty color protocol key: {f}", .{v.key}, ); continue; @@ -1560,7 +1561,7 @@ pub const StreamHandler = struct { }, else => { log.warn( - "ignoring unsupported kitty color protocol key: {}", + "ignoring unsupported kitty color protocol key: {f}", .{key}, ); continue; @@ -1576,12 +1577,12 @@ pub const StreamHandler = struct { } // If we had any writes to our buffer, we queue them now - if (buf.items.len > 0) { + if (stream.written().len > 0) { try writer.writeAll(request.terminator.string()); self.messageWriter(.{ .write_alloc = .{ .alloc = self.alloc, - .data = try buf.toOwnedSlice(), + .data = try stream.toOwnedSlice(), }, }); } From 01126075322b6661990044a0a6deaae70b3c1001 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:31:11 -0700 Subject: [PATCH 133/319] Zig 0.15: zig build test macOS --- pkg/macos/os/log.zig | 3 ++- src/apprt/embedded.zig | 6 +++--- src/crash/sentry.zig | 9 ++++++--- src/termio/Exec.zig | 27 ++++++++++++++------------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pkg/macos/os/log.zig b/pkg/macos/os/log.zig index 32ecb3296..219c914da 100644 --- a/pkg/macos/os/log.zig +++ b/pkg/macos/os/log.zig @@ -32,10 +32,11 @@ pub const Log = opaque { comptime format: []const u8, args: anytype, ) void { - const str = nosuspend std.fmt.allocPrintZ( + const str = nosuspend std.fmt.allocPrintSentinel( alloc, format, args, + 0, ) catch return; defer alloc.free(str); zig_os_log_with_type(self, typ, str.ptr); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 08d8291ef..617557995 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -266,8 +266,8 @@ pub const App = struct { // embedded apprt. self.performPreAction(target, action, value); - log.debug("dispatching action target={s} action={} value={}", .{ - @tagName(target), + log.debug("dispatching action target={t} action={} value={any}", .{ + target, action, value, }); @@ -1910,7 +1910,7 @@ pub const CAPI = struct { }; return ptr.core_surface.performBindingAction(action) catch |err| { - log.err("error performing binding action action={} err={}", .{ action, err }); + log.err("error performing binding action action={f} err={}", .{ action, err }); return false; }; } diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index 820c3e9a1..555b70fe9 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -265,8 +265,8 @@ pub const Transport = struct { const json = envelope.serialize(); defer sentry.free(@ptrCast(json.ptr)); var parsed: crash.Envelope = parsed: { - var fbs = std.io.fixedBufferStream(json); - break :parsed try crash.Envelope.parse(alloc, fbs.reader()); + var reader: std.Io.Reader = .fixed(json); + break :parsed try crash.Envelope.parse(alloc, &reader); }; defer parsed.deinit(); @@ -298,7 +298,10 @@ pub const Transport = struct { }); const file = try std.fs.cwd().createFile(path, .{}); defer file.close(); - try file.writer().writeAll(json); + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + try file_writer.interface.writeAll(json); + try file_writer.end(); log.warn("crash report written to disk path={s}", .{path}); } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d822377c9..481b994db 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1440,7 +1440,7 @@ fn execCommand( // grow if necessary for a longer command (uncommon). 9, ); - defer args.deinit(); + defer args.deinit(alloc); // The reason for executing login this way is unclear. This // comment will attempt to explain but prepare for a truly @@ -1487,40 +1487,41 @@ fn execCommand( // macOS. // // Awesome. - try args.append("/usr/bin/login"); - if (hush) try args.append("-q"); - try args.append("-flp"); - try args.append(username); + try args.append(alloc, "/usr/bin/login"); + if (hush) try args.append(alloc, "-q"); + try args.append(alloc, "-flp"); + try args.append(alloc, username); switch (command) { // Direct args can be passed directly to login, since // login uses execvp we don't need to worry about PATH // searching. - .direct => |v| try args.appendSlice(v), + .direct => |v| try args.appendSlice(alloc, v), .shell => |v| { // Use "exec" to replace the bash process with // our intended command so we don't have a parent // process hanging around. - const cmd = try std.fmt.allocPrintZ( + const cmd = try std.fmt.allocPrintSentinel( alloc, "exec -l {s}", .{v}, + 0, ); // We execute bash with "--noprofile --norc" so that it doesn't // load startup files so that (1) our shell integration doesn't // break and (2) user configuration doesn't mess this process // up. - try args.append("/bin/bash"); - try args.append("--noprofile"); - try args.append("--norc"); - try args.append("-c"); - try args.append(cmd); + try args.append(alloc, "/bin/bash"); + try args.append(alloc, "--noprofile"); + try args.append(alloc, "--norc"); + try args.append(alloc, "-c"); + try args.append(alloc, cmd); }, } - return try args.toOwnedSlice(); + return try args.toOwnedSlice(alloc); } return switch (command) { From f0cfaa958026f09a723026c1359d1eacd0ec99d2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:38:32 -0700 Subject: [PATCH 134/319] zig 0.15: build on macOS --- src/build/GhosttyLib.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index 4b9729170..d5ec66de8 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -28,7 +28,9 @@ pub fn initStatic( .omit_frame_pointer = deps.config.strip, .unwind_tables = if (deps.config.strip) .none else .sync, }), - .linkage = .static, + + // Fails on self-hosted x86_64 on macOS + .use_llvm = true, }); lib.linkLibC(); From 9ec3b1b152a1e766a9296cd31b414d96b89ae0a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:41:59 -0700 Subject: [PATCH 135/319] Zig 0.15: webdata --- src/build/webgen/main_actions.zig | 6 ++++-- src/build/webgen/main_commands.zig | 8 +++++--- src/build/webgen/main_config.zig | 10 ++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig index 5002a5bac..85357b972 100644 --- a/src/build/webgen/main_actions.zig +++ b/src/build/webgen/main_actions.zig @@ -3,6 +3,8 @@ const help_strings = @import("help_strings"); const helpgen_actions = @import("../../input/helpgen_actions.zig"); pub fn main() !void { - const output = std.io.getStdOut().writer(); - try helpgen_actions.generate(output, .markdown, true, std.heap.page_allocator); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + try helpgen_actions.generate(stdout, .markdown, true, std.heap.page_allocator); } diff --git a/src/build/webgen/main_commands.zig b/src/build/webgen/main_commands.zig index ad5c75734..65f144522 100644 --- a/src/build/webgen/main_commands.zig +++ b/src/build/webgen/main_commands.zig @@ -3,14 +3,16 @@ const Action = @import("../../cli/ghostty.zig").Action; const help_strings = @import("help_strings"); pub fn main() !void { - const output = std.io.getStdOut().writer(); - try genActions(output); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + try genActions(stdout); } // Note: as a shortcut for defining inline editOnGithubLinks per cli action the user // is directed to the folder view on Github. This includes a README pointing them to // the files to edit. -pub fn genActions(writer: anytype) !void { +pub fn genActions(writer: *std.Io.Writer) !void { // Write the header try writer.writeAll( \\--- diff --git a/src/build/webgen/main_config.zig b/src/build/webgen/main_config.zig index 1bde2f9cc..1363fadc4 100644 --- a/src/build/webgen/main_config.zig +++ b/src/build/webgen/main_config.zig @@ -3,11 +3,13 @@ const Config = @import("../../config/Config.zig"); const help_strings = @import("help_strings"); pub fn main() !void { - const output = std.io.getStdOut().writer(); - try genConfig(output); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + try genConfig(stdout); } -pub fn genConfig(writer: anytype) !void { +pub fn genConfig(writer: *std.Io.Writer) !void { // Write the header try writer.writeAll( \\--- @@ -122,7 +124,7 @@ pub fn genConfig(writer: anytype) !void { } } -fn endBlock(writer: anytype, block: anytype) !void { +fn endBlock(writer: *std.Io.Writer, block: anytype) !void { if (block) |v| switch (v) { .text => {}, .code => try writer.writeAll("```\n"), From 2af424268a2f5f6d44ba0f9271e1b536612499e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:52:05 -0700 Subject: [PATCH 136/319] Zig 0.15: emit bench --- src/synthetic/cli.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig index 36832587c..b32469aab 100644 --- a/src/synthetic/cli.zig +++ b/src/synthetic/cli.zig @@ -90,12 +90,17 @@ fn mainActionImpl( const rand = prng.random(); // Our output always goes to stdout. - const writer = std.io.getStdOut().writer(); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; // Create our implementation const impl = try Impl.create(alloc, opts); defer impl.destroy(alloc); try impl.run(writer, rand); + + // Always flush + try writer.flush(); } test { From e1b5464babfcc1e33be8e87b58a2d7143f1a4631 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:53:38 -0700 Subject: [PATCH 137/319] Zig 0.15: build snap --- src/apprt/gtk/class/surface.zig | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 20b8b5cba..5ca964fe3 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1375,11 +1375,11 @@ pub const Surface = extern struct { defer arena.deinit(); const alloc = arena.allocator(); - var env_to_remove = std.ArrayList([]const u8).init(alloc); - var env_to_update = std.ArrayList(struct { + var env_to_remove: std.ArrayList([]const u8) = .empty; + var env_to_update: std.ArrayList(struct { key: []const u8, value: []const u8, - }).init(alloc); + }) = .empty; var it = env_map.iterator(); while (it.next()) |entry| { @@ -1392,13 +1392,11 @@ pub const Surface = extern struct { // Any env var starting with SNAP must be removed if (std.mem.startsWith(u8, key, "SNAP_")) { - try env_to_remove.append(key); + try env_to_remove.append(alloc, key); continue; } - var filtered_paths = std.ArrayList([]const u8).init(alloc); - defer filtered_paths.deinit(); - + var filtered_paths: std.ArrayList([]const u8) = .empty; var modified = false; var paths = std.mem.splitAny(u8, value, ":"); while (paths.next()) |path| { @@ -1411,15 +1409,15 @@ pub const Surface = extern struct { break; } }; - if (include) try filtered_paths.append(path); + if (include) try filtered_paths.append(alloc, path); } if (modified) { if (filtered_paths.items.len > 0) { const new_value = try std.mem.join(alloc, ":", filtered_paths.items); - try env_to_update.append(.{ .key = key, .value = new_value }); + try env_to_update.append(alloc, .{ .key = key, .value = new_value }); } else { - try env_to_remove.append(key); + try env_to_remove.append(alloc, key); } } } From 4e3e0ed0563f359d8dcba3984d0a094f38049fe0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:54:40 -0700 Subject: [PATCH 138/319] Zig 0.15: Flatpak --- flatpak/dependencies.yml | 8 ++++---- src/termio/Exec.zig | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml index 082107923..667e4662c 100644 --- a/flatpak/dependencies.yml +++ b/flatpak/dependencies.yml @@ -13,12 +13,12 @@ modules: - chmod a+x /app/zig/zig sources: - type: archive - sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c - url: https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz + sha256: c61c5da6edeea14ca51ecd5e4520c6f4189ef5250383db33d01848293bfafe05 + url: https://ziglang.org/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz only-arches: [x86_64] - type: archive - sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b - url: https://ziglang.org/download/0.14.1/zig-aarch64-linux-0.14.1.tar.xz + sha256: bb4a8d2ad735e7fba764c497ddf4243cb129fece4148da3222a7046d3f1f19fe + url: https://ziglang.org/download/0.15.1/zig-aarch64-linux-0.15.1.tar.xz only-arches: [aarch64] - name: bzip2-redirect diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 481b994db..319ae0ee6 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -971,7 +971,7 @@ const Subprocess = struct { const pid = try cmd.spawn(alloc); errdefer killCommandFlatpak(cmd); - log.info("started subcommand on host via flatpak API path={s} pid={?}", .{ + log.info("started subcommand on host via flatpak API path={s} pid={}", .{ self.args[0], pid, }); From 87b77e19803f1fccd4eaa7136ee2f0104369cfa2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 15:57:09 -0700 Subject: [PATCH 139/319] ci: cleanup --- .github/workflows/test.yml | 6 +++--- nix/package.nix | 4 ++-- src/build/GhosttyLib.zig | 3 +++ src/build/docker/debian/Dockerfile | 4 ++-- src/font/Collection.zig | 9 +++++---- src/synthetic/cli/Ascii.zig | 10 ++++------ src/synthetic/cli/Osc.zig | 10 ++++------ src/synthetic/cli/Utf8.zig | 10 ++++------ src/terminal/kitty/graphics_command.zig | 24 ++++++++++++------------ src/terminfo/Source.zig | 6 +++--- src/terminfo/ghostty.zig | 6 +++--- 11 files changed, 45 insertions(+), 47 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1638b0fd9..0b038a56d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -333,7 +333,7 @@ jobs: run: nix build .#ghostty-releasefast - name: Check version - run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.ReleaseFast' + run: result/bin/ghostty +version | grep -q '.ReleaseFast' - name: Check to see if the binary has been stripped run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'no symbols' @@ -342,7 +342,7 @@ jobs: run: nix build .#ghostty-debug - name: Check version - run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.Debug' + run: result/bin/ghostty +version | grep -q '.Debug' - name: Check to see if the binary has not been stripped run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main' @@ -513,7 +513,7 @@ jobs: $fileContent = Get-Content -Path "build.zig" -Raw $pattern = 'buildpkg\.requireZig\("(.*?)"\);' $zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value - $version = "zig-windows-x86_64-$zigVersion" + $version = "zig-x86_64-windows-$zigVersion" Write-Output $version $uri = "https://ziglang.org/download/$zigVersion/$version.zip" Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip" diff --git a/nix/package.nix b/nix/package.nix index fcc80b9dc..73d31c3b9 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -10,7 +10,7 @@ git, ncurses, pkg-config, - zig_0_14, + zig_0_15, pandoc, revision ? "dirty", optimize ? "Debug", @@ -27,7 +27,7 @@ # https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is # ultimately acted on and has made its way to a nixpkgs implementation, this # can probably be removed in favor of that. - zig_hook = zig_0_14.hook.overrideAttrs { + zig_hook = zig_0_15.hook.overrideAttrs { zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off"; }; gi_typelib_path = import ./build-support/gi-typelib-path.nix { diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index d5ec66de8..2ac383544 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -82,6 +82,9 @@ pub fn initShared( .omit_frame_pointer = deps.config.strip, .unwind_tables = if (deps.config.strip) .none else .sync, }), + + // Fails on self-hosted x86_64 + .use_llvm = true, }); _ = try deps.add(lib); diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile index 73c7da7c8..815d395cd 100644 --- a/src/build/docker/debian/Dockerfile +++ b/src/build/docker/debian/Dockerfile @@ -29,10 +29,10 @@ COPY ./build.zig /src # Install zig # https://ziglang.org/download/ -RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" && \ +RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$(uname -m)-linux-$ZIG_VERSION.tar.xz" && \ tar -xf /tmp/zig.tar.xz -C /opt && \ rm /tmp/zig.tar.xz && \ - ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig + ln -s "/opt/zig-$(uname -m)-linux-$ZIG_VERSION/zig" /usr/local/bin/zig COPY . /src diff --git a/src/font/Collection.zig b/src/font/Collection.zig index e91fe03ae..5ec076608 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -223,12 +223,13 @@ fn getFaceFromEntry( // Calculate the scale factor for this // entry now that we have a loaded face. - entry.scale_factor = .{ - .scale = self.scaleFactor( + if (entry.scale_factor == .adjustment) { + const factor = self.scaleFactor( face.getMetrics(), entry.scale_factor.adjustment, - ), - }; + ); + entry.scale_factor = .{ .scale = factor }; + } // If our scale factor is something other // than 1.0 then we need to resize the face. diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index 14de5edb8..339bdee2e 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -35,10 +35,10 @@ pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { while (true) { const data = try gen.next(&buf); writer.writeAll(data) catch |err| { - const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed - error.NoSpaceLeft => return, // fixed buffer full + error.WriteFailed => return, // fixed buffer full else => return err, } }; @@ -56,8 +56,6 @@ test Ascii { const rand = prng.random(); var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - - try impl.run(writer, rand); + var writer: std.Io.Writer = .fixed(&buf); + try impl.run(&writer, rand); } diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig index 8dd3d46ed..23d19e4ae 100644 --- a/src/synthetic/cli/Osc.zig +++ b/src/synthetic/cli/Osc.zig @@ -39,10 +39,10 @@ pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void { while (true) { const data = try gen.next(&buf); writer.writeAll(data) catch |err| { - const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed - error.NoSpaceLeft => return, // fixed buffer full + error.WriteFailed => return, // fixed buffer full else => return err, } }; @@ -60,8 +60,6 @@ test Osc { const rand = prng.random(); var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - - try impl.run(writer, rand); + var writer: std.Io.Writer = .fixed(&buf); + try impl.run(&writer, rand); } diff --git a/src/synthetic/cli/Utf8.zig b/src/synthetic/cli/Utf8.zig index 65437d1ae..3c2fddef7 100644 --- a/src/synthetic/cli/Utf8.zig +++ b/src/synthetic/cli/Utf8.zig @@ -34,10 +34,10 @@ pub fn run(self: *Utf8, writer: *std.Io.Writer, rand: std.Random) !void { while (true) { const data = try gen.next(&buf); writer.writeAll(data) catch |err| { - const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed - error.NoSpaceLeft => return, // fixed buffer full + error.WriteFailed => return, // fixed buffer full else => return err, } }; @@ -55,8 +55,6 @@ test Utf8 { const rand = prng.random(); var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - - try impl.run(writer, rand); + var writer: std.Io.Writer = .fixed(&buf); + try impl.run(&writer, rand); } diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index 08b2e378a..99a7cdaac 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -1214,41 +1214,41 @@ test "all i32 values" { test "response: encode nothing without ID or image number" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{}; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("", writer.buffered()); } test "response: encode with only image id" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{ .id = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_Gi=4;OK\x1b\\", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("\x1b_Gi=4;OK\x1b\\", writer.buffered()); } test "response: encode with only image number" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{ .image_number = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_GI=4;OK\x1b\\", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("\x1b_GI=4;OK\x1b\\", writer.buffered()); } test "response: encode with image ID and number" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{ .id = 12, .image_number = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", writer.buffered()); } test "delete range command 1" { diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig index 6a068c906..91fee1ace 100644 --- a/src/terminfo/Source.zig +++ b/src/terminfo/Source.zig @@ -230,8 +230,8 @@ test "encode" { // Encode var buf: [1024]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - try src.encode(buf_stream.writer()); + var writer: std.Io.Writer = .fixed(&buf); + try src.encode(&writer); const expected = "ghostty|xterm-ghostty|Ghostty,\n" ++ @@ -239,5 +239,5 @@ test "encode" { "\tccc@,\n" ++ "\tcolors#256,\n" ++ "\tbel=^G,\n"; - try std.testing.expectEqualStrings(@as([]const u8, expected), buf_stream.getWritten()); + try std.testing.expectEqualStrings(@as([]const u8, expected), writer.buffered()); } diff --git a/src/terminfo/ghostty.zig b/src/terminfo/ghostty.zig index f96154c9b..6451836e7 100644 --- a/src/terminfo/ghostty.zig +++ b/src/terminfo/ghostty.zig @@ -391,7 +391,7 @@ pub const ghostty: Source = .{ test "encode" { // Encode var buf: [1024 * 16]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - try ghostty.encode(buf_stream.writer()); - try std.testing.expect(buf_stream.getWritten().len > 0); + var writer: std.Io.Writer = .fixed(&buf); + try ghostty.encode(&writer); + try std.testing.expect(writer.buffered().len > 0); } From ba100dddff42ebdbbbd5fe3058a83487bcab1574 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 16:09:07 -0700 Subject: [PATCH 140/319] update deps --- build.zig.zon.json | 75 ++++++++++++++++++++-------------- build.zig.zon.nix | 84 +++++++++++++++++++++++++-------------- build.zig.zon.txt | 23 ++++++----- flatpak/zig-packages.json | 80 ++++++++++++++++++++++--------------- 4 files changed, 161 insertions(+), 101 deletions(-) diff --git a/build.zig.zon.json b/build.zig.zon.json index 83625f765..445f39827 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -64,10 +64,10 @@ "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", "hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=" }, - "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q": { + "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs": { "name": "libxev", - "url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz", - "hash": "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc=" + "url": "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + "hash": "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc=" }, "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": { "name": "libxml2", @@ -109,15 +109,20 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE": { + "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", - "hash": "sha256-iq9Oyns5e5Tnz2BKPPPTuyJ03BN4bK0dsmSPE1s0wig=" + "url": "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + "hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" }, - "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { + "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA": { "name": "vaxis", - "url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", - "hash": "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY=" + "url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", + "hash": "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM=" + }, + "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": { + "name": "vaxis", + "url": "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", + "hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=" }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "name": "wayland", @@ -134,40 +139,50 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP": { + "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", - "hash": "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0=" + "url": "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", + "hash": "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg=" }, - "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { + "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR": { "name": "zf", - "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", - "hash": "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I=" + "url": "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", + "hash": "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ=" }, - "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": { + "zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM": { "name": "zg", - "url": "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc", - "hash": "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA=" + "url": "git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9", + "hash": "sha256-P0ieLuOQ05wKVaMmeNKJIxCWMIdyeKkmhsj8Ps80BGU=" }, - "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ": { + "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9": { + "name": "zg", + "url": "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz", + "hash": "sha256-BZhz1nPqxK6hdsJQ66n7Jk4zMgFSGLXm8eU0CX/7mDI=" + }, + "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": { "name": "zig_js", - "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", - "hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=" + "url": "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + "hash": "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M=" }, - "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": { + "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK": { "name": "zig_objc", - "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", - "hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=" + "url": "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + "hash": "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw=" }, - "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": { + "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe": { "name": "zig_wayland", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", - "hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk=" + "url": "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + "hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=" }, - "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj": { + "zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL": { "name": "zigimg", - "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d", - "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=" + "url": "git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726", + "hash": "sha256-Ko5RuxxTAvpUHCnWEdHqNl7b+PVUAxg1/OPmzGGjdt0=" + }, + "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": { + "name": "zigimg", + "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", + "hash": "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM=" }, "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": { "name": "zlib", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index abd5a37c5..0111f0769 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -187,11 +187,11 @@ in }; } { - name = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q"; + name = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs"; path = fetchZigArtifact { name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz"; - hash = "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc="; + url = "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"; + hash = "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc="; }; } { @@ -259,19 +259,27 @@ in }; } { - name = "uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE"; + name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz"; - hash = "sha256-iq9Oyns5e5Tnz2BKPPPTuyJ03BN4bK0dsmSPE1s0wig="; + url = "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz"; + hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; }; } { - name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; + name = "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA"; path = fetchZigArtifact { name = "vaxis"; - url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; - hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; + url = "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz"; + hash = "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM="; + }; + } + { + name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ"; + path = fetchZigArtifact { + name = "vaxis"; + url = "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz"; + hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="; }; } { @@ -299,59 +307,75 @@ in }; } { - name = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP"; + name = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz"; - hash = "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0="; + url = "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz"; + hash = "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg="; }; } { - name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; + name = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; - hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; + url = "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz"; + hash = "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ="; }; } { - name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"; + name = "zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM"; path = fetchZigArtifact { name = "zg"; - url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc"; - hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="; + url = "git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9"; + hash = "sha256-P0ieLuOQ05wKVaMmeNKJIxCWMIdyeKkmhsj8Ps80BGU="; }; } { - name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ"; + name = "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9"; + path = fetchZigArtifact { + name = "zg"; + url = "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz"; + hash = "sha256-BZhz1nPqxK6hdsJQ66n7Jk4zMgFSGLXm8eU0CX/7mDI="; + }; + } + { + name = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi"; path = fetchZigArtifact { name = "zig_js"; - url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz"; - hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="; + url = "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz"; + hash = "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M="; }; } { - name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk"; + name = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK"; path = fetchZigArtifact { name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz"; - hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="; + url = "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz"; + hash = "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw="; }; } { - name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy"; + name = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe"; path = fetchZigArtifact { name = "zig_wayland"; - url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz"; - hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="; + url = "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz"; + hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="; }; } { - name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; + name = "zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL"; path = fetchZigArtifact { name = "zigimg"; - url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; - hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; + url = "git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726"; + hash = "sha256-Ko5RuxxTAvpUHCnWEdHqNl7b+PVUAxg1/OPmzGGjdt0="; + }; + } + { + name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms"; + path = fetchZigArtifact { + name = "zigimg"; + url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz"; + hash = "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 453a12347..7dda3d294 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,7 +1,7 @@ -git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc -git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d -git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23 -https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz +git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9 +git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726 +https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz +https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz @@ -24,12 +24,15 @@ https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0 https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz -https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst -https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz +https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz +https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz +https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz -https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz -https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz -https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz +https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz +https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz +https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz +https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz +https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz +https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index beea0dc04..5d0ed3108 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -79,9 +79,9 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz", - "dest": "vendor/p/libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q", - "sha256": "29aa3360a121853ffab089de7fbffc3bfeb42c304937ef1099d2ee358d469267" + "url": "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + "dest": "vendor/p/libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", + "sha256": "6003ea6b96e4a518a128f932327d79a11bd30996b13b73baeb29916379487dd7" }, { "type": "archive", @@ -133,15 +133,21 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/190706c6b56f0842d29778007f74f7d3d1335fc5.tar.gz", - "dest": "vendor/p/uucode-0.1.0-ZZjBPpAFQABNCvd9cVPBg4I7233Ays-NWfWphPNqGbyE", - "sha256": "8aaf4eca7b397b94e7cf604a3cf3d3bb2274dc13786cad1db2648f135b34c228" + "url": "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + "dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", + "sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" }, { - "type": "git", - "url": "https://github.com/rockorager/libvaxis", - "commit": "1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", - "dest": "vendor/p/vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn" + "type": "archive", + "url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", + "dest": "vendor/p/vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA", + "sha256": "7aae580b6e8e6348b671d409d195cc67ea36bc740b10534d1b342de59bb3e013" + }, + { + "type": "archive", + "url": "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", + "dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", + "sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2" }, { "type": "archive", @@ -163,45 +169,57 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", - "dest": "vendor/p/z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP", - "sha256": "d036c3292600d5e8e1571fd66ce9304e00f9ecf35115c9d1be2a8187cc693d9d" + "url": "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", + "dest": "vendor/p/z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", + "sha256": "e7fa91640221d54e36bfb8ea97d5b48ebdb3cd066dbb7f43c493cb56b4b26c98" }, { "type": "archive", - "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", - "dest": "vendor/p/zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9", - "sha256": "de7ba535077fe2b678a5a7972585f002588d37244db08397feadf3d4907c0bb2" + "url": "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", + "dest": "vendor/p/zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", + "sha256": "f018a76da9d27d978103c481028a55c7024e6cddfafc14e9c551c004a89cb0c4" }, { "type": "git", - "url": "https://codeberg.org/atman/zg", - "commit": "4a002763419a34d61dcbb1f415821b83b9bf8ddc", - "dest": "vendor/p/zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM" + "url": "https://codeberg.org/ivanstepanovftw/zg", + "commit": "4fe689e56ce2ed5a8f59308b471bccd7da89fac9", + "dest": "vendor/p/zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM" }, { "type": "archive", - "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", - "dest": "vendor/p/N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", - "sha256": "7f235e0956c2f5401a28963a261019953d00e3bf4cfc029830f2161196c3583d" + "url": "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz", + "dest": "vendor/p/zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9", + "sha256": "059873d673eac4aea176c250eba9fb264e3332015218b5e6f1e534097ffb9832" }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", - "dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk", - "sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc" + "url": "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + "dest": "vendor/p/zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", + "sha256": "4c2018e56015d39504b8090386ad9ce9393f38380085d9c32373bf7e56fc73a3" }, { "type": "archive", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", - "dest": "vendor/p/wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy", - "sha256": "13bec6675e403d86db3b55b39ae262f1e1bdfe24056dcd82824341c6308b5219" + "url": "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + "dest": "vendor/p/zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", + "sha256": "dd84af737625356fcd722cb30909f3b2e8d702667cf579714aa7eabc0ac08ecc" + }, + { + "type": "archive", + "url": "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + "dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", + "sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e" }, { "type": "git", - "url": "https://github.com/TUSF/zigimg", - "commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d", - "dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj" + "url": "https://github.com/ivanstepanovftw/zigimg", + "commit": "aa4c31db872612c39edbb79f753b3cd9a79fe726", + "dest": "vendor/p/zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL" + }, + { + "type": "archive", + "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", + "dest": "vendor/p/zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms", + "sha256": "2c1ed76ba2b35514544b0c27c9633ecba7c31be9080e37e7a010c93b5a1bc553" }, { "type": "archive", From a41f59837e959f86708890d1508266dca2dcf090 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 16:32:05 -0700 Subject: [PATCH 141/319] nix: update to unstable for Zig 0.15 in package --- flake.lock | 10 +++++----- flake.nix | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 03413ce88..349248668 100644 --- a/flake.lock +++ b/flake.lock @@ -36,15 +36,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748189127, - "narHash": "sha256-zRDR+EbbeObu4V2X5QCd2Bk5eltfDlCr5yvhBwUT6pY=", - "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", + "lastModified": 315532800, + "narHash": "sha256-YwoXN6fthkakCFD7nXPcUK+rkNr6ZTNTuF8zdGaxZo0=", + "rev": "dc704e6102e76aad573f63b74c742cd96f8f1e6c", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.802491.7c43f080a7f2/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre870318.dc704e6102e7/nixexprs.tar.xz" }, "original": { "type": "tarball", - "url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz" + "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" } }, "nixpkgs_2": { diff --git a/flake.nix b/flake.nix index 2f2d8a2a9..18241a447 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,9 @@ # We want to stay as up to date as possible but need to be careful that the # glibc versions used by our dependencies from Nix are compatible with the # system glibc that the user is building for. - nixpkgs.url = "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz"; + # + # We are currently on unstable to get Zig 0.15 for our package.nix + nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; flake-utils.url = "github:numtide/flake-utils"; # Used for shell.nix From 22caf60263e5048b33c653d428770fdb4469e232 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Oct 2025 16:41:29 -0700 Subject: [PATCH 142/319] update a bunch of required Zig versions to 0.15 --- build.zig.zon | 2 +- example/c-vt/build.zig.zon | 2 +- example/zig-vt/build.zig.zon | 2 +- snap/snapcraft.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b28cd4991..a1885f7f7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,7 +3,7 @@ .version = "1.2.1", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, - .minimum_zig_version = "0.14.1", + .minimum_zig_version = "0.15.1", .dependencies = .{ // Zig libs diff --git a/example/c-vt/build.zig.zon b/example/c-vt/build.zig.zon index 3230f440e..5da1a9168 100644 --- a/example/c-vt/build.zig.zon +++ b/example/c-vt/build.zig.zon @@ -2,7 +2,7 @@ .name = .c_vt, .version = "0.0.0", .fingerprint = 0x413a8529b1255f9a, - .minimum_zig_version = "0.14.1", + .minimum_zig_version = "0.15.1", .dependencies = .{ // Ghostty dependency. In reality, you'd probably use a URL-based // dependency like the one showed (and commented out) below this one. diff --git a/example/zig-vt/build.zig.zon b/example/zig-vt/build.zig.zon index 852e736ca..bc7246de5 100644 --- a/example/zig-vt/build.zig.zon +++ b/example/zig-vt/build.zig.zon @@ -2,7 +2,7 @@ .name = .zig_vt, .version = "0.0.0", .fingerprint = 0x6045575a7a8387e6, - .minimum_zig_version = "0.14.1", + .minimum_zig_version = "0.15.1", .dependencies = .{ // Ghostty dependency. In reality, you'd probably use a URL-based // dependency like the one showed (and commented out) below this one. diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index e48fa93c8..2e434843c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -52,7 +52,7 @@ parts: rm -rf $CRAFT_PART_SRC/* if [[ -n $arch ]]; then - curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.14.0/zig-linux-$arch-0.14.0.tar.xz + curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.15.1/zig-$arch-linux-0.15.1.tar.xz else echo "Unsupported arch" exit 1 From bb98bc744d1ad6242b95e48ec8c787a870f5a8e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 06:55:02 -0700 Subject: [PATCH 143/319] ci: disable freebsd for now --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b038a56d..9893106dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,6 @@ jobs: - build-dist - build-examples - build-flatpak - - build-freebsd - build-libghostty-vt - build-linux - build-linux-libghostty @@ -1138,6 +1137,7 @@ jobs: name: Build on FreeBSD needs: test runs-on: namespace-profile-mitchellh-sm-systemd + if: false # FIXME: FreeBSD does not yet ship with Zig 0.15 strategy: matrix: release: From 569fe9238959943a6d3ff39d54ffec10b31cae0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 07:11:45 -0700 Subject: [PATCH 144/319] fix up merge conflicts --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 686647a67..403a628d2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1009,7 +1009,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { self.command_timer = null; const duration: Duration = .{ .duration = end.since(start) }; - log.debug("command took {}", .{duration}); + log.debug("command took {f}", .{duration}); _ = self.rt_app.performAction( .{ .surface = self }, From 4d9718664327b74e6ad29f413739081bf2f1e4a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 07:17:22 -0700 Subject: [PATCH 145/319] apprt/gtk: fix Zig 0.15 --- src/apprt/gtk/class/surface.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 5ca964fe3..0e0334ecf 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -850,15 +850,17 @@ pub const Surface = extern struct { }; const title = std.mem.span(title_); const body = body: { - const exit_code = value.exit_code orelse break :body std.fmt.allocPrintZ( + const exit_code = value.exit_code orelse break :body std.fmt.allocPrintSentinel( alloc, "Command took {}.", .{value.duration.round(std.time.ns_per_ms)}, + 0, ) catch break :notify; - break :body std.fmt.allocPrintZ( + break :body std.fmt.allocPrintSentinel( alloc, "Command took {} and exited with code {d}.", .{ value.duration.round(std.time.ns_per_ms), exit_code }, + 0, ) catch break :notify; }; defer alloc.free(body); From 1c1a56394d50575f29e724ffc56189b17251f2fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 07:33:37 -0700 Subject: [PATCH 146/319] apprt/gtk: Zig 0.15 whack a mole --- src/apprt/gtk/class/surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 0e0334ecf..cc8359b7e 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -852,13 +852,13 @@ pub const Surface = extern struct { const body = body: { const exit_code = value.exit_code orelse break :body std.fmt.allocPrintSentinel( alloc, - "Command took {}.", + "Command took {f}.", .{value.duration.round(std.time.ns_per_ms)}, 0, ) catch break :notify; break :body std.fmt.allocPrintSentinel( alloc, - "Command took {} and exited with code {d}.", + "Command took {f} and exited with code {d}.", .{ value.duration.round(std.time.ns_per_ms), exit_code }, 0, ) catch break :notify; From f0eb46ea2648512016b45dbcbf544d2d0661a2fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 07:17:22 -0700 Subject: [PATCH 147/319] Add update-mirror Nu script to ease mirror updating for deps This adds a new script we can manually run that downloads all the files that need to be uploaded to the mirror and updates our build.zig.zon. The upload still happens manually [by me] but this simplifies the task greatly. --- build.zig.zon | 20 +++---- build.zig.zon.json | 28 ++++----- build.zig.zon.nix | 34 +++++------ build.zig.zon.txt | 20 +++---- flatpak/zig-packages.json | 30 +++++----- nix/build-support/update-mirror.nu | 96 ++++++++++++++++++++++++++++++ nix/build-support/update-mirror.sh | 30 ---------- 7 files changed, 162 insertions(+), 96 deletions(-) create mode 100755 nix/build-support/update-mirror.nu delete mode 100755 nix/build-support/update-mirror.sh diff --git a/build.zig.zon b/build.zig.zon index a1885f7f7..e76c5e354 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,55 +9,55 @@ .libxev = .{ // mitchellh/libxev - .url = "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + .url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", .hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", .lazy = true, }, .vaxis = .{ // rockorager/libvaxis - .url = "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", + .url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", .hash = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", .lazy = true, }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", + .url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", .hash = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", .lazy = true, }, .zig_objc = .{ // mitchellh/zig-objc - .url = "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + .url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", .hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", .lazy = true, }, .zig_js = .{ // mitchellh/zig-js - .url = "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + .url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", .hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", .lazy = true, }, .uucode = .{ // TODO: currently the use-llvm branch because its broken on self-hosted - .url = "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + .url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", .hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland - .url = "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + .url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", .hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", .lazy = true, }, .zf = .{ // natecraddock/zf - .url = "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", + .url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", .hash = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", .lazy = true, }, .gobject = .{ // https://github.com/jcollie/ghostty-gobject based on zig_gobject // Temporary until we generate them at build time automatically. - .url = "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst", + .url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", .hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", .lazy = true, }, @@ -116,7 +116,7 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + .url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", .hash = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", .lazy = true, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 445f39827..ee374e695 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -26,7 +26,7 @@ }, "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": { "name": "gobject", - "url": "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst", + "url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", "hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=" }, "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": { @@ -51,7 +51,7 @@ }, "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + "url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", "hash": "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { @@ -66,7 +66,7 @@ }, "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs": { "name": "libxev", - "url": "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + "url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", "hash": "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc=" }, "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": { @@ -111,19 +111,19 @@ }, "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", "hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" }, + "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": { + "name": "vaxis", + "url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", + "hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=" + }, "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA": { "name": "vaxis", "url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", "hash": "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM=" }, - "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": { - "name": "vaxis", - "url": "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", - "hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=" - }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "name": "wayland", "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", @@ -141,12 +141,12 @@ }, "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", + "url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", "hash": "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg=" }, "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR": { "name": "zf", - "url": "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", + "url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", "hash": "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ=" }, "zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM": { @@ -161,17 +161,17 @@ }, "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": { "name": "zig_js", - "url": "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + "url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", "hash": "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M=" }, "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK": { "name": "zig_objc", - "url": "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + "url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", "hash": "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw=" }, "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe": { "name": "zig_wayland", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + "url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", "hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=" }, "zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL": { diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 0111f0769..e9d2fc0bc 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -126,7 +126,7 @@ in name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV"; path = fetchZigArtifact { name = "gobject"; - url = "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst"; + url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst"; hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="; }; } @@ -166,7 +166,7 @@ in name = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz"; + url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz"; hash = "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ="; }; } @@ -190,7 +190,7 @@ in name = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs"; path = fetchZigArtifact { name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"; + url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"; hash = "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc="; }; } @@ -262,10 +262,18 @@ in name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz"; + url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz"; hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; }; } + { + name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ"; + path = fetchZigArtifact { + name = "vaxis"; + url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz"; + hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="; + }; + } { name = "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA"; path = fetchZigArtifact { @@ -274,14 +282,6 @@ in hash = "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM="; }; } - { - name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ"; - path = fetchZigArtifact { - name = "vaxis"; - url = "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz"; - hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="; - }; - } { name = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t"; path = fetchZigArtifact { @@ -310,7 +310,7 @@ in name = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz"; + url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz"; hash = "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg="; }; } @@ -318,7 +318,7 @@ in name = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz"; + url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz"; hash = "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ="; }; } @@ -342,7 +342,7 @@ in name = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi"; path = fetchZigArtifact { name = "zig_js"; - url = "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz"; + url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz"; hash = "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M="; }; } @@ -350,7 +350,7 @@ in name = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK"; path = fetchZigArtifact { name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz"; + url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz"; hash = "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw="; }; } @@ -358,7 +358,7 @@ in name = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe"; path = fetchZigArtifact { name = "zig_wayland"; - url = "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz"; + url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz"; hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="; }; } diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 7dda3d294..8ebecee9b 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,7 +1,6 @@ git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9 git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726 https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz -https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz @@ -9,11 +8,14 @@ https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz https://deps.files.ghostty.org/gettext-0.24.tar.gz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz +https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz +https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz +https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz @@ -21,18 +23,16 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz +https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz +https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz +https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz +https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz +https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz +https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz +https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz -https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz -https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz -https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz -https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz -https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz -https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz -https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz -https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 5d0ed3108..58ef3c97a 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -31,7 +31,7 @@ }, { "type": "archive", - "url": "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst", + "url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", "dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", "sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a" }, @@ -61,7 +61,7 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + "url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", "dest": "vendor/p/N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", "sha256": "99d854c4002a2b14515f0103da569a6d4a3d659a996bf31902c3b4f98f4beee4" }, @@ -79,7 +79,7 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/libxev/archive/34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + "url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", "dest": "vendor/p/libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", "sha256": "6003ea6b96e4a518a128f932327d79a11bd30996b13b73baeb29916379487dd7" }, @@ -133,22 +133,22 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", "dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", "sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", + "dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", + "sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2" + }, { "type": "archive", "url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", "dest": "vendor/p/vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA", "sha256": "7aae580b6e8e6348b671d409d195cc67ea36bc740b10534d1b342de59bb3e013" }, - { - "type": "archive", - "url": "https://github.com/rockorager/libvaxis/archive/9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", - "dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", - "sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2" - }, { "type": "archive", "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", @@ -169,13 +169,13 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", + "url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", "dest": "vendor/p/z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", "sha256": "e7fa91640221d54e36bfb8ea97d5b48ebdb3cd066dbb7f43c493cb56b4b26c98" }, { "type": "archive", - "url": "https://github.com/jcollie/zf/archive/52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", + "url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", "dest": "vendor/p/zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", "sha256": "f018a76da9d27d978103c481028a55c7024e6cddfafc14e9c551c004a89cb0c4" }, @@ -193,19 +193,19 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-js/archive/04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + "url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", "dest": "vendor/p/zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", "sha256": "4c2018e56015d39504b8090386ad9ce9393f38380085d9c32373bf7e56fc73a3" }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-objc/archive/f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + "url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", "dest": "vendor/p/zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", "sha256": "dd84af737625356fcd722cb30909f3b2e8d702667cf579714aa7eabc0ac08ecc" }, { "type": "archive", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + "url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", "dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", "sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e" }, diff --git a/nix/build-support/update-mirror.nu b/nix/build-support/update-mirror.nu new file mode 100755 index 000000000..8571ddea6 --- /dev/null +++ b/nix/build-support/update-mirror.nu @@ -0,0 +1,96 @@ +#!/usr/bin/env nu + +# This script downloads external dependencies from build.zig.zon.json that +# are not already mirrored at deps.files.ghostty.org, saves them to a local +# directory, and updates build.zig.zon to point to the new mirror URLs. +# +# The downloaded files are unmodified so their checksums and content hashes +# will match the originals. +# +# After running this script, the files in the output directory can be uploaded +# to blob storage, and build.zig.zon will already be updated with the new URLs. +def main [ + --output: string = "tmp-mirror", # Output directory for the mirrored files + --prefix: string = "https://deps.files.ghostty.org/", # Final URL prefix to ignore + --dry-run, # Print what would be downloaded without downloading +] { + let script_dir = ($env.CURRENT_FILE | path dirname) + let input_file = ($script_dir | path join ".." ".." "build.zig.zon.json") + let zon_file = ($script_dir | path join ".." ".." "build.zig.zon") + let output_dir = $output + + # Ensure the output directory exists + mkdir $output_dir + + # Read and parse the JSON file + let deps = open $input_file + + # Track URL replacements for build.zig.zon + mut url_replacements = [] + + # Process each dependency + for entry in ($deps | transpose key value) { + let key = $entry.key + let name = $entry.value.name + let url = $entry.value.url + + # Skip URLs that don't start with http(s) + if not ($url | str starts-with "http") { + continue + } + + # Skip URLs already hosted at the prefix + if ($url | str starts-with $prefix) { + continue + } + + # Extract the file extension from the URL + let extension = ($url | parse -r '(\.[a-z0-9]+(?:\.[a-z0-9]+)?)$' | get -o capture0.0 | default "") + + # Try to extract commit hash (40 hex chars) from URL + let commit_hash = ($url | parse -r '([a-f0-9]{40})' | get -o capture0.0 | default "") + + # Try to extract date pattern (YYYY-MM-DD or YYYYMMDD with optional suffixes) + let date_pattern = ($url | parse -r '((?:release-)?20\d{2}(?:-?\d{2}){2}(?:[-]\d+)*(?:[-][a-z0-9]+)?)' | get -o capture0.0 | default "") + + # Build filename based on what we found + let filename = if (not ($commit_hash | is-empty)) { + $"($name)-($commit_hash)($extension)" + } else if (not ($date_pattern | is-empty)) { + $"($name)-($date_pattern)($extension)" + } else { + $"($key)($extension)" + } + let new_url = $"($prefix)($filename)" + print $"($url) -> ($filename)" + + # Track the replacement + $url_replacements = ($url_replacements | append {old: $url, new: $new_url}) + + # Download the file + if not $dry_run { + http get $url | save -f ($output_dir | path join $filename) + } + } + + if $dry_run { + print "Dry run complete - no files were downloaded\n" + print $"Would update ($url_replacements | length) URLs in build.zig.zon" + } else { + print "All dependencies downloaded successfully\n" + print $"Updating ($zon_file)..." + + # Backup the old file + let backup_file = $"($zon_file).bak" + cp $zon_file $backup_file + print $"Backed up to ($backup_file)" + + mut zon_content = (open $zon_file) + for replacement in $url_replacements { + $zon_content = ($zon_content | str replace $replacement.old $replacement.new) + } + $zon_content | save -f $zon_file + + print $"Updated ($url_replacements | length) URLs in build.zig.zon" + } +} diff --git a/nix/build-support/update-mirror.sh b/nix/build-support/update-mirror.sh deleted file mode 100755 index f346572ed..000000000 --- a/nix/build-support/update-mirror.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# -# This script generates a directory that can be uploaded to blob -# storage to mirror our dependencies. The dependencies are unmodified -# so their checksum and content hashes will match. - -set -e # Exit immediately if a command exits with a non-zero status - -SCRIPT_PATH="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" -INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock" -OUTPUT_DIR="blob" - -# Ensure the output directory exists -mkdir -p "$OUTPUT_DIR" - -# Use jq to iterate over the JSON and download files -jq -r 'to_entries[] | "\(.key) \(.value.name) \(.value.url)"' "$INPUT_FILE" | while read -r key name url; do - # Skip URLs that don't start with http(s). They aren't necessary for - # our mirror. - if ! echo "$url" | grep -Eq "^https?://"; then - continue - fi - - # Extract the file extension from the URL - extension=$(echo "$url" | grep -oE '\.[a-z0-9]+(\.[a-z0-9]+)?$') - - filename="${name}-${key}${extension}" - echo "$url -> $filename" - curl -L -o "$OUTPUT_DIR/$filename" "$url" -done From a667b740eefb03962b69f238a10d539791120112 Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Fri, 3 Oct 2025 18:47:47 +0200 Subject: [PATCH 148/319] Fix typos --- macos/Sources/Features/Splits/TerminalSplitTreeView.swift | 2 +- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 2 +- src/build/SharedDeps.zig | 2 +- src/renderer/generic.zig | 2 +- src/stb/stb_image.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index f19640707..6b8171ff5 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -12,7 +12,7 @@ struct TerminalSplitTreeView: View { onResize: onResize) // This is necessary because we can't rely on SwiftUI's implicit // structural identity to detect changes to this view. Due to - // the tree structure of splits it could result in bad beaviors. + // the tree structure of splits it could result in bad behaviors. // See: https://github.com/ghostty-org/ghostty/issues/7546 .id(node.structuralIdentity) } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 22784d164..2b3fd261c 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1027,7 +1027,7 @@ extension Ghostty { // If we are in a keyDown then we don't need to redispatch a command-modded // key event (see docs for this field) so reset this to nil because - // `interpretKeyEvents` may dispach it. + // `interpretKeyEvents` may dispatch it. self.lastPerformKeyEvent = nil self.interpretKeyEvents([translationEvent]) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 785830ab9..dfa676bba 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -692,7 +692,7 @@ fn addGtkNg( } } -/// Add only the dependencies required for `Config.simd` enbled. This also +/// Add only the dependencies required for `Config.simd` enabled. This also /// adds all the simd source files for compilation. pub fn addSimd( b: *std.Build, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 802c769a6..d66a32286 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -184,7 +184,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Background image, if we have one. bg_image: ?imagepkg.Image = null, - /// Set whenever the background image changes, singalling + /// Set whenever the background image changes, signalling /// that the new background image needs to be uploaded to /// the GPU. /// diff --git a/src/stb/stb_image.h b/src/stb/stb_image.h index 3ae1815c1..ed7791dff 100644 --- a/src/stb/stb_image.h +++ b/src/stb/stb_image.h @@ -6831,7 +6831,7 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i // 0: not specified. } - // background is what out is after the undoing of the previou frame; + // background is what out is after the undoing of the previous frame; memcpy( g->background, g->out, 4 * g->w * g->h ); } From 96fbff681bf2e22440a8542daa4b3e96c14cf600 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 1 Sep 2025 18:28:51 -0700 Subject: [PATCH 149/319] Center before quantizing bitmap glyphs --- src/font/face/coretext.zig | 20 ++++++++++---------- src/font/face/freetype.zig | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 8c9611c04..a44fb7043 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -378,16 +378,6 @@ pub const Face = struct { var width = glyph_size.width; var height = glyph_size.height; - // If this is a bitmap glyph, it will always render as full pixels, - // not fractional pixels, so we need to quantize its position and - // size accordingly to align to full pixels so we get good results. - if (sbix) { - width = cell_width - @round(cell_width - width - x) - @round(x); - height = cell_height - @round(cell_height - height - y) - @round(y); - x = @round(x); - y = @round(y); - } - // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. @@ -400,6 +390,16 @@ pub const Face = struct { x += (cell_width - metrics.face_width) / 2; } + // If this is a bitmap glyph, it will always render as full pixels, + // not fractional pixels, so we need to quantize its position and + // size accordingly to align to full pixels so we get good results. + if (sbix) { + width = cell_width - @round(cell_width - width - x) - @round(x); + height = cell_height - @round(cell_height - height - y) - @round(y); + x = @round(x); + y = @round(y); + } + // Our whole-pixel bearings for the final glyph. // The fractional portion will be included in the rasterized position. const px_x: i32 = @intFromFloat(@floor(x)); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index bdcd82ab3..8be7647e5 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -492,16 +492,6 @@ pub const Face = struct { var x = glyph_size.x; var y = glyph_size.y; - // If this is a bitmap glyph, it will always render as full pixels, - // not fractional pixels, so we need to quantize its position and - // size accordingly to align to full pixels so we get good results. - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) { - width = cell_width - @round(cell_width - width - x) - @round(x); - height = cell_height - @round(cell_height - height - y) - @round(y); - x = @round(x); - y = @round(y); - } - // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. @@ -520,6 +510,16 @@ pub const Face = struct { x += @round((cell_width - metrics.face_width) / 2); } + // If this is a bitmap glyph, it will always render as full pixels, + // not fractional pixels, so we need to quantize its position and + // size accordingly to align to full pixels so we get good results. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) { + width = cell_width - @round(cell_width - width - x) - @round(x); + height = cell_height - @round(cell_height - height - y) - @round(y); + x = @round(x); + y = @round(y); + } + // Now we can render the glyph. var bitmap: freetype.c.FT_Bitmap = undefined; _ = freetype.c.FT_Bitmap_Init(&bitmap); From 5c129205a55d802341c536cd940042ea9cb75edb Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 2 Oct 2025 18:17:42 -0700 Subject: [PATCH 150/319] Use correct and consistent pre-constraint glyph rect In Freetype, measure rect after emboldening, so constraints apply to the true glyph size like in CoreText. In CoreText, don't let font smoothing affect the rect (only the canvas). --- src/font/face/coretext.zig | 31 +++++++++++++------------------ src/font/face/freetype.zig | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index a44fb7043..52eb1d668 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -319,17 +319,6 @@ pub const Face = struct { rect.origin.y -= line_width / 2; }; - // We make an assumption that font smoothing ("thicken") - // adds no more than 1 extra pixel to any edge. We don't - // add extra size if it's a sbix color font though, since - // bitmaps aren't affected by smoothing. - if (opts.thicken and !sbix) { - rect.size.width += 2.0; - rect.size.height += 2.0; - rect.origin.x -= 1.0; - rect.origin.y -= 1.0; - } - // If our rect is smaller than a quarter pixel in either axis // then it has no outlines or they're too small to render. // @@ -400,10 +389,16 @@ pub const Face = struct { y = @round(y); } + // We make an assumption that font smoothing ("thicken") + // adds no more than 1 extra pixel to any edge. We don't + // add extra size if it's a sbix color font though, since + // bitmaps aren't affected by smoothing. + const canvas_padding: u32 = if (opts.thicken and !sbix) 1 else 0; + // Our whole-pixel bearings for the final glyph. // The fractional portion will be included in the rasterized position. - const px_x: i32 = @intFromFloat(@floor(x)); - const px_y: i32 = @intFromFloat(@floor(y)); + const px_x = @as(i32, @intFromFloat(@floor(x))) - @as(i32, @intCast(canvas_padding)); + const px_y = @as(i32, @intFromFloat(@floor(y))) - @as(i32, @intCast(canvas_padding)); // We keep track of the fractional part of the pixel bearings, which // we will add as an offset when rasterizing to make sure we get the @@ -413,9 +408,9 @@ pub const Face = struct { // Add the fractional pixel to the width and height and take // the ceiling to get a canvas size that will definitely fit - // our drawn glyph, including the fractional offset. - const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); - const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); + // our drawn glyph, including the fractional offset and font smoothing. + const px_width = @as(u32, @intFromFloat(@ceil(width + frac_x))) + (2 * canvas_padding); + const px_height = @as(u32, @intFromFloat(@ceil(height + frac_y))) + (2 * canvas_padding); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -526,8 +521,8 @@ pub const Face = struct { // `drawGlyphs`, we pass the negated bearings. context.translateCTM( ctx, - frac_x, - frac_y, + frac_x + @as(f64, @floatFromInt(canvas_padding)), + frac_y + @as(f64, @floatFromInt(canvas_padding)), ); // Scale the drawing context so that when we draw diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 8be7647e5..55fac7a9d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -429,6 +429,17 @@ pub const Face = struct { try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything())); const glyph = self.face.handle.*.glyph; + // For synthetic bold, we embolden the glyph. + if (self.synthetic.bold) { + // We need to scale the embolden amount based on the font size. + // This is a heuristic I found worked well across a variety of + // founts: 1 pixel per 64 units of height. + const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); + const ratio: f64 = 64.0 / 2048.0; + const amount = @ceil(font_height * ratio); + _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); + } + // We get a rect that represents the position // and size of the glyph before any changes. const rect = getGlyphSize(glyph); @@ -447,17 +458,6 @@ pub const Face = struct { .atlas_y = 0, }; - // For synthetic bold, we embolden the glyph. - if (self.synthetic.bold) { - // We need to scale the embolden amount based on the font size. - // This is a heuristic I found worked well across a variety of - // founts: 1 pixel per 64 units of height. - const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); - const ratio: f64 = 64.0 / 2048.0; - const amount = @ceil(font_height * ratio); - _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); - } - const metrics = opts.grid_metrics; const cell_width: f64 = @floatFromInt(metrics.cell_width); const cell_height: f64 = @floatFromInt(metrics.cell_height); From f245574087dfc4a8f261d28dc1025b8aad40127a Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 2 Oct 2025 18:38:39 -0700 Subject: [PATCH 151/319] Fix comment --- 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 55fac7a9d..0d2ddc366 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -441,7 +441,7 @@ pub const Face = struct { } // We get a rect that represents the position - // and size of the glyph before any changes. + // and size of the glyph before constraints. const rect = getGlyphSize(glyph); // If our glyph is smaller than a quarter pixel in either axis From 93c634c86659dcda0ef0788ef7520979b47b6cf7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 13:27:24 -0700 Subject: [PATCH 152/319] flush output for our builddata executable Fixes #9018 We were truncated our terminfo causing tmux to not respect some features. --- src/Surface.zig | 2 ++ src/main_build_data.zig | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 403a628d2..018c4206b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3998,6 +3998,8 @@ pub fn cursorPosCallback( crash.sentry.thread_state = self.crashThreadState(); defer crash.sentry.thread_state = null; + // log.debug("cursor pos x={} y={} mods={?}", .{ pos.x, pos.y, mods }); + // If the position is negative, it is outside our viewport and // we need to clear any hover states. if (pos.x < 0 or pos.y < 0) { diff --git a/src/main_build_data.zig b/src/main_build_data.zig index 9fffdd0d6..9dd1da395 100644 --- a/src/main_build_data.zig +++ b/src/main_build_data.zig @@ -47,4 +47,5 @@ pub fn main() !void { .@"vim-compiler" => try writer.writeAll(@import("extra/vim.zig").compiler), .terminfo => try @import("terminfo/ghostty.zig").ghostty.encode(writer), } + try stdout_writer.end(); } From 77114d79270de31d84987bd9861f862280e6108f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 13:35:36 -0700 Subject: [PATCH 153/319] macos: avoid any zero-sized content size increments Fixes #9016 --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index a34be4125..f660ea3ad 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -743,6 +743,10 @@ class BaseTerminalController: NSWindowController, func cellSizeDidChange(to: NSSize) { guard derivedConfig.windowStepResize else { return } + // Stage manager can sometimes present windows in such a way that the + // cell size is temporarily zero due to the window being tiny. We can't + // set content resize increments to this value, so avoid an assertion failure. + guard to.width > 0 && to.height > 0 else { return } self.window?.contentResizeIncrements = to } From 7acf617763cc87e04daca7d0635cd2094539a195 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Wed, 1 Oct 2025 13:32:46 -0700 Subject: [PATCH 154/319] fix(font): Anchor scaling at bounding box center --- src/font/face.zig | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index 7216fea97..0b7bfbdbd 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -265,15 +265,15 @@ pub const RenderOptions = struct { }; }; - // The new, constrained glyph size - var constrained_glyph = glyph; - - // Apply prescribed scaling + // Apply prescribed scaling, preserving the + // center bearings of the group bounding box const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width); - constrained_glyph.width *= width_factor; - constrained_glyph.x *= width_factor; - constrained_glyph.height *= height_factor; - constrained_glyph.y *= height_factor; + const center_x = group.x + (group.width / 2); + const center_y = group.y + (group.height / 2); + group.width *= width_factor; + group.height *= height_factor; + group.x = center_x - (group.width / 2); + group.y = center_y - (group.height / 2); // NOTE: font_patcher jumps through a lot of hoops at this // point to ensure that the glyph remains within the target @@ -283,25 +283,20 @@ pub const RenderOptions = struct { // Align vertically if (self.align_vertical != .none) { - // Vertically scale group bounding box. - group.height *= height_factor; - group.y *= height_factor; - - // Calculate offset and shift the glyph - constrained_glyph.y += self.offset_vertical(group, metrics); + group.y += self.offset_vertical(group, metrics); } - // Align horizontally if (self.align_horizontal != .none) { - // Horizontally scale group bounding box. - group.width *= width_factor; - group.x *= width_factor; - - // Calculate offset and shift the glyph - constrained_glyph.x += self.offset_horizontal(group, metrics, min_constraint_width); + group.x += self.offset_horizontal(group, metrics, min_constraint_width); } - return constrained_glyph; + // Transfer the scaling and alignment back to the glyph and return. + return .{ + .width = width_factor * glyph.width, + .height = height_factor * glyph.height, + .x = group.x + (group.width * self.relative_x), + .y = group.y + (group.height * self.relative_y), + }; } /// Return width and height scaling factors for this scaling group. From 32f8c71be3f827cc091671a5a97eb81a61b51de8 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Wed, 1 Oct 2025 15:21:41 -0700 Subject: [PATCH 155/319] Always clamp scaled glyph to cell Also take padding into account for centered alignment, necessary since our constraint type allows asymmetric padding. --- src/font/face.zig | 104 ++++++++++++++++++++++--------------- src/font/face/coretext.zig | 6 +-- src/font/face/freetype.zig | 6 +-- 3 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index 0b7bfbdbd..88801fb17 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -281,14 +281,9 @@ pub const RenderOptions = struct { // This is irrelevant here as we're not rounding, we're // staying in f64 and heading straight to rendering. - // Align vertically - if (self.align_vertical != .none) { - group.y += self.offset_vertical(group, metrics); - } - // Align horizontally - if (self.align_horizontal != .none) { - group.x += self.offset_horizontal(group, metrics, min_constraint_width); - } + // Apply prescribed alignment + group.y = self.aligned_y(group, metrics); + group.x = self.aligned_x(group, metrics, min_constraint_width); // Transfer the scaling and alignment back to the glyph and return. return .{ @@ -376,63 +371,88 @@ pub const RenderOptions = struct { return .{ width_factor, height_factor }; } - /// Return vertical offset needed to align this group - fn offset_vertical( + /// Return vertical bearing for aligning this group + fn aligned_y( self: Constraint, group: GlyphSize, metrics: Metrics, ) f64 { + if ((self.size == .none) and (self.align_vertical == .none)) { + // If we don't have any constraints affecting the vertical axis, + // we don't touch vertical alignment. + return group.y; + } // We use face_height and offset by face_y, rather than // using cell_height directly, to account for the asymmetry // of the pixel cell around the face (a consequence of // aligning the baseline with a pixel boundary rather than // vertically centering the face). - const new_group_y = metrics.face_y + switch (self.align_vertical) { - .none => return 0.0, - .start => self.pad_bottom * metrics.face_height, - .end => end: { - const pad_top_dy = self.pad_top * metrics.face_height; - break :end metrics.face_height - pad_top_dy - group.height; - }, - .center, .center1 => (metrics.face_height - group.height) / 2, + const pad_bottom_dy = self.pad_bottom * metrics.face_height; + const pad_top_dy = self.pad_top * metrics.face_height; + const start_y = metrics.face_y + pad_bottom_dy; + const end_y = metrics.face_y + (metrics.face_height - group.height - pad_top_dy); + const center_y = (start_y + end_y) / 2; + return switch (self.align_vertical) { + // NOTE: Even if there is no prescribed alignment, we ensure + // that the group doesn't protrude outside the padded cell, + // since this is implied by every available size constraint. If + // the group is too high we fall back to centering, though if we + // hit the .none prong we always have self.size != .none, so + // this should never happen. + .none => if (end_y < start_y) + center_y + else + @max(start_y, @min(group.y, end_y)), + .start => start_y, + .end => end_y, + .center, .center1 => center_y, }; - return new_group_y - group.y; } - /// Return horizontal offset needed to align this group - fn offset_horizontal( + /// Return horizontal bearing for aligning this group + fn aligned_x( self: Constraint, group: GlyphSize, metrics: Metrics, min_constraint_width: u2, ) f64 { + if ((self.size == .none) and (self.align_horizontal == .none)) { + // If we don't have any constraints affecting the horizontal + // axis, we don't touch horizontal alignment. + return group.x; + } // For multi-cell constraints, we align relative to the span - // from the left edge of the first face cell to the right - // edge of the last face cell as they sit within the rounded - // and adjusted pixel cell (centered if narrower than the - // pixel cell, left-aligned if wider). - const face_x, const full_face_span = facecalcs: { - const cell_width: f64 = @floatFromInt(metrics.cell_width); - const full_width: f64 = @floatFromInt(min_constraint_width * metrics.cell_width); - const cell_margin = cell_width - metrics.face_width; - break :facecalcs .{ @max(0, cell_margin / 2), full_width - cell_margin }; - }; - const pad_left_x = self.pad_left * metrics.face_width; - const new_group_x = face_x + switch (self.align_horizontal) { - .none => return 0.0, - .start => pad_left_x, - .end => end: { - const pad_right_dx = self.pad_right * metrics.face_width; - break :end @max(pad_left_x, full_face_span - pad_right_dx - group.width); - }, - .center => @max(pad_left_x, (full_face_span - group.width) / 2), + // from the left edge of the first cell to the right edge of + // the last face cell assuming it's left-aligned within the + // rounded and adjusted pixel cell. Any horizontal offset to + // center the face within the grid cell is the responsibility + // of the backend-specific rendering code, and should be done + // after applying constraints. + const full_face_span = metrics.face_width + @as(f64, @floatFromInt((min_constraint_width - 1) * metrics.cell_width)); + const pad_left_dx = self.pad_left * metrics.face_width; + const pad_right_dx = self.pad_right * metrics.face_width; + const start_x = pad_left_dx; + const end_x = full_face_span - group.width - pad_right_dx; + return switch (self.align_horizontal) { + // NOTE: Even if there is no prescribed alignment, we ensure + // that the glyph doesn't protrude outside the padded cell, + // since this is implied by every available size constraint. The + // left-side bound has priority if the group is too wide, though + // if we hit the .none prong we always have self.size != .none, + // so this should never happen. + .none => @max(start_x, @min(group.x, end_x)), + .start => start_x, + .end => @max(start_x, end_x), + .center => @max(start_x, (start_x + end_x) / 2), // NOTE: .center1 implements the font_patcher rule of centering // in the first cell even for multi-cell constraints. Since glyphs // are not allowed to protrude to the left, this results in the // left-alignment like .start when the glyph is wider than a cell. - .center1 => @max(pad_left_x, (metrics.face_width - group.width) / 2), + .center1 => center1: { + const end1_x = metrics.face_width - group.width - pad_right_dx; + break :center1 @max(start_x, (start_x + end1_x) / 2); + }, }; - return new_group_x - group.x; } }; }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 52eb1d668..77ad1811d 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -370,11 +370,7 @@ pub const Face = struct { // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. - // - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) { + if (metrics.face_width < cell_width) { // We add half the difference to re-center. x += (cell_width - metrics.face_width) / 2; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 0d2ddc366..4112f91b4 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -495,11 +495,7 @@ pub const Face = struct { // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. - // - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) { + if (metrics.face_width < cell_width) { // We add half the difference to re-center. // // NOTE: We round this to a whole-pixel amount because under From 50bdd3bac65f937e002d4e281f1406747073abf7 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 30 Sep 2025 21:15:13 -0700 Subject: [PATCH 156/319] Align stretched glyphs to cell, not face --- src/font/face.zig | 33 +++++++++++++++++++++++++++++++++ src/font/face/coretext.zig | 21 +++++++++++++-------- src/font/face/freetype.zig | 15 ++++++--------- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index 88801fb17..f660565fe 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -244,6 +244,39 @@ pub const RenderOptions = struct { ) GlyphSize { if (!self.doesAnything()) return glyph; + switch (self.size) { + .stretch => { + // Stretched glyphs are usually meant to align across cell + // boundaries, which works best if they're scaled and + // aligned to the grid rather than the face. This is most + // easily done by inserting this little fib in the metrics. + var m = metrics; + m.face_width = @floatFromInt(m.cell_width); + m.face_height = @floatFromInt(m.cell_height); + m.face_y = 0.0; + + // Negative padding for stretched glyphs is a band-aid to + // avoid gaps due to pixel rounding, but at the cost of + // unsightly overlap artifacts. Since we scale and align to + // the grid rather than the face, we don't need it. + var c = self; + c.pad_bottom = @max(0, c.pad_bottom); + c.pad_top = @max(0, c.pad_top); + c.pad_left = @max(0, c.pad_left); + c.pad_right = @max(0, c.pad_right); + + return c.constrainInner(glyph, m, constraint_width); + }, + else => return self.constrainInner(glyph, metrics, constraint_width), + } + } + + fn constrainInner( + self: Constraint, + glyph: GlyphSize, + metrics: Metrics, + constraint_width: u2, + ) GlyphSize { // For extra wide font faces, never stretch glyphs across two cells. // This mirrors font_patcher. const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height)) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 77ad1811d..bd1716a61 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -338,14 +338,7 @@ pub const Face = struct { const cell_height: f64 = @floatFromInt(metrics.cell_height); // Next we apply any constraints to get the final size of the glyph. - var constraint = opts.constraint; - - // We eliminate any negative vertical padding since these overlap - // values aren't needed with how precisely we apply constraints, - // and they can lead to extra height that looks bad for things like - // powerline glyphs. - constraint.pad_top = @max(0.0, constraint.pad_top); - constraint.pad_bottom = @max(0.0, constraint.pad_bottom); + const constraint = opts.constraint; // We need to add the baseline position before passing to the constrain // function since it operates on cell-relative positions, not baseline. @@ -385,6 +378,18 @@ pub const Face = struct { y = @round(y); } + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { + // We add half the difference to re-center. + x += (cell_width - metrics.face_width) / 2; + } + // We make an assumption that font smoothing ("thicken") // adds no more than 1 extra pixel to any edge. We don't // add extra size if it's a sbix color font though, since diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 4112f91b4..259e91b8c 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -463,14 +463,7 @@ pub const Face = struct { const cell_height: f64 = @floatFromInt(metrics.cell_height); // Next we apply any constraints to get the final size of the glyph. - var constraint = opts.constraint; - - // We eliminate any negative vertical padding since these overlap - // values aren't needed with how precisely we apply constraints, - // and they can lead to extra height that looks bad for things like - // powerline glyphs. - constraint.pad_top = @max(0.0, constraint.pad_top); - constraint.pad_bottom = @max(0.0, constraint.pad_bottom); + const constraint = opts.constraint; // We need to add the baseline position before passing to the constrain // function since it operates on cell-relative positions, not baseline. @@ -495,7 +488,11 @@ pub const Face = struct { // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. - if (metrics.face_width < cell_width) { + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { // We add half the difference to re-center. // // NOTE: We round this to a whole-pixel amount because under From a1b7ea2e712b225f74afdb889baf15f50805c5b2 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 6 Sep 2025 16:57:42 -0700 Subject: [PATCH 157/319] Add font_patcher's grouped vs individual alignment --- src/font/nerd_font_attributes.zig | 135 +++--------------------------- src/font/nerd_font_codegen.py | 32 +++++-- 2 files changed, 37 insertions(+), 130 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 04088b1aa..6546fe10b 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -365,9 +365,9 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xebd7...0xec06, 0xec08...0xec0a, 0xec0d...0xec1e, - 0xed00...0xf018, - 0xf01a...0xf02f, + 0xed00...0xf02f, 0xf031...0xf03c, + 0xf03f, 0xf041...0xf043, 0xf045...0xf049, 0xf04b...0xf050, @@ -378,18 +378,16 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf07c...0xf080, 0xf082...0xf08b, 0xf08d...0xf091, - 0xf093...0xf09e, - 0xf0a0, + 0xf093...0xf0a0, 0xf0a5...0xf0a9, 0xf0ab...0xf0c9, 0xf0cb...0xf0d5, 0xf0d7...0xf0dd, 0xf0df...0xf0e6, 0xf0e8...0xf295, - 0xf297...0xf2c3, + 0xf297...0xf2c4, 0xf2c6...0xf2ef, - 0xf2f1...0xf305, - 0xf307...0xf847, + 0xf2f1...0xf847, 0xf0001...0xf1af0, => .{ .size = .fit_cover1, @@ -670,22 +668,15 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_width = 0.8008342022940563, .relative_x = 0.1991657977059437, }, - 0xf019, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.8885754583921015, - }, 0xf030, 0xf03e, + 0xf071, + 0xf08c, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.8885754583921015, .relative_height = 0.8751322751322751, .relative_y = 0.0624338624338624, }, @@ -698,24 +689,13 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7502645502645503, .relative_y = 0.1248677248677249, }, - 0xf03f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.5554597570408116, - .relative_x = 0.0005406676069582, - }, 0xf040, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.8877888683953564, .relative_height = 0.9992749363119733, - .relative_x = 0.0003164442515641, .relative_y = 0.0001959631589261, }, 0xf044, @@ -724,37 +704,27 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9913442331878375, .relative_height = 0.9923123057630445, .relative_y = 0.0002010014265405, }, 0xf04a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.8885754583921015, - .relative_height = 0.7506817256817256, - .relative_y = 0.1247354497354497, - }, 0xf051, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.5557122708039492, .relative_height = 0.7506817256817256, .relative_y = 0.1247354497354497, }, 0xf052, + 0xf081, + 0xf092, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.8741409740917385, .relative_height = 0.8748851565736010, .relative_y = 0.0626172338785870, }, @@ -764,39 +734,26 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.4993711622401420, .relative_height = 0.8759430588185509, .relative_y = 0.0620882827561120, }, 0xf05a...0xf05b, + 0xf0a2, 0xf0aa, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9987423244802840, .relative_height = 0.9997176214776941, .relative_y = 0.0002010014265405, }, - 0xf071, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.8885754583921015, - .relative_height = 0.8751322751322751, - .relative_x = 0.0004701457451810, - .relative_y = 0.0624338624338624, - }, 0xf078, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.8745600777856455, .relative_height = 0.4993298596163721, .relative_y = 0.1879786499051550, }, @@ -806,70 +763,25 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.8885754583921015, .relative_height = 0.8139763779527559, .relative_y = 0.0930118110236220, }, - 0xf081, - 0xf092, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.8740316426933279, - .relative_height = 0.8748851565736010, - .relative_y = 0.0626172338785870, - }, - 0xf08c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.7776210625293841, - .relative_height = 0.8751322751322751, - .relative_y = 0.0624338624338624, - }, - 0xf09f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.5717654171704958, - .relative_x = 0.0006952841596131, - }, 0xf0a1, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.8885754583921015, .relative_height = 0.9303101594008066, .relative_y = 0.0349409448818898, }, - 0xf0a2, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.8749266777006549, - .relative_height = 0.9997176214776941, - .relative_x = 0.0001253913778381, - .relative_y = 0.0002010014265405, - }, 0xf0a3, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9994081526966043, .relative_height = 0.9998551487695376, - .relative_x = 0.0005918473033957, }, 0xf0a4, => .{ @@ -877,7 +789,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9987423244802840, .relative_height = 0.7500526916695081, .relative_y = 0.1250334663306335, }, @@ -887,7 +798,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9675646540335450, .relative_height = 0.8124689241215546, .relative_y = 0.0938253501046103, }, @@ -906,9 +816,7 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.7150686682199350, .relative_height = 0.3756613756613756, - .relative_x = 0.0004030632809351, .relative_y = 0.5708994708994709, }, 0xf0e7, @@ -917,9 +825,7 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.7491243338952770, .relative_height = 0.9998803756692248, - .relative_x = 0.0006021702214782, .relative_y = 0.0001196243307751, }, 0xf296, @@ -928,28 +834,16 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9994800427141276, .relative_height = 0.9627792014248586, - .relative_x = 0.0001795653226322, .relative_y = 0.0187142907131644, }, - 0xf2c4, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.7523272214386461, - }, 0xf2c5, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9883117728988424, .relative_height = 0.8573155985489722, - .relative_x = 0.0004377219006858, .relative_y = 0.0713422007255139, }, 0xf2f0, @@ -958,18 +852,9 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9987423244802840, .relative_height = 0.9669226518842459, .relative_y = 0.0165984862232646, }, - 0xf306, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.7691584391161260, - }, else => null, }; } diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 4965dabe4..82cd159b6 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -282,17 +282,23 @@ def generate_zig_switch_arms( entries |= {k: v for k, v in attributes.items() if isinstance(k, int)} - if entry["ScaleRules"] is not None and "ScaleGroups" in entry["ScaleRules"]: + if entry["ScaleRules"] is not None: + if "ScaleGroups" not in entry["ScaleRules"]: + raise ValueError( + f"Scale rule format {entry['ScaleRules']} not implemented." + ) for group in entry["ScaleRules"]["ScaleGroups"]: xMin = math.inf yMin = math.inf xMax = -math.inf yMax = -math.inf individual_bounds: dict[int, tuple[int, int, int, int]] = {} + individual_advances: set[float] = set() for cp in group: if cp not in cmap: continue glyph = glyphs[cmap[cp]] + individual_advances.add(glyph.width) bounds = BoundsPen(glyphSet=glyphs) glyph.draw(bounds) individual_bounds[cp] = bounds.bounds @@ -302,16 +308,32 @@ def generate_zig_switch_arms( yMax = max(bounds.bounds[3], yMax) group_width = xMax - xMin group_height = yMax - yMin + group_is_monospace = (len(individual_bounds) > 1) and ( + len(individual_advances) == 1 + ) for cp in group: - if cp not in cmap or cp not in entries: + if ( + cp not in cmap + or cp not in entries + # Codepoints may contribute to the bounding box of multiple groups, + # but should be scaled according to the first group they are found + # in. Hence, to avoid overwriting, we need to skip codepoints that + # have already been assigned a scale group. + or "relative_height" in entries[cp] + ): continue this_bounds = individual_bounds[cp] - this_width = this_bounds[2] - this_bounds[0] this_height = this_bounds[3] - this_bounds[1] - entries[cp]["relative_width"] = this_width / group_width entries[cp]["relative_height"] = this_height / group_height - entries[cp]["relative_x"] = (this_bounds[0] - xMin) / group_width entries[cp]["relative_y"] = (this_bounds[1] - yMin) / group_height + # Horizontal alignment should only be grouped if the group is monospace, + # that is, if all glyphs in the group have the same advance width. + if group_is_monospace: + this_width = this_bounds[2] - this_bounds[0] + entries[cp]["relative_width"] = this_width / group_width + entries[cp]["relative_x"] = ( + this_bounds[0] - xMin + ) / group_width del entries[0] From d07237fff5f2ad7e5526f7b6c91a299ef83554ab Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 22 Sep 2025 00:16:49 -0700 Subject: [PATCH 158/319] Skip patchsets and codepoints not in SymbolsNF --- src/font/nerd_font_attributes.zig | 209 ++++++++++++++++++++++++++---- src/font/nerd_font_codegen.py | 43 ++++-- 2 files changed, 222 insertions(+), 30 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 6546fe10b..638c9aa6c 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -9,17 +9,6 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; /// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { - 0x2500...0x259f, - => .{ - .size = .stretch, - .max_constraint_width = 1, - .align_horizontal = .center1, - .align_vertical = .center1, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, - }, 0x2630, => .{ .size = .cover, @@ -291,8 +280,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, }, 0xe0cf, - 0xe0d3, - 0xe0d5, => .{ .size = .fit_cover1, .align_horizontal = .center1, @@ -350,22 +337,31 @@ pub fn getConstraint(cp: u21) ?Constraint { 0x2665, 0x26a1, 0x2b58, - 0xe000...0xe0a9, - 0xe4fa...0xe7ef, + 0xe000...0xe00a, + 0xe0a0...0xe0a3, + 0xe5fa...0xe6b8, + 0xe700...0xe7ef, 0xea60, 0xea62...0xea7c, - 0xea7e...0xea98, + 0xea7e...0xea88, + 0xea8a...0xea8c, + 0xea8f...0xea98, 0xeaa3...0xeab3, - 0xeab8...0xead3, - 0xead7...0xeb42, - 0xeb44...0xeb6d, + 0xeab8...0xeac7, + 0xeac9, + 0xeacc...0xead3, + 0xead7...0xeb09, + 0xeb0b...0xeb42, + 0xeb44...0xeb4e, + 0xeb50...0xeb6d, 0xeb72...0xeb89, 0xeb8b...0xeb99, 0xeb9b...0xebd4, 0xebd7...0xec06, 0xec08...0xec0a, 0xec0d...0xec1e, - 0xed00...0xf02f, + 0xed00...0xefce, + 0xf000...0xf02f, 0xf031...0xf03c, 0xf03f, 0xf041...0xf043, @@ -384,10 +380,22 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf0cb...0xf0d5, 0xf0d7...0xf0dd, 0xf0df...0xf0e6, - 0xf0e8...0xf295, + 0xf0e8...0xf105, + 0xf108...0xf12f, + 0xf131...0xf140, + 0xf142...0xf152, + 0xf155, + 0xf15a...0xf174, + 0xf176, + 0xf179...0xf181, + 0xf183...0xf220, + 0xf223, + 0xf22e...0xf254, + 0xf259, + 0xf25c...0xf295, 0xf297...0xf2c4, 0xf2c6...0xf2ef, - 0xf2f1...0xf847, + 0xf2f1...0xf381, 0xf0001...0xf1af0, => .{ .size = .fit_cover1, @@ -672,6 +680,8 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf03e, 0xf071, 0xf08c, + 0xf153...0xf154, + 0xf158, => .{ .size = .fit_cover1, .height = .icon, @@ -828,6 +838,161 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9998803756692248, .relative_y = 0.0001196243307751, }, + 0xf106...0xf107, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5000000000000000, + .relative_y = 0.2853688029020556, + }, + 0xf130, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9998602571268865, + }, + 0xf141, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.2593984962406015, + .relative_y = 0.3696741854636592, + }, + 0xf156, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8752505446623093, + .relative_y = 0.0623155929038282, + }, + 0xf157, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8756468797564688, + .relative_y = 0.0624338624338624, + }, + 0xf159, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8756067947646895, + .relative_y = 0.0623492063492063, + }, + 0xf175, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9989423585404548, + .relative_y = 0.0005288207297726, + }, + 0xf177...0xf178, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6250661025912215, + .relative_y = 0.1877313590692755, + }, + 0xf182, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9998046921689268, + }, + 0xf221, + 0xf224...0xf226, + 0xf228, + 0xf22a, + 0xf22c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9994854643684076, + }, + 0xf222, + 0xf227, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8746819883943630, + .relative_y = 0.0624017379870223, + }, + 0xf229, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9370837263813853, + .relative_y = 0.0624017379870223, + }, + 0xf22b, + 0xf22d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6874767744332962, + .relative_y = 0.1560043449675557, + }, + 0xf255...0xf256, + 0xf25a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9993997599039616, + }, + 0xf257, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7810124049619848, + .relative_y = 0.0935945806894186, + }, + 0xf258, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7498142113988452, + .relative_y = 0.1247927742525582, + }, + 0xf25b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9975006099019084, + }, 0xf296, => .{ .size = .fit_cover1, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 82cd159b6..a973c6c6e 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -57,6 +57,7 @@ class PatchSetAttributeEntry(TypedDict): class PatchSet(TypedDict): + Name: str SymStart: int SymEnd: int SrcStart: int | None @@ -113,20 +114,43 @@ class PatchSetExtractor(ast.NodeVisitor): if hasattr(ast, "unparse"): return eval( ast.unparse(node), - {"box_keep": True}, - {"self": SimpleNamespace(args=SimpleNamespace(careful=True))}, + {"box_enabled": False, "box_keep": False}, + { + "self": SimpleNamespace( + args=SimpleNamespace( + careful=False, + custom=False, + fontawesome=True, + fontawesomeextension=True, + fontlogos=True, + octicons=True, + codicons=True, + powersymbols=True, + pomicons=True, + powerline=True, + powerlineextra=True, + material=True, + weather=True, + ) + ), + }, ) msg = f"" raise ValueError(msg) from None def process_patch_entry(self, dict_node: ast.Dict) -> None: entry = {} - disallowed_key_nodes = frozenset({"Enabled", "Name", "Filename", "Exact"}) + disallowed_key_nodes = frozenset({"Filename", "Exact"}) for key_node, value_node in zip(dict_node.keys, dict_node.values): if ( isinstance(key_node, ast.Constant) and key_node.value not in disallowed_key_nodes ): + if key_node.value == "Enabled": + if self.safe_literal_eval(value_node): + continue # This patch set is enabled, continue to next key + else: + return # This patch set is disabled, skip key = ast.literal_eval(cast("ast.Constant", key_node)) entry[key] = self.resolve_symbol(value_node) self.patch_set_values.append(cast("PatchSet", entry)) @@ -275,12 +299,17 @@ def generate_zig_switch_arms( entries: dict[int, PatchSetAttributeEntry] = {} for entry in patch_sets: + print(f"Info: Extracting rules from patch set '{entry['Name']}'") attributes = entry["Attributes"] for cp in range(entry["SymStart"], entry["SymEnd"] + 1): - entries[cp] = attributes["default"].copy() - - entries |= {k: v for k, v in attributes.items() if isinstance(k, int)} + if cp not in cmap: + print(f"Info: Skipping missing codepoint {hex(cp)}") + continue + if cp in attributes: + entries[cp] = attributes[cp].copy() + else: + entries[cp] = attributes["default"].copy() if entry["ScaleRules"] is not None: if "ScaleGroups" not in entry["ScaleRules"]: @@ -335,8 +364,6 @@ def generate_zig_switch_arms( this_bounds[0] - xMin ) / group_width - del entries[0] - # Group codepoints by attribute key grouped = defaultdict[AttributeHash, list[int]](list) for cp, attr in entries.items(): From 44951d9b1c8b1dddba5abd5e146dbcd15cc8c69b Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 22 Sep 2025 00:55:21 -0700 Subject: [PATCH 159/319] Handle font_patcher codepoint offsets --- src/font/nerd_font_attributes.zig | 1919 ++++++++++++++++++++++++++++- src/font/nerd_font_codegen.py | 51 +- 2 files changed, 1892 insertions(+), 78 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 638c9aa6c..1ddc0f691 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -56,6 +56,13 @@ pub fn getConstraint(cp: u21) ?Constraint { .pad_top = 0.15, .pad_bottom = 0.15, }, + 0xe0a0...0xe0a3, + 0xe0cf, + => .{ + .size = .fit_cover1, + .align_horizontal = .center1, + .align_vertical = .center1, + }, 0xe0b0, => .{ .size = .stretch, @@ -279,12 +286,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_horizontal = .start, .align_vertical = .center1, }, - 0xe0cf, - => .{ - .size = .fit_cover1, - .align_horizontal = .center1, - .align_vertical = .center1, - }, 0xe0d2, => .{ .size = .stretch, @@ -333,14 +334,1304 @@ pub fn getConstraint(cp: u21) ?Constraint { .pad_bottom = -0.01, .max_xy_ratio = 0.7, }, + 0xe300, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8984375000000000, + .relative_y = 0.0986328125000000, + }, + 0xe301, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8798828125000000, + .relative_y = 0.1171875000000000, + }, + 0xe302, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7646484375000000, + .relative_y = 0.2314453125000000, + }, + 0xe303, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8789062500000000, + .relative_y = 0.1171875000000000, + }, + 0xe304, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9755859375000000, + .relative_y = 0.0244140625000000, + }, + 0xe305, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9960937500000000, + .relative_y = 0.0019531250000000, + }, + 0xe306, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9863281250000000, + .relative_y = 0.0097656250000000, + }, + 0xe307, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9951171875000000, + .relative_y = 0.0039062500000000, + }, + 0xe308, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9785156250000000, + .relative_y = 0.0195312500000000, + }, + 0xe309, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9736328125000000, + .relative_y = 0.0214843750000000, + }, + 0xe30a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9648437500000000, + .relative_y = 0.0302734375000000, + }, + 0xe30b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8437500000000000, + .relative_y = 0.1513671875000000, + }, + 0xe30c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8027343750000000, + .relative_y = 0.1835937500000000, + }, + 0xe30d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7753906250000000, + .relative_y = 0.1083984375000000, + }, + 0xe30e, + 0xe365, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9833984375000000, + .relative_y = 0.0166015625000000, + }, + 0xe30f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9716796875000000, + .relative_y = 0.0263671875000000, + }, + 0xe310, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6621093750000000, + .relative_y = 0.0986328125000000, + }, + 0xe311, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6425781250000000, + .relative_y = 0.1171875000000000, + }, + 0xe312, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5322265625000000, + .relative_y = 0.2314453125000000, + }, + 0xe313, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6416015625000000, + .relative_y = 0.1181640625000000, + }, + 0xe314, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7382812500000000, + .relative_y = 0.0195312500000000, + }, + 0xe315, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6787109375000000, + .relative_y = 0.1357421875000000, + }, + 0xe316, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7480468750000000, + .relative_y = 0.0097656250000000, + }, + 0xe317, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7529296875000000, + .relative_y = 0.0048828125000000, + }, + 0xe318, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.0263671875000000, + }, + 0xe319, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7402343750000000, + .relative_y = 0.0195312500000000, + }, + 0xe31a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7294921875000000, + .relative_y = 0.0283203125000000, + }, + 0xe31b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6074218750000000, + .relative_y = 0.1503906250000000, + }, + 0xe31c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7363281250000000, + .relative_y = 0.0224609375000000, + }, + 0xe31d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7460937500000000, + .relative_y = 0.0126953125000000, + }, + 0xe31e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.2675781250000000, + .relative_y = 0.3310546875000000, + }, + 0xe31f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7363281250000000, + .relative_y = 0.0986328125000000, + }, + 0xe320, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7177734375000000, + .relative_y = 0.1171875000000000, + }, + 0xe321, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8085937500000000, + .relative_y = 0.0253906250000000, + }, + 0xe322, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7509765625000000, + .relative_y = 0.0839843750000000, + }, + 0xe323, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8281250000000000, + .relative_y = 0.0097656250000000, + }, + 0xe324, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8349609375000000, + }, + 0xe325, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8154296875000000, + .relative_y = 0.0214843750000000, + }, + 0xe326, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8144531250000000, + .relative_y = 0.0195312500000000, + }, + 0xe327, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8076171875000000, + .relative_y = 0.0273437500000000, + }, + 0xe328, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6845703125000000, + .relative_y = 0.1503906250000000, + }, + 0xe329, + 0xe367, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8173828125000000, + .relative_y = 0.0175781250000000, + }, + 0xe32a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8105468750000000, + .relative_y = 0.0263671875000000, + }, + 0xe32b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5175781250000000, + .relative_y = 0.2421875000000000, + }, + 0xe32c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6992187500000000, + .relative_y = 0.1005859375000000, + }, + 0xe32d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6787109375000000, + .relative_y = 0.1201171875000000, + }, + 0xe32e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5654296875000000, + .relative_y = 0.2324218750000000, + }, + 0xe32f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0273437500000000, + }, + 0xe330, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7148437500000000, + .relative_y = 0.0830078125000000, + }, + 0xe331, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7919921875000000, + .relative_y = 0.0097656250000000, + }, + 0xe332, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7871093750000000, + .relative_y = 0.0126953125000000, + }, + 0xe333, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0263671875000000, + }, + 0xe334, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7773437500000000, + .relative_y = 0.0195312500000000, + }, + 0xe335, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0283203125000000, + }, + 0xe336, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6503906250000000, + .relative_y = 0.1503906250000000, + }, + 0xe337, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7753906250000000, + .relative_y = 0.0234375000000000, + }, + 0xe338, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7792968750000000, + .relative_y = 0.0185546875000000, + }, + 0xe339, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4882812500000000, + .relative_y = 0.2109375000000000, + }, + 0xe33a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5283203125000000, + .relative_y = 0.2324218750000000, + }, + 0xe33b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5449218750000000, + .relative_y = 0.2148437500000000, + }, + 0xe33c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6006674082313682, + .relative_y = 0.1952169076751947, + }, + 0xe33d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5273437500000000, + .relative_y = 0.2324218750000000, + }, + 0xe33e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.1904296875000000, + .relative_y = 0.5986328125000000, + }, + 0xe33f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3300781250000000, + .relative_y = 0.3544921875000000, + }, + 0xe340, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5273437500000000, + .relative_y = 0.2373046875000000, + }, + 0xe341, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4814453125000000, + .relative_y = 0.2138671875000000, + }, + 0xe343, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6816406250000000, + .relative_y = 0.1591796875000000, + }, + 0xe344, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3369140625000000, + .relative_y = 0.3154296875000000, + }, + 0xe345, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6073507601038191, + .relative_y = 0.1629495736002966, + }, + 0xe348, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6347656250000000, + .relative_y = 0.1826171875000000, + }, + 0xe349, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3402542969850663, + .relative_y = 0.3471400394477318, + }, + 0xe34b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6621093750000000, + .relative_y = 0.1689453125000000, + }, + 0xe34c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8662109375000000, + .relative_y = 0.1337890625000000, + }, + 0xe34d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9892578125000000, + }, + 0xe351, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7992831541218638, + .relative_y = 0.0919952210274791, + }, + 0xe352, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4050179211469534, + .relative_y = 0.3739545997610514, + }, + 0xe353, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7040688830943068, + .relative_y = 0.1811983920034767, + }, + 0xe356, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7564102564102564, + .relative_y = 0.1213017751479290, + }, + 0xe357, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7509765625000000, + .relative_y = 0.1230468750000000, + }, + 0xe358, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7490234375000000, + .relative_y = 0.1250000000000000, + }, + 0xe359, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7643248629795715, + .relative_y = 0.1121076233183857, + }, + 0xe35a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7643248629795715, + .relative_y = 0.1111111111111111, + }, + 0xe35b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7683109118086696, + .relative_y = 0.1111111111111111, + }, + 0xe35c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9895366218236173, + }, + 0xe35d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5590433482810164, + .relative_y = 0.2152466367713005, + }, + 0xe35e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7443946188340808, + .relative_y = 0.0134529147982063, + }, + 0xe35f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9845540607872446, + .relative_y = 0.0154459392127554, + }, + 0xe360, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7852516193323368, + .relative_y = 0.0154459392127554, + }, + 0xe361, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8241155954160438, + .relative_y = 0.0124564025909317, + }, + 0xe364, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8251953125000000, + .relative_y = 0.0097656250000000, + }, + 0xe366, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7832031250000000, + .relative_y = 0.0166015625000000, + }, + 0xe369, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4902343750000000, + .relative_y = 0.2548828125000000, + }, + 0xe36b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9335937500000000, + .relative_y = 0.0263671875000000, + }, + 0xe36c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7076171875000000, + .relative_y = 0.1083984375000000, + }, + 0xe36d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8427734375000000, + .relative_y = 0.0625000000000000, + }, + 0xe36e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8712355686563498, + .relative_y = 0.0383689511176615, + }, + 0xe371, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6163708086785010, + .relative_y = 0.1903353057199211, + }, + 0xe372, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9725209080047790, + }, + 0xe373, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8737672583826430, + .relative_y = 0.0009861932938856, + }, + 0xe374, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3185404339250493, + .relative_y = 0.2840236686390533, + }, + 0xe375, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6839250493096647, + .relative_y = 0.1267258382642998, + }, + 0xe376, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7061143984220908, + .relative_y = 0.1301775147928994, + }, + 0xe377, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7386587771203156, + .relative_y = 0.1518737672583826, + }, + 0xe378, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7386587771203156, + .relative_y = 0.1508875739644970, + }, + 0xe379, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5808678500986193, + .relative_y = 0.1794871794871795, + }, + 0xe37a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5315581854043393, + .relative_y = 0.2258382642998027, + }, + 0xe37b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5808678500986193, + .relative_y = 0.1804733727810651, + }, + 0xe37d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9003906250000000, + .relative_y = 0.0957031250000000, + }, + 0xe37e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6015625000000000, + .relative_y = 0.2324218750000000, + }, + 0xe37f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3300781250000000, + .relative_y = 0.3593750000000000, + }, + 0xe380, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3300781250000000, + .relative_y = 0.3496093750000000, + }, + 0xe381...0xe383, + 0xe385...0xe388, + 0xf451...0xf453, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7500000000000000, + .relative_y = 0.1250000000000000, + }, + 0xe389...0xe38c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987004548408057, + .relative_height = 0.9974025974025974, + .relative_y = 0.0012987012987013, + }, + 0xe38e...0xe391, + 0xe394, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4990253411306043, + .relative_height = 0.9987012987012988, + .relative_x = 0.4996751137102014, + }, + 0xe392...0xe393, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4996751137102014, + .relative_height = 0.9987012987012988, + .relative_x = 0.4990253411306043, + }, + 0xe395...0xe396, + 0xe39b, + 0xe3a2...0xe3a8, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7568897637795275, + .relative_y = 0.1190944881889764, + }, + 0xe397...0xe39a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7578740157480315, + .relative_y = 0.1190944881889764, + }, + 0xe39c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7529527559055118, + .relative_y = 0.1190944881889764, + }, + 0xe39d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7539370078740157, + .relative_y = 0.1190944881889764, + }, + 0xe39e...0xe3a0, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7549212598425197, + .relative_y = 0.1190944881889764, + }, + 0xe3a1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7559055118110236, + .relative_y = 0.1190944881889764, + }, + 0xe3a9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7568897637795275, + .relative_y = 0.1181102362204724, + }, + 0xe3aa, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9980314960629921, + .relative_y = 0.0019685039370079, + }, + 0xe3ab, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7962598425196851, + }, + 0xe3ac, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8316929133858267, + .relative_y = 0.0019685039370079, + }, + 0xe3ad, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7578740157480315, + .relative_y = 0.0009842519685039, + }, + 0xe3ae, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6200787401574803, + .relative_y = 0.2283464566929134, + }, + 0xe3af, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7057086614173228, + .relative_y = 0.1456692913385827, + }, + 0xe3b0, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7037401574803149, + .relative_y = 0.1476377952755905, + }, + 0xe3b1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7125062282012955, + .relative_y = 0.1400099651220728, + }, + 0xe3b2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6982421875000000, + .relative_y = 0.1523437500000000, + }, + 0xe3b3, + 0xe3b5...0xe3b6, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7001953125000000, + .relative_y = 0.1503906250000000, + }, + 0xe3b4, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7011718750000000, + .relative_y = 0.1494140625000000, + }, + 0xe3b7...0xe3bb, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7000244081034903, + .relative_y = 0.1505979985355138, + }, + 0xe3bc, + 0xe3c0, + 0xe3c3, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9997559189650964, + .relative_y = 0.0002440810349036, + }, + 0xe3bd, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9431291188674640, + .relative_y = 0.0285574810837198, + }, + 0xe3be, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9896346920510943, + .relative_y = 0.0051257017329753, + }, + 0xe3bf, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9060288015621186, + .relative_y = 0.0471076397363925, + }, + 0xe3c1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6590187942396876, + .relative_y = 0.1349768123016842, + }, + 0xe3c2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7939956065413717, + }, + 0xe3c9...0xe3ca, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9175627240143369, + .relative_y = 0.0824372759856631, + }, 0x23fb...0x23fe, 0x2665, 0x26a1, 0x2b58, 0xe000...0xe00a, - 0xe0a0...0xe0a3, + 0xe200...0xe2a9, + 0xe342, + 0xe346...0xe347, + 0xe34a, + 0xe34e...0xe350, + 0xe354...0xe355, + 0xe362...0xe363, + 0xe368, + 0xe36a, + 0xe36f...0xe370, + 0xe37c, + 0xe384, + 0xe38d, + 0xe3c4...0xe3c8, + 0xe3cb...0xe3e3, 0xe5fa...0xe6b8, - 0xe700...0xe7ef, + 0xe700...0xe8ef, 0xea60, 0xea62...0xea7c, 0xea7e...0xea88, @@ -361,26 +1652,34 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xec08...0xec0a, 0xec0d...0xec1e, 0xed00...0xefce, - 0xf000...0xf02f, - 0xf031...0xf03c, + 0xf000...0xf004, + 0xf006...0xf025, + 0xf028...0xf02a, + 0xf02c...0xf02f, + 0xf034, + 0xf036...0xf03c, 0xf03f, 0xf041...0xf043, - 0xf045...0xf049, - 0xf04b...0xf050, - 0xf054...0xf059, - 0xf05c...0xf070, - 0xf072...0xf077, + 0xf045, + 0xf047, + 0xf053...0xf059, + 0xf05c...0xf05f, + 0xf062, + 0xf064...0xf070, + 0xf072...0xf076, 0xf079...0xf07a, - 0xf07c...0xf080, - 0xf082...0xf08b, + 0xf07c...0xf07d, + 0xf07f...0xf080, + 0xf082...0xf088, + 0xf08a...0xf08b, 0xf08d...0xf091, 0xf093...0xf0a0, - 0xf0a5...0xf0a9, + 0xf0a6...0xf0a9, 0xf0ab...0xf0c9, 0xf0cb...0xf0d5, - 0xf0d7...0xf0dd, + 0xf0db, 0xf0df...0xf0e6, - 0xf0e8...0xf105, + 0xf0e8...0xf0ff, 0xf108...0xf12f, 0xf131...0xf140, 0xf142...0xf152, @@ -390,12 +1689,47 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf179...0xf181, 0xf183...0xf220, 0xf223, - 0xf22e...0xf254, + 0xf22e...0xf245, + 0xf247...0xf254, 0xf259, - 0xf25c...0xf295, - 0xf297...0xf2c4, - 0xf2c6...0xf2ef, - 0xf2f1...0xf381, + 0xf25c, + 0xf25e...0xf269, + 0xf26b, + 0xf26e...0xf276, + 0xf278...0xf27d, + 0xf281...0xf286, + 0xf289...0xf295, + 0xf297...0xf29d, + 0xf29f...0xf2a4, + 0xf2a6...0xf2a7, + 0xf2a9...0xf2ad, + 0xf2af...0xf2b8, + 0xf2ba...0xf2be, + 0xf2c0...0xf2c4, + 0xf2c6...0xf2c8, + 0xf2ca...0xf2cb, + 0xf2cd, + 0xf2d2...0xf2d6, + 0xf2d8...0xf2ef, + 0xf2f1...0xf313, + 0xf315...0xf381, + 0xf400...0xf418, + 0xf41a...0xf42f, + 0xf431...0xf43d, + 0xf43f, + 0xf441...0xf443, + 0xf445...0xf449, + 0xf44b...0xf450, + 0xf454...0xf459, + 0xf45c...0xf470, + 0xf472...0xf47a, + 0xf47c...0xf480, + 0xf482...0xf491, + 0xf493...0xf49e, + 0xf4a0...0xf4c2, + 0xf4c4...0xf4ee, + 0xf4f3...0xf51c, + 0xf51e...0xf532, 0xf0001...0xf1af0, => .{ .size = .fit_cover1, @@ -676,12 +2010,45 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_width = 0.8008342022940563, .relative_x = 0.1991657977059437, }, + 0xf005, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9999664113932554, + .relative_y = 0.0000335886067446, + }, + 0xf026...0xf027, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9786184354605580, + .relative_y = 0.0103951316192896, + }, + 0xf02b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9758052740827267, + .relative_y = 0.0238869355863696, + }, 0xf030, 0xf03e, + 0xf046, 0xf071, 0xf08c, 0xf153...0xf154, 0xf158, + 0xf280, + 0xf2a5, + 0xf2bf, + 0xf2d0, + 0xf2d7, => .{ .size = .fit_cover1, .height = .icon, @@ -690,7 +2057,26 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8751322751322751, .relative_y = 0.0624338624338624, }, + 0xf031...0xf033, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987922705314010, + .relative_y = 0.0006038647342995, + }, + 0xf035, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9989935587761675, + .relative_y = 0.0004025764895330, + }, 0xf03d, + 0xf0a4...0xf0a5, => .{ .size = .fit_cover1, .height = .icon, @@ -714,38 +2100,64 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.9923123057630445, - .relative_y = 0.0002010014265405, + .relative_height = 0.9925925925925926, }, + 0xf048, 0xf04a, + 0xf04e, 0xf051, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7506817256817256, - .relative_y = 0.1247354497354497, + .relative_height = 0.8577706898990622, + .relative_y = 0.0711892586341537, + }, + 0xf049, + 0xf050, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8579450878868969, + .relative_y = 0.0710148606463189, + }, + 0xf04b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9997041418532618, + .relative_y = 0.0002958581467381, + }, + 0xf04c...0xf04d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8572940020656472, + .relative_y = 0.0713404035569438, + }, + 0xf04f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7138835298072554, + .relative_y = 0.1433479295317200, }, 0xf052, - 0xf081, - 0xf092, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8748851565736010, - .relative_y = 0.0626172338785870, - }, - 0xf053, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8759430588185509, - .relative_y = 0.0620882827561120, + .relative_height = 0.9999748091795350, }, 0xf05a...0xf05b, 0xf0a2, @@ -758,14 +2170,41 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9997176214776941, .relative_y = 0.0002010014265405, }, + 0xf060...0xf061, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8567975830815709, + .relative_y = 0.0719033232628399, + }, + 0xf063, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987915407854985, + .relative_y = 0.0006042296072508, + }, + 0xf077, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5700483091787439, + .relative_y = 0.2862318840579710, + }, 0xf078, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.4993298596163721, - .relative_y = 0.1879786499051550, + .relative_height = 0.5700483091787439, + .relative_y = 0.1437198067632850, }, 0xf07b, => .{ @@ -776,6 +2215,34 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8139763779527559, .relative_y = 0.0930118110236220, }, + 0xf07e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4989429175475687, + .relative_y = 0.2505285412262157, + }, + 0xf081, + 0xf092, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8748851565736010, + .relative_y = 0.0626172338785870, + }, + 0xf089, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9998488512696494, + .relative_y = 0.0001511487303507, + }, 0xf0a1, => .{ .size = .fit_cover1, @@ -793,15 +2260,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_height = 0.9998551487695376, }, - 0xf0a4, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7500526916695081, - .relative_y = 0.1250334663306335, - }, 0xf0ca, => .{ .size = .fit_cover1, @@ -820,14 +2278,63 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.6978346456692913, .relative_y = 0.1510826771653543, }, - 0xf0de, + 0xf0d7, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.3756613756613756, - .relative_y = 0.5708994708994709, + .relative_height = 0.4281400966183575, + .relative_y = 0.2053140096618357, + }, + 0xf0d8, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4281400966183575, + .relative_y = 0.3472222222222222, + }, + 0xf0d9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7140772371750631, + .relative_y = 0.1333462732919255, + }, + 0xf0da, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7140396210163651, + .relative_y = 0.1333838894506235, + }, + 0xf0dc, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + }, + 0xf0dd, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .relative_height = 0.4275362318840580, + .relative_y = 0.0012077294685990, + }, + 0xf0de, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .relative_height = 0.4287439613526570, + .relative_y = 0.5712560386473430, }, 0xf0e7, => .{ @@ -838,6 +2345,34 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9998803756692248, .relative_y = 0.0001196243307751, }, + 0xf100...0xf101, + 0xf104...0xf105, + 0xf2c5, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8573155985489722, + .relative_y = 0.0713422007255139, + }, + 0xf102, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9286577992744861, + .relative_y = 0.0713422007255139, + }, + 0xf103, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9286577992744861, + }, 0xf106...0xf107, => .{ .size = .fit_cover1, @@ -958,6 +2493,15 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.6874767744332962, .relative_y = 0.1560043449675557, }, + 0xf246, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9997816982260961, + .relative_y = 0.0002148974577418, + }, 0xf255...0xf256, 0xf25a, => .{ @@ -993,6 +2537,57 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_height = 0.9975006099019084, }, + 0xf25d, + 0xf26c, + 0xf277, + 0xf2b9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9993050939633815, + .relative_y = 0.0004531995890990, + }, + 0xf26a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9686385884343465, + .relative_y = 0.0157864523536165, + }, + 0xf26d, + 0xf27e, + 0xf29e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8745241404314460, + .relative_y = 0.0628436763550668, + }, + 0xf27f, + 0xf288, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9693121693121693, + .relative_y = 0.0153439153439153, + }, + 0xf287, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7661375661375661, + .relative_y = 0.1169312169312169, + }, 0xf296, => .{ .size = .fit_cover1, @@ -1002,14 +2597,68 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9627792014248586, .relative_y = 0.0187142907131644, }, - 0xf2c5, + 0xf2a8, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8573155985489722, - .relative_y = 0.0713422007255139, + .relative_height = 0.9723057045073505, + .relative_y = 0.0137673026561915, + }, + 0xf2ae, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8931764276591863, + .relative_y = 0.0534391534391534, + }, + 0xf2c9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9228515625000000, + .relative_y = 0.0385742187500000, + }, + 0xf2cc, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8076171875000000, + .relative_y = 0.0961914062500000, + }, + 0xf2ce, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9722222222222222, + .relative_y = 0.0138888888888889, + }, + 0xf2cf, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9994939038417299, + .relative_y = 0.0005060961582701, + }, + 0xf2d1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.0931216931216931, + .relative_y = 0.4534391534391534, }, 0xf2f0, => .{ @@ -1020,6 +2669,158 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9669226518842459, .relative_y = 0.0165984862232646, }, + 0xf314, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5486681262543333, + .relative_y = 0.2256704980842912, + }, + 0xf419, + 0xf45a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8750000000000000, + .relative_y = 0.0625000000000000, + }, + 0xf430, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8496093750000000, + .relative_y = 0.0751953125000000, + }, + 0xf43e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5024414062500000, + .relative_y = 0.2500000000000000, + }, + 0xf440, + 0xf492, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8437500000000000, + .relative_y = 0.0781250000000000, + }, + 0xf444, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5000000000000000, + .relative_y = 0.2500000000000000, + }, + 0xf44a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4560546875000000, + .relative_y = 0.2719726562500000, + }, + 0xf45b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.0937500000000000, + .relative_y = 0.4531250000000000, + }, + 0xf471, + 0xf481, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9375000000000000, + .relative_y = 0.0312500000000000, + }, + 0xf47b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3593750000000000, + .relative_y = 0.3281250000000000, + }, + 0xf49f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7680000000000000, + .relative_y = 0.1160000000000000, + }, + 0xf4c3, + 0xf51d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5417989417989418, + .relative_y = 0.2291005291005291, + }, + 0xf4ef, + 0xf4f2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7142857142857143, + .relative_x = 0.1428571428571428, + }, + 0xf4f0, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9642857142857143, + .relative_height = 0.7407407407407407, + .relative_y = 0.1111111111111111, + }, + 0xf4f1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9642857142857143, + .relative_height = 0.7407407407407407, + .relative_x = 0.0357142857142857, + .relative_y = 0.1111111111111111, + }, + 0xf533, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9228395061728395, + .relative_y = 0.0390946502057613, + }, else => null, }; } diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index a973c6c6e..c7d592798 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -302,14 +302,23 @@ def generate_zig_switch_arms( print(f"Info: Extracting rules from patch set '{entry['Name']}'") attributes = entry["Attributes"] - for cp in range(entry["SymStart"], entry["SymEnd"] + 1): - if cp not in cmap: - print(f"Info: Skipping missing codepoint {hex(cp)}") + # A glyph's scale rules are specified using its codepoint in + # the original font, which is sometimes different from its + # Nerd Font codepoint. In font_patcher, the font to be patched + # (including the Symbols Only font embedded in Ghostty) is + # termed the sourceFont, while the original font is the + # symbolFont. Thus, the offset that maps the scale rule + # codepoint to the Nerd Font codepoint is SrcStart - SymStart. + cp_offset = entry["SrcStart"] - entry["SymStart"] if entry["SrcStart"] else 0 + for cp_rule in range(entry["SymStart"], entry["SymEnd"] + 1): + cp_font = cp_rule + cp_offset + if cp_font not in cmap: + print(f"Info: Skipping missing codepoint {hex(cp_font)}") continue - if cp in attributes: - entries[cp] = attributes[cp].copy() + if cp_rule in attributes: + entries[cp_font] = attributes[cp_rule].copy() else: - entries[cp] = attributes["default"].copy() + entries[cp_font] = attributes["default"].copy() if entry["ScaleRules"] is not None: if "ScaleGroups" not in entry["ScaleRules"]: @@ -323,14 +332,15 @@ def generate_zig_switch_arms( yMax = -math.inf individual_bounds: dict[int, tuple[int, int, int, int]] = {} individual_advances: set[float] = set() - for cp in group: - if cp not in cmap: + for cp_rule in group: + cp_font = cp_rule + cp_offset + if cp_font not in cmap: continue - glyph = glyphs[cmap[cp]] + glyph = glyphs[cmap[cp_font]] individual_advances.add(glyph.width) bounds = BoundsPen(glyphSet=glyphs) glyph.draw(bounds) - individual_bounds[cp] = bounds.bounds + individual_bounds[cp_font] = bounds.bounds xMin = min(bounds.bounds[0], xMin) yMin = min(bounds.bounds[1], yMin) xMax = max(bounds.bounds[2], xMax) @@ -340,27 +350,30 @@ def generate_zig_switch_arms( group_is_monospace = (len(individual_bounds) > 1) and ( len(individual_advances) == 1 ) - for cp in group: + for cp_rule in group: + cp_font = cp_rule + cp_offset if ( - cp not in cmap - or cp not in entries + cp_font not in cmap + or cp_font not in entries # Codepoints may contribute to the bounding box of multiple groups, # but should be scaled according to the first group they are found # in. Hence, to avoid overwriting, we need to skip codepoints that # have already been assigned a scale group. - or "relative_height" in entries[cp] + or "relative_height" in entries[cp_font] ): continue - this_bounds = individual_bounds[cp] + this_bounds = individual_bounds[cp_font] this_height = this_bounds[3] - this_bounds[1] - entries[cp]["relative_height"] = this_height / group_height - entries[cp]["relative_y"] = (this_bounds[1] - yMin) / group_height + entries[cp_font]["relative_height"] = this_height / group_height + entries[cp_font]["relative_y"] = ( + this_bounds[1] - yMin + ) / group_height # Horizontal alignment should only be grouped if the group is monospace, # that is, if all glyphs in the group have the same advance width. if group_is_monospace: this_width = this_bounds[2] - this_bounds[0] - entries[cp]["relative_width"] = this_width / group_width - entries[cp]["relative_x"] = ( + entries[cp_font]["relative_width"] = this_width / group_width + entries[cp_font]["relative_x"] = ( this_bounds[0] - xMin ) / group_width From 78f1bf18071db54f16651aec0295387028658959 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 22 Sep 2025 00:59:43 -0700 Subject: [PATCH 160/319] Handle font_patcher codepoint range overlaps --- src/font/nerd_font_attributes.zig | 476 ++++++++++-------------------- src/font/nerd_font_codegen.py | 44 ++- 2 files changed, 185 insertions(+), 335 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 1ddc0f691..138108288 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -143,18 +143,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .max_xy_ratio = 0.5, }, - 0xe0b8, - 0xe0bc, - => .{ - .size = .stretch, - .max_constraint_width = 1, - .align_horizontal = .start, - .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, - }, 0xe0b9, 0xe0bd, => .{ @@ -163,18 +151,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_horizontal = .start, .align_vertical = .center1, }, - 0xe0ba, - 0xe0be, - => .{ - .size = .stretch, - .max_constraint_width = 1, - .align_horizontal = .end, - .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, - }, 0xe0bb, 0xe0bf, => .{ @@ -1651,35 +1627,25 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xebd7...0xec06, 0xec08...0xec0a, 0xec0d...0xec1e, - 0xed00...0xefce, + 0xed00...0xedff, + 0xee0c...0xefce, 0xf000...0xf004, 0xf006...0xf025, 0xf028...0xf02a, - 0xf02c...0xf02f, + 0xf02c...0xf030, 0xf034, - 0xf036...0xf03c, - 0xf03f, - 0xf041...0xf043, + 0xf036...0xf043, 0xf045, 0xf047, - 0xf053...0xf059, - 0xf05c...0xf05f, + 0xf053...0xf05f, 0xf062, - 0xf064...0xf070, - 0xf072...0xf076, - 0xf079...0xf07a, - 0xf07c...0xf07d, - 0xf07f...0xf080, - 0xf082...0xf088, - 0xf08a...0xf08b, - 0xf08d...0xf091, - 0xf093...0xf0a0, - 0xf0a6...0xf0a9, - 0xf0ab...0xf0c9, - 0xf0cb...0xf0d5, + 0xf064...0xf076, + 0xf079...0xf07d, + 0xf07f...0xf088, + 0xf08a...0xf0a3, + 0xf0a6...0xf0d6, 0xf0db, - 0xf0df...0xf0e6, - 0xf0e8...0xf0ff, + 0xf0df...0xf0ff, 0xf108...0xf12f, 0xf131...0xf140, 0xf142...0xf152, @@ -1689,30 +1655,9 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf179...0xf181, 0xf183...0xf220, 0xf223, - 0xf22e...0xf245, - 0xf247...0xf254, + 0xf22e...0xf254, 0xf259, - 0xf25c, - 0xf25e...0xf269, - 0xf26b, - 0xf26e...0xf276, - 0xf278...0xf27d, - 0xf281...0xf286, - 0xf289...0xf295, - 0xf297...0xf29d, - 0xf29f...0xf2a4, - 0xf2a6...0xf2a7, - 0xf2a9...0xf2ad, - 0xf2af...0xf2b8, - 0xf2ba...0xf2be, - 0xf2c0...0xf2c4, - 0xf2c6...0xf2c8, - 0xf2ca...0xf2cb, - 0xf2cd, - 0xf2d2...0xf2d6, - 0xf2d8...0xf2ef, - 0xf2f1...0xf313, - 0xf315...0xf381, + 0xf25c...0xf381, 0xf400...0xf418, 0xf41a...0xf42f, 0xf431...0xf43d, @@ -2010,6 +1955,129 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_width = 0.8008342022940563, .relative_x = 0.1991657977059437, }, + 0xe0ba, + 0xe0be, + 0xee00, + 0xee03, + => .{ + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .end, + .align_vertical = .center1, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xee01, + 0xee04, + => .{ + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .pad_left = -0.1, + .pad_right = -0.1, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xe0b8, + 0xe0bc, + 0xee02, + 0xee05, + => .{ + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .start, + .align_vertical = .center1, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xee06, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7059415911379657, + .relative_height = 0.2234524408656266, + .relative_x = 0.1470292044310171, + .relative_y = 0.7765475591343735, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.03, + .pad_bottom = 0.03, + }, + 0xee07, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5000000000000000, + .relative_height = 0.7498741821841973, + .relative_x = 0.5000000000000000, + .relative_y = 0.2501258178158027, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.03, + .pad_bottom = 0.03, + }, + 0xee08, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6299093655589124, + .relative_height = 0.8535480624056366, + .relative_x = 0.3700906344410876, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.03, + .pad_bottom = 0.03, + }, + 0xee09, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4997483643683945, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.03, + .pad_bottom = 0.03, + }, + 0xee0a, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6299093655589124, + .relative_height = 0.8535480624056366, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.03, + .pad_bottom = 0.03, + }, + 0xee0b, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5000000000000000, + .relative_height = 0.7498741821841973, + .relative_y = 0.2501258178158027, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.03, + .pad_bottom = 0.03, + }, 0xf005, => .{ .size = .fit_cover1, @@ -2037,26 +2105,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9758052740827267, .relative_y = 0.0238869355863696, }, - 0xf030, - 0xf03e, - 0xf046, - 0xf071, - 0xf08c, - 0xf153...0xf154, - 0xf158, - 0xf280, - 0xf2a5, - 0xf2bf, - 0xf2d0, - 0xf2d7, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8751322751322751, - .relative_y = 0.0624338624338624, - }, 0xf031...0xf033, => .{ .size = .fit_cover1, @@ -2075,25 +2123,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9989935587761675, .relative_y = 0.0004025764895330, }, - 0xf03d, - 0xf0a4...0xf0a5, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7502645502645503, - .relative_y = 0.1248677248677249, - }, - 0xf040, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9992749363119733, - .relative_y = 0.0001959631589261, - }, 0xf044, => .{ .size = .fit_cover1, @@ -2102,6 +2131,17 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_height = 0.9925925925925926, }, + 0xf046, + 0xf153...0xf154, + 0xf158, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, + }, 0xf048, 0xf04a, 0xf04e, @@ -2159,17 +2199,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_height = 0.9999748091795350, }, - 0xf05a...0xf05b, - 0xf0a2, - 0xf0aa, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9997176214776941, - .relative_y = 0.0002010014265405, - }, 0xf060...0xf061, => .{ .size = .fit_cover1, @@ -2206,15 +2235,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.5700483091787439, .relative_y = 0.1437198067632850, }, - 0xf07b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8139763779527559, - .relative_y = 0.0930118110236220, - }, 0xf07e, => .{ .size = .fit_cover1, @@ -2224,16 +2244,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.4989429175475687, .relative_y = 0.2505285412262157, }, - 0xf081, - 0xf092, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8748851565736010, - .relative_y = 0.0626172338785870, - }, 0xf089, => .{ .size = .fit_cover1, @@ -2243,40 +2253,14 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9998488512696494, .relative_y = 0.0001511487303507, }, - 0xf0a1, + 0xf0a4...0xf0a5, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.9303101594008066, - .relative_y = 0.0349409448818898, - }, - 0xf0a3, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9998551487695376, - }, - 0xf0ca, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8124689241215546, - .relative_y = 0.0938253501046103, - }, - 0xf0d6, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6978346456692913, - .relative_y = 0.1510826771653543, + .relative_height = 0.7502645502645503, + .relative_y = 0.1248677248677249, }, 0xf0d7, => .{ @@ -2336,18 +2320,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.4287439613526570, .relative_y = 0.5712560386473430, }, - 0xf0e7, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9998803756692248, - .relative_y = 0.0001196243307751, - }, 0xf100...0xf101, 0xf104...0xf105, - 0xf2c5, => .{ .size = .fit_cover1, .height = .icon, @@ -2493,15 +2467,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.6874767744332962, .relative_y = 0.1560043449675557, }, - 0xf246, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9997816982260961, - .relative_y = 0.0002148974577418, - }, 0xf255...0xf256, 0xf25a, => .{ @@ -2537,147 +2502,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_height = 0.9975006099019084, }, - 0xf25d, - 0xf26c, - 0xf277, - 0xf2b9, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9993050939633815, - .relative_y = 0.0004531995890990, - }, - 0xf26a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9686385884343465, - .relative_y = 0.0157864523536165, - }, - 0xf26d, - 0xf27e, - 0xf29e, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8745241404314460, - .relative_y = 0.0628436763550668, - }, - 0xf27f, - 0xf288, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9693121693121693, - .relative_y = 0.0153439153439153, - }, - 0xf287, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7661375661375661, - .relative_y = 0.1169312169312169, - }, - 0xf296, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9627792014248586, - .relative_y = 0.0187142907131644, - }, - 0xf2a8, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9723057045073505, - .relative_y = 0.0137673026561915, - }, - 0xf2ae, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8931764276591863, - .relative_y = 0.0534391534391534, - }, - 0xf2c9, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9228515625000000, - .relative_y = 0.0385742187500000, - }, - 0xf2cc, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8076171875000000, - .relative_y = 0.0961914062500000, - }, - 0xf2ce, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9722222222222222, - .relative_y = 0.0138888888888889, - }, - 0xf2cf, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9994939038417299, - .relative_y = 0.0005060961582701, - }, - 0xf2d1, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.0931216931216931, - .relative_y = 0.4534391534391534, - }, - 0xf2f0, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9669226518842459, - .relative_y = 0.0165984862232646, - }, - 0xf314, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5486681262543333, - .relative_y = 0.2256704980842912, - }, 0xf419, 0xf45a, => .{ diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index c7d592798..4b1a2b857 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -299,8 +299,11 @@ def generate_zig_switch_arms( entries: dict[int, PatchSetAttributeEntry] = {} for entry in patch_sets: - print(f"Info: Extracting rules from patch set '{entry['Name']}'") + patch_set_name = entry["Name"] + print(f"Info: Extracting rules from patch set '{patch_set_name}'") + attributes = entry["Attributes"] + patch_set_entries: dict[int, PatchSetAttributeEntry] = {} # A glyph's scale rules are specified using its codepoint in # the original font, which is sometimes different from its @@ -315,10 +318,28 @@ def generate_zig_switch_arms( if cp_font not in cmap: print(f"Info: Skipping missing codepoint {hex(cp_font)}") continue + elif cp_font in entries: + # Patch sets sometimes have overlapping codepoint ranges. + # Sometimes a later set is a smaller set filling in a gap + # in the range of a larger, preceding set. Sometimes it's + # the other way around. The best thing we can do is hardcode + # each case. + if patch_set_name == "Font Awesome": + # The Font Awesome range has a gap matching the + # prededing Progress Indicators range. + print(f"Info: Not overwriting existing codepoint {hex(cp_font)}") + continue + elif patch_set_name == "Octicons": + # The fourth Octicons range overlaps with the first. + print(f"Info: Overwriting existing codepoint {hex(cp_font)}") + else: + raise ValueError( + f"Unknown case of overlap for codepoint {hex(cp_font)} in patch set '{patch_set_name}'" + ) if cp_rule in attributes: - entries[cp_font] = attributes[cp_rule].copy() + patch_set_entries[cp_font] = attributes[cp_rule].copy() else: - entries[cp_font] = attributes["default"].copy() + patch_set_entries[cp_font] = attributes["default"].copy() if entry["ScaleRules"] is not None: if "ScaleGroups" not in entry["ScaleRules"]: @@ -354,28 +375,33 @@ def generate_zig_switch_arms( cp_font = cp_rule + cp_offset if ( cp_font not in cmap - or cp_font not in entries + or cp_font not in patch_set_entries # Codepoints may contribute to the bounding box of multiple groups, # but should be scaled according to the first group they are found # in. Hence, to avoid overwriting, we need to skip codepoints that # have already been assigned a scale group. - or "relative_height" in entries[cp_font] + or "relative_height" in patch_set_entries[cp_font] ): continue this_bounds = individual_bounds[cp_font] this_height = this_bounds[3] - this_bounds[1] - entries[cp_font]["relative_height"] = this_height / group_height - entries[cp_font]["relative_y"] = ( + patch_set_entries[cp_font]["relative_height"] = ( + this_height / group_height + ) + patch_set_entries[cp_font]["relative_y"] = ( this_bounds[1] - yMin ) / group_height # Horizontal alignment should only be grouped if the group is monospace, # that is, if all glyphs in the group have the same advance width. if group_is_monospace: this_width = this_bounds[2] - this_bounds[0] - entries[cp_font]["relative_width"] = this_width / group_width - entries[cp_font]["relative_x"] = ( + patch_set_entries[cp_font]["relative_width"] = ( + this_width / group_width + ) + patch_set_entries[cp_font]["relative_x"] = ( this_bounds[0] - xMin ) / group_width + entries |= patch_set_entries # Group codepoints by attribute key grouped = defaultdict[AttributeHash, list[int]](list) From 42a38ff672fe0cbbb8588380058c91ac16ed9069 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 14:12:03 -0700 Subject: [PATCH 161/319] snap: fix Zig 0.15 install --- snap/snapcraft.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2e434843c..271deeeb2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -58,9 +58,9 @@ parts: exit 1 fi - tar xf zig-lin*xz + tar xf zig-$arch-lin*xz rm -f *xz - mv zig-linux*/* . + mv zig-$arch-linux*/* . prime: - -* From e07415a2e212e9a12fb7bd6f6cbc0eb45c4c11b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Oct 2025 14:41:03 -0700 Subject: [PATCH 162/319] build: framegen can use self-hosted This was a red herring when I was doing the 0.15 port. It works with self-hosted just fine. --- src/build/GhosttyFrameData.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig index def1dbdb3..7193162bd 100644 --- a/src/build/GhosttyFrameData.zig +++ b/src/build/GhosttyFrameData.zig @@ -43,7 +43,6 @@ pub fn distResources(b: *std.Build) struct { .root_module = b.createModule(.{ .target = b.graph.host, }), - .use_llvm = true, }); exe.addCSourceFile(.{ .file = b.path("src/build/framegen/main.c"), From 5360aeb8aab98cbf921107432e90ed8d4817be17 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 3 Oct 2025 22:20:40 -0700 Subject: [PATCH 163/319] Fix botched cherry-pick from #8990 --- src/font/face/coretext.zig | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index bd1716a61..9e7bc4d5d 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -363,7 +363,11 @@ pub const Face = struct { // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. - if (metrics.face_width < cell_width) { + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { // We add half the difference to re-center. x += (cell_width - metrics.face_width) / 2; } @@ -378,18 +382,6 @@ pub const Face = struct { y = @round(y); } - // We center all glyphs within the pixel-rounded and adjusted - // cell width if it's larger than the face width, so that they - // aren't weirdly off to the left. - // - // We don't do this if the glyph has a stretch constraint, - // since in that case the position was already calculated with the - // new cell width in mind. - if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { - // We add half the difference to re-center. - x += (cell_width - metrics.face_width) / 2; - } - // We make an assumption that font smoothing ("thicken") // adds no more than 1 extra pixel to any edge. We don't // add extra size if it's a sbix color font though, since From 6bc60b6c643cf3036b553ce15b202d895d4b9308 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 3 Oct 2025 21:51:25 -0700 Subject: [PATCH 164/319] Add comprehensive constraint tests --- src/font/face.zig | 193 +++++++++++++++++++++++++++++++++++++ src/font/face/freetype.zig | 37 ------- 2 files changed, 193 insertions(+), 37 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index f660565fe..586de4ad6 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -507,3 +507,196 @@ test "Variation.Id: slnt should be 1936486004" { try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id))); try testing.expectEqualStrings("slnt", &(id.str())); } + +test "Constraints" { + const comparison = @import("../datastruct/comparison.zig"); + const getConstraint = @import("nerd_font_attributes.zig").getConstraint; + + // Hardcoded data matches metrics from CoreText at size 12 and DPI 96. + + // Define grid metrics (matches font-family = JetBrains Mono) + const metrics: Metrics = .{ + .cell_width = 10, + .cell_height = 22, + .cell_baseline = 5, + .underline_position = 19, + .underline_thickness = 1, + .strikethrough_position = 12, + .strikethrough_thickness = 1, + .overline_position = 0, + .overline_thickness = 1, + .box_thickness = 1, + .cursor_thickness = 1, + .cursor_height = 22, + .icon_height = 44.48 / 3.0, + .face_width = 9.6, + .face_height = 21.12, + .face_y = 0.2, + }; + + // ASCII (no constraint). + { + const constraint: RenderOptions.Constraint = .none; + + // BBox of 'x' from JetBrains Mono. + const glyph_x: GlyphSize = .{ + .width = 6.784, + .height = 15.28, + .x = 1.408, + .y = 4.84, + }; + + // Any constraint width: do nothing. + inline for (.{ 1, 2 }) |constraint_width| { + try comparison.expectApproxEqual( + glyph_x, + constraint.constrain(glyph_x, metrics, constraint_width), + ); + } + } + + // Symbol (same constraint as hardcoded in Renderer.addGlyph). + { + const constraint: RenderOptions.Constraint = .{ .size = .fit }; + + // BBox of '■' (0x25A0 black square) from Iosevka. + // NOTE: This glyph is designed to span two cells. + const glyph_25A0: GlyphSize = .{ + .width = 10.272, + .height = 10.272, + .x = 2.864, + .y = 5.304, + }; + + // Constraint width 1: scale down and shift to fit a single cell. + try comparison.expectApproxEqual( + GlyphSize{ + .width = metrics.face_width, + .height = metrics.face_width, + .x = 0, + .y = 5.64, + }, + constraint.constrain(glyph_25A0, metrics, 1), + ); + + // Constraint width 2: do nothing. + try comparison.expectApproxEqual( + glyph_25A0, + constraint.constrain(glyph_25A0, metrics, 2), + ); + } + + // Emoji (same constraint as hardcoded in SharedGrid.renderGlyph). + { + const constraint: RenderOptions.Constraint = .{ + .size = .cover, + .align_horizontal = .center, + .align_vertical = .center, + .pad_left = 0.025, + .pad_right = 0.025, + }; + + // BBox of '🥸' (0x1F978) from Apple Color Emoji. + const glyph_1F978: GlyphSize = .{ + .width = 20, + .height = 20, + .x = 0.46, + .y = 1, + }; + + // Constraint width 2: scale to cover two cells with padding, center; + try comparison.expectApproxEqual( + GlyphSize{ + .width = 18.72, + .height = 18.72, + .x = 0.44, + .y = 1.4, + }, + constraint.constrain(glyph_1F978, metrics, 2), + ); + } + + // Nerd Font default. + { + const constraint = getConstraint(0xea61).?; + + // Verify that this is the constraint we expect. + try std.testing.expectEqual(.fit_cover1, constraint.size); + try std.testing.expectEqual(.icon, constraint.height); + try std.testing.expectEqual(.center1, constraint.align_horizontal); + try std.testing.expectEqual(.center1, constraint.align_vertical); + + // BBox of '' (0xEA61 nf-cod-lightbulb) from Symbols Only. + // NOTE: This icon is part of a group, so the + // constraint applies to a larger bounding box. + const glyph_EA61: GlyphSize = .{ + .width = 9.015625, + .height = 13.015625, + .x = 3.015625, + .y = 3.76525, + }; + + // Constraint width 1: scale and shift group to fit a single cell. + try comparison.expectApproxEqual( + GlyphSize{ + .width = 7.2125, + .height = 10.4125, + .x = 0.8125, + .y = 5.950695224719102, + }, + constraint.constrain(glyph_EA61, metrics, 1), + ); + + // Constraint width 2: no scaling; left-align and vertically center group. + try comparison.expectApproxEqual( + GlyphSize{ + .width = glyph_EA61.width, + .height = glyph_EA61.height, + .x = 1.015625, + .y = 4.7483690308988775, + }, + constraint.constrain(glyph_EA61, metrics, 2), + ); + } + + // Nerd Font stretch. + { + const constraint = getConstraint(0xe0c0).?; + + // Verify that this is the constraint we expect. + try std.testing.expectEqual(.stretch, constraint.size); + try std.testing.expectEqual(.cell, constraint.height); + try std.testing.expectEqual(.start, constraint.align_horizontal); + try std.testing.expectEqual(.center1, constraint.align_vertical); + + // BBox of ' ' (0xE0C0 nf-ple-flame_thick) from Symbols Only. + const glyph_E0C0: GlyphSize = .{ + .width = 16.796875, + .height = 16.46875, + .x = -0.796875, + .y = 1.7109375, + }; + + // Constraint width 1: stretch and position to exactly cover one cell. + try comparison.expectApproxEqual( + GlyphSize{ + .width = @floatFromInt(metrics.cell_width), + .height = @floatFromInt(metrics.cell_height), + .x = 0, + .y = 0, + }, + constraint.constrain(glyph_E0C0, metrics, 1), + ); + + // Constraint width 1: stretch and position to exactly cover two cells. + try comparison.expectApproxEqual( + GlyphSize{ + .width = @floatFromInt(2 * metrics.cell_width), + .height = @floatFromInt(metrics.cell_height), + .x = 0, + .y = 0, + }, + constraint.constrain(glyph_E0C0, metrics, 2), + ); + } +} diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 259e91b8c..4958c48c8 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -1177,43 +1177,6 @@ test "color emoji" { const glyph_id = ft_font.glyphIndex('🥸').?; try testing.expect(ft_font.isColorGlyph(glyph_id)); } - - // resize - // TODO: Comprehensive tests for constraints, - // this is just an adapted legacy test. - { - const glyph = try ft_font.renderGlyph( - alloc, - &atlas, - ft_font.glyphIndex('🥸').?, - .{ - .grid_metrics = .{ - .cell_width = 13, - .cell_height = 24, - .cell_baseline = 0, - .underline_position = 0, - .underline_thickness = 0, - .strikethrough_position = 0, - .strikethrough_thickness = 0, - .overline_position = 0, - .overline_thickness = 0, - .box_thickness = 0, - .cursor_height = 0, - .icon_height = 0, - .face_width = 13, - .face_height = 24, - .face_y = 0, - }, - .constraint_width = 2, - .constraint = .{ - .size = .fit, - .align_horizontal = .center, - .align_vertical = .center, - }, - }, - ); - try testing.expectEqual(@as(u32, 24), glyph.height); - } } test "mono to bgra" { From 0b14026696e0bf54d5dba8940d8ae97201ddab27 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 4 Oct 2025 00:04:19 -0700 Subject: [PATCH 165/319] Expand `~` in `macos-custom-icon` --- macos/Sources/Ghostty/Ghostty.Config.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 05a3be2cd..0d75922cb 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -314,17 +314,14 @@ extension Ghostty { var macosCustomIcon: String { #if os(macOS) - let homeDirURL = FileManager.default.homeDirectoryForCurrentUser - let ghosttyConfigIconPath = homeDirURL.appendingPathComponent( - ".config/ghostty/Ghostty.icns", - conformingTo: .fileURL).path() - let defaultValue = ghosttyConfigIconPath + let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath guard let config = self.config else { return defaultValue } var v: UnsafePointer? = nil let key = "macos-custom-icon" guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } guard let ptr = v else { return defaultValue } - return String(cString: ptr) + guard let path = NSString(utf8String: ptr) else { return defaultValue } + return path.expandingTildeInPath #else return "" #endif From 3620132dfc449b95270703c386556278f26fa7f0 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 4 Oct 2025 00:06:41 -0700 Subject: [PATCH 166/319] Remove incorrect note from config docs --- src/config/Config.zig | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index caaf5feb8..bd2fe1cfb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2957,9 +2957,6 @@ keybind: Keybinds = .{}, /// Supported formats include PNG, JPEG, and ICNS. /// /// Defaults to `~/.config/ghostty/Ghostty.icns` -/// -/// Note: This configuration is required when `macos-icon` is set to -/// `custom` @"macos-custom-icon": ?[:0]const u8 = null, /// The material to use for the frame of the macOS app icon. From d4dcecb071a113f5a27ab2948c721384210114fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Oct 2025 13:24:34 -0700 Subject: [PATCH 167/319] Move paste encoding to the input package, test, optimize away one alloc This moves our paste logic to `src/input` in preparation for exposing this as part of libghostty-vt. This yields an immediate benefit of unit tests for paste encoding. Additionally, we were able to remove one allocation on every unbracketed paste path unless the input specifically contains a newline. Unlikely to be noticable, but nice. NOTE: This also includes one change in behavior: we no longer encode `\r\n` and a single `\r`, but as a duplicate `\r\r`. This matches xterm behavior and I don't think will result in any issues since duplicate carriage returns should do nothing in well-behaved terminals. --- src/Surface.zig | 76 +++++++------------- src/input.zig | 1 + src/input/paste.zig | 146 ++++++++++++++++++++++++++++++++++++++ src/lib_vt.zig | 13 ++++ src/terminal/main.zig | 3 - src/terminal/modes.zig | 2 +- src/terminal/sanitize.zig | 14 ---- 7 files changed, 186 insertions(+), 69 deletions(-) create mode 100644 src/input/paste.zig delete mode 100644 src/terminal/sanitize.zig diff --git a/src/Surface.zig b/src/Surface.zig index 018c4206b..5aabb2b80 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5270,13 +5270,10 @@ fn completeClipboardPaste( ) !void { if (data.len == 0) return; - const critical: struct { - bracketed: bool, - } = critical: { + const encode_opts: input.paste.Options = encode_opts: { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - - const bracketed = self.io.terminal.modes.get(.bracketed_paste); + const opts: input.paste.Options = .fromTerminal(&self.io.terminal); // If we have paste protection enabled, we detect unsafe pastes and return // an error. The error approach allows apprt to attempt to complete the paste @@ -5292,7 +5289,7 @@ fn completeClipboardPaste( // This is set during confirmation usually. if (allow_unsafe) break :unsafe false; - if (bracketed) { + if (opts.bracketed) { // If we're bracketed and the paste contains and ending // bracket then something naughty might be going on and we // never trust it. @@ -5303,7 +5300,7 @@ fn completeClipboardPaste( if (self.config.clipboard_paste_bracketed_safe) break :unsafe false; } - break :unsafe !terminal.isSafePaste(data); + break :unsafe !input.paste.isSafe(data); }; if (unsafe) { @@ -5317,55 +5314,32 @@ fn completeClipboardPaste( log.warn("error scrolling to bottom err={}", .{err}); }; - break :critical .{ - .bracketed = bracketed, - }; + break :encode_opts opts; }; - if (critical.bracketed) { - // If we're bracketd we write the data as-is to the terminal with - // the bracketed paste escape codes around it. - self.io.queueMessage(.{ - .write_stable = "\x1B[200~", - }, .unlocked); + // Encode the data. In most cases this doesn't require any + // copies, so we optimize for that case. + var data_duped: ?[]u8 = null; + const vecs = input.paste.encode(data, encode_opts) catch |err| switch (err) { + error.MutableRequired => vecs: { + const buf: []u8 = try self.alloc.dupe(u8, data); + errdefer self.alloc.free(buf); + data_duped = buf; + break :vecs input.paste.encode(buf, encode_opts); + }, + }; + defer if (data_duped) |v| { + // This code path means the data did require a copy and mutation. + // We must free it. + self.alloc.free(v); + }; + + for (vecs) |vec| if (vec.len > 0) { self.io.queueMessage(try termio.Message.writeReq( self.alloc, - data, + vec, ), .unlocked); - self.io.queueMessage(.{ - .write_stable = "\x1B[201~", - }, .unlocked); - } else { - // If its not bracketed the input bytes are indistinguishable from - // keystrokes, so we must be careful. For example, we must replace - // any newlines with '\r'. - - // We just do a heap allocation here because its easy and I don't think - // worth the optimization of using small messages. - var buf = try self.alloc.alloc(u8, data.len); - defer self.alloc.free(buf); - - // This is super, super suboptimal. We can easily make use of SIMD - // here, but maybe LLVM in release mode is smart enough to figure - // out something clever. Either way, large non-bracketed pastes are - // increasingly rare for modern applications. - var len: usize = 0; - for (data, 0..) |ch, i| { - const dch = switch (ch) { - '\n' => '\r', - '\r' => if (i + 1 < data.len and data[i + 1] == '\n') continue else ch, - else => ch, - }; - - buf[len] = dch; - len += 1; - } - - self.io.queueMessage(try termio.Message.writeReq( - self.alloc, - buf[0..len], - ), .unlocked); - } + }; } fn completeClipboardReadOSC52( diff --git a/src/input.zig b/src/input.zig index caaf80509..06d7dc96a 100644 --- a/src/input.zig +++ b/src/input.zig @@ -9,6 +9,7 @@ pub const command = @import("input/command.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); pub const kitty = @import("input/kitty.zig"); +pub const paste = @import("input/paste.zig"); pub const ctrlOrSuper = key.ctrlOrSuper; pub const Action = key.Action; diff --git a/src/input/paste.zig b/src/input/paste.zig new file mode 100644 index 000000000..29787c385 --- /dev/null +++ b/src/input/paste.zig @@ -0,0 +1,146 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Terminal = @import("../terminal/Terminal.zig"); + +pub const Options = struct { + /// True if bracketed paste mode is on. + bracketed: bool, + + /// Return the encoding options based on the current terminal state. + pub fn fromTerminal(t: *const Terminal) Options { + return .{ + .bracketed = t.modes.get(.bracketed_paste), + }; + } +}; + +/// Encode the given data for pasting. The resulting value can be written +/// to the pty to perform a paste of the input data. +/// +/// The data can be either a `[]u8` or a `[]const u8`. If the data +/// type is const then `EncodeError` may be returned. If the data type +/// is mutable then this function can't return an error. +/// +/// This slightly complex calling style allows for initially const +/// data to be passed in without an allocation, since it is rare in normal +/// use cases that the data will need to be modified. In the unlikely case +/// data does need to be modified, the caller can make a mutable copy +/// after seeing the error. +/// +/// The data is returned as a set of slices to limit allocations. The caller +/// can combine the slices into a single buffer if desired. +/// +/// WARNING: The input data is not checked for safety. See the `isSafe` +/// function to check if the data is safe to paste. +pub fn encode( + data: anytype, + opts: Options, +) switch (@TypeOf(data)) { + []u8 => [3][]const u8, + []const u8 => Error![3][]const u8, + else => unreachable, +} { + const mutable = @TypeOf(data) == []u8; + + var result: [3][]const u8 = .{ "", data, "" }; + + // Bracketed paste mode (mode 2004) wraps pasted data in + // fenceposts so that the terminal can ignore things like newlines. + if (opts.bracketed) { + result[0] = "\x1b[200~"; + result[2] = "\x1b[201~"; + return result; + } + + // Non-bracketed. We have to replace newline with `\r`. This matches + // the behavior of xterm and other terminals. For `\r\n` this will + // result in `\r\r` which does match xterm. + if (comptime mutable) { + std.mem.replaceScalar(u8, data, '\n', '\r'); + } else if (std.mem.indexOfScalar(u8, data, '\n') != null) { + return Error.MutableRequired; + } + + return result; +} + +pub const Error = error{ + /// Returned if encoding requires a mutable copy of the data. This + /// can only be returned if the input data type is const. + MutableRequired, +}; + +/// Returns true if the data looks safe to paste. Data is considered +/// unsafe if it contains any of the following: +/// +/// - `\n`: Newlines can be used to inject commands. +/// - `\x1b[201~`: This is the end of a bracketed paste. This cane be used +/// to exit a bracketed paste and inject commands. +/// +/// We consider any scenario unsafe regardless of current terminal state. +/// For example, even if bracketed paste mode is not active, we still +/// consider `\x1b[201~` unsafe. The existence of these types of bytes +/// should raise suspicion that the producer of the paste data is +/// acting strangely. +pub fn isSafe(data: []const u8) bool { + return std.mem.indexOf(u8, data, "\n") == null and + std.mem.indexOf(u8, data, "\x1b[201~") == null; +} + +test isSafe { + const testing = std.testing; + try testing.expect(isSafe("hello")); + try testing.expect(!isSafe("hello\n")); + try testing.expect(!isSafe("hello\nworld")); + try testing.expect(!isSafe("he\x1b[201~llo")); +} + +test "encode bracketed" { + const testing = std.testing; + const result = try encode( + @as([]const u8, "hello"), + .{ .bracketed = true }, + ); + try testing.expectEqualStrings("\x1b[200~", result[0]); + try testing.expectEqualStrings("hello", result[1]); + try testing.expectEqualStrings("\x1b[201~", result[2]); +} + +test "encode unbracketed no newlines" { + const testing = std.testing; + const result = try encode( + @as([]const u8, "hello"), + .{ .bracketed = false }, + ); + try testing.expectEqualStrings("", result[0]); + try testing.expectEqualStrings("hello", result[1]); + try testing.expectEqualStrings("", result[2]); +} + +test "encode unbracketed newlines const" { + const testing = std.testing; + try testing.expectError(Error.MutableRequired, encode( + @as([]const u8, "hello\nworld"), + .{ .bracketed = false }, + )); +} + +test "encode unbracketed newlines" { + const testing = std.testing; + const data: []u8 = try testing.allocator.dupe(u8, "hello\nworld"); + defer testing.allocator.free(data); + const result = encode(data, .{ .bracketed = false }); + try testing.expectEqualStrings("", result[0]); + try testing.expectEqualStrings("hello\rworld", result[1]); + try testing.expectEqualStrings("", result[2]); +} + +test "encode unbracketed windows-stye newline" { + const testing = std.testing; + const data: []u8 = try testing.allocator.dupe(u8, "hello\r\nworld"); + defer testing.allocator.free(data); + const result = encode(data, .{ .bracketed = false }); + try testing.expectEqualStrings("", result[0]); + try testing.expectEqualStrings("hello\r\rworld", result[1]); + try testing.expectEqualStrings("", result[2]); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 8c49b4900..4b064dc0d 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -66,6 +66,18 @@ pub const EraseLine = terminal.EraseLine; pub const TabClear = terminal.TabClear; pub const Attribute = terminal.Attribute; +/// Terminal-specific input encoding is also part of libghostty-vt. +pub const input = struct { + // We have to be careful to only import targeted files within + // the input package because the full package brings in too many + // other dependencies. + const paste = @import("input/paste.zig"); + pub const PasteError = paste.Error; + pub const PasteOptions = paste.Options; + pub const isSafePaste = paste.isSafe; + pub const encodePaste = paste.encode; +}; + comptime { // If we're building the C library (vs. the Zig module) then // we want to reference the C API so that it gets exported. @@ -84,6 +96,7 @@ comptime { test { _ = terminal; _ = @import("lib/main.zig"); + @import("std").testing.refAllDecls(input); if (comptime terminal.options.c_abi) { _ = terminal.c_api; } diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 6875ba89d..4498a5def 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -1,7 +1,6 @@ const builtin = @import("builtin"); const charsets = @import("charsets.zig"); -const sanitize = @import("sanitize.zig"); const stream = @import("stream.zig"); const ansi = @import("ansi.zig"); const csi = @import("csi.zig"); @@ -59,8 +58,6 @@ pub const EraseLine = csi.EraseLine; pub const TabClear = csi.TabClear; pub const Attribute = sgr.Attribute; -pub const isSafePaste = sanitize.isSafePaste; - pub const Options = @import("build_options.zig").Options; pub const options = @import("terminal_options"); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 9a74db73c..13b7c1eac 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -43,7 +43,7 @@ pub const ModeState = struct { } /// Get the value of a mode. - pub fn get(self: *ModeState, mode: Mode) bool { + pub fn get(self: *const ModeState, mode: Mode) bool { switch (mode) { inline else => |mode_comptime| { const entry = comptime entryForMode(mode_comptime); diff --git a/src/terminal/sanitize.zig b/src/terminal/sanitize.zig deleted file mode 100644 index f96e8a00e..000000000 --- a/src/terminal/sanitize.zig +++ /dev/null @@ -1,14 +0,0 @@ -const std = @import("std"); - -/// Returns true if the data looks safe to paste. -pub fn isSafePaste(data: []const u8) bool { - return std.mem.indexOf(u8, data, "\n") == null and - std.mem.indexOf(u8, data, "\x1b[201~") == null; -} - -test isSafePaste { - const testing = std.testing; - try testing.expect(isSafePaste("hello")); - try testing.expect(!isSafePaste("hello\n")); - try testing.expect(!isSafePaste("hello\nworld")); -} From cef99250a796c2898a9a197ef263a5697632d613 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 5 Oct 2025 00:16:06 +0000 Subject: [PATCH 168/319] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index e76c5e354..4039c6bdd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -116,8 +116,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", - .hash = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz", + .hash = "N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index ee374e695..ad9763f62 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv": { + "N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y": { "name": "iterm2_themes", - "url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", - "hash": "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz", + "hash": "sha256-GsEWVt4wMzp6+7N5I+QVuhCVJ70cFrdADwUds59AKnw=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index e9d2fc0bc..9d189cfdc 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv"; + name = "N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz"; - hash = "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz"; + hash = "sha256-GsEWVt4wMzp6+7N5I+QVuhCVJ70cFrdADwUds59AKnw="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 8ebecee9b..c3727f1e5 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -13,7 +13,6 @@ https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz -https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz @@ -35,4 +34,5 @@ https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 58ef3c97a..1eba46fc2 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", - "dest": "vendor/p/N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", - "sha256": "99d854c4002a2b14515f0103da569a6d4a3d659a996bf31902c3b4f98f4beee4" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y", + "sha256": "1ac11656de30333a7afbb37923e415ba109527bd1c16b7400f051db39f402a7c" }, { "type": "archive", From 44496df8994640975720938fb150a67e7d111663 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Oct 2025 15:04:52 -0700 Subject: [PATCH 169/319] input: use std.Io.Writer for key encoder, new API, expose via libghostty This modernizes `KeyEncoder` to a new `std.Io.Writer`-based API. Additionally, instead of a single struct, it is now an `encode` function that takes a series of more focused options. This is more idiomatic Zig while also making it easier to expose via libghostty-vt. libghostty-vt also gains access to key encoding APIs. --- src/Surface.zig | 99 +- src/config.zig | 1 - src/config/Config.zig | 10 +- src/input.zig | 4 +- src/input/config.zig | 8 + src/input/function_keys.zig | 5 + src/input/key.zig | 4 +- src/input/{KeyEncoder.zig => key_encode.zig} | 1611 +++++++++--------- src/input/keyboard.zig | 2 +- src/lib_vt.zig | 12 + src/terminal/kitty/key.zig | 15 +- 11 files changed, 880 insertions(+), 891 deletions(-) create mode 100644 src/input/config.zig rename src/input/{KeyEncoder.zig => key_encode.zig} (63%) diff --git a/src/Surface.zig b/src/Surface.zig index 5aabb2b80..b1553edff 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -271,7 +271,7 @@ const DerivedConfig = struct { mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, - macos_option_as_alt: ?configpkg.OptionAsAlt, + macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, selection_clear_on_typing: bool, vt_kam_allowed: bool, @@ -1130,7 +1130,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { // so that we can close the terminal. We close the terminal on // any key press that encodes a character. t.modes.set(.disable_keyboard, false); - t.screen.kitty_keyboard.set(.set, .{}); + t.screen.kitty_keyboard.set(.set, .disabled); } // Waiting after command we stop here. The terminal is updated, our @@ -2611,56 +2611,32 @@ fn encodeKey( event: input.KeyEvent, insp_ev: ?*inspectorpkg.key.Event, ) !?termio.Message.WriteReq { - // Build up our encoder. Under different modes and - // inputs there are many keybindings that result in no encoding - // whatsoever. - const enc: input.KeyEncoder = enc: { - const option_as_alt: configpkg.OptionAsAlt = self.config.macos_option_as_alt orelse detect: { - // Non-macOS doesn't use this value so ignore. - if (comptime builtin.os.tag != .macos) break :detect .false; - - // If we don't have alt pressed, it doesn't matter what this - // config is so we can just say "false" and break out and avoid - // more expensive checks below. - if (!event.mods.alt) break :detect .false; - - // Alt is pressed, we're on macOS. We break some encapsulation - // here and assume libghostty for ease... - break :detect self.rt_app.keyboardLayout().detectOptionAsAlt(); - }; - - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - const t = &self.io.terminal; - break :enc .{ - .event = event, - .macos_option_as_alt = option_as_alt, - .alt_esc_prefix = t.modes.get(.alt_esc_prefix), - .cursor_key_application = t.modes.get(.cursor_keys), - .keypad_key_application = t.modes.get(.keypad_keys), - .ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock), - .modify_other_keys_state_2 = t.flags.modify_other_keys_2, - .kitty_flags = t.screen.kitty_keyboard.current(), - }; - }; - const write_req: termio.Message.WriteReq = req: { + // Build our encoding options, which requires the lock. + const encoding_opts = self.encodeKeyOpts(); + // Try to write the input into a small array. This fits almost // every scenario. Larger situations can happen due to long // pre-edits. var data: termio.Message.WriteReq.Small.Array = undefined; - if (enc.encode(&data)) |seq| { + var writer: std.Io.Writer = .fixed(&data); + if (input.key_encode.encode( + &writer, + event, + encoding_opts, + )) { + const written = writer.buffered(); + // Special-case: we did nothing. - if (seq.len == 0) return null; + if (written.len == 0) return null; break :req .{ .small = .{ .data = data, - .len = @intCast(seq.len), + .len = @intCast(written.len), } }; } else |err| switch (err) { // Means we need to allocate - error.OutOfMemory => {}, - else => return err, + error.WriteFailed => {}, } // We need to allocate. We allocate double the UTF-8 length @@ -2669,16 +2645,23 @@ fn encodeKey( // typing this where we don't have enough space is a long preedit, // and in that case the size we need is exactly the UTF-8 length, // so the double is being safe. - const buf = try self.alloc.alloc(u8, @max( - event.utf8.len * 2, - data.len * 2, - )); - defer self.alloc.free(buf); + var alloc_writer: std.Io.Writer.Allocating = try .initCapacity( + self.alloc, + @max(event.utf8.len * 2, data.len * 2), + ); + defer alloc_writer.deinit(); // This results in a double allocation but this is such an unlikely // path the performance impact is unimportant. - const seq = try enc.encode(buf); - break :req try termio.Message.WriteReq.init(self.alloc, seq); + try input.key_encode.encode( + &alloc_writer.writer, + event, + encoding_opts, + ); + break :req try termio.Message.WriteReq.init( + self.alloc, + alloc_writer.writer.buffered(), + ); }; // Copy the encoded data into the inspector event if we have one. @@ -2698,6 +2681,28 @@ fn encodeKey( return write_req; } +fn encodeKeyOpts(self: *const Surface) input.key_encode.Options { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t = &self.io.terminal; + + var opts: input.key_encode.Options = .fromTerminal(t); + if (comptime builtin.os.tag != .macos) return opts; + + opts.macos_option_as_alt = self.config.macos_option_as_alt orelse detect: { + // If we don't have alt pressed, it doesn't matter what this + // config is so we can just say "false" and break out and avoid + // more expensive checks below. + if (!self.mouse.mods.alt) break :detect .false; + + // Alt is pressed, we're on macOS. We break some encapsulation + // here and assume libghostty for ease... + break :detect self.rt_app.keyboardLayout().detectOptionAsAlt(); + }; + + return opts; +} + /// Sends text as-is to the terminal without triggering any keyboard /// protocol. This will treat the input text as if it was pasted /// from the clipboard so the same logic will be applied. Namely, diff --git a/src/config.zig b/src/config.zig index 569d4bec2..a596eb5e6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -29,7 +29,6 @@ pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; -pub const OptionAsAlt = Config.OptionAsAlt; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; diff --git a/src/config/Config.zig b/src/config/Config.zig index bd2fe1cfb..a203a32a1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2861,7 +2861,7 @@ keybind: Keybinds = .{}, /// /// The values `left` or `right` enable this for the left or right *Option* /// key, respectively. -@"macos-option-as-alt": ?OptionAsAlt = null, +@"macos-option-as-alt": ?inputpkg.OptionAsAlt = null, /// Whether to enable the macOS window shadow. The default value is true. /// With some window managers and window transparency settings, you may @@ -4821,14 +4821,6 @@ pub const NonNativeFullscreen = enum(c_int) { @"padded-notch", }; -/// Valid values for macos-option-as-alt. -pub const OptionAsAlt = enum { - false, - true, - left, - right, -}; - pub const WindowPaddingColor = enum { background, extend, diff --git a/src/input.zig b/src/input.zig index 06d7dc96a..be84a60d6 100644 --- a/src/input.zig +++ b/src/input.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const config = @import("input/config.zig"); const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); const keyboard = @import("input/keyboard.zig"); @@ -8,6 +9,7 @@ const keyboard = @import("input/keyboard.zig"); pub const command = @import("input/command.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); +pub const key_encode = @import("input/key_encode.zig"); pub const kitty = @import("input/kitty.zig"); pub const paste = @import("input/paste.zig"); @@ -18,13 +20,13 @@ pub const Command = command.Command; pub const Link = @import("input/Link.zig"); pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; -pub const KeyEncoder = @import("input/KeyEncoder.zig"); pub const KeyEvent = key.KeyEvent; pub const InspectorMode = Binding.Action.InspectorMode; pub const Mods = key.Mods; pub const MouseButton = mouse.Button; pub const MouseButtonState = mouse.ButtonState; pub const MousePressureStage = mouse.PressureStage; +pub const OptionAsAlt = config.OptionAsAlt; pub const ScrollMods = mouse.ScrollMods; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; diff --git a/src/input/config.zig b/src/input/config.zig new file mode 100644 index 000000000..fd839a20e --- /dev/null +++ b/src/input/config.zig @@ -0,0 +1,8 @@ +/// Determines the macOS option key behavior. See the config +/// `macos-option-as-alt` for a lot more details. +pub const OptionAsAlt = enum(c_int) { + false, + true, + left, + right, +}; diff --git a/src/input/function_keys.zig b/src/input/function_keys.zig index 8c89b39bd..efe86d9e3 100644 --- a/src/input/function_keys.zig +++ b/src/input/function_keys.zig @@ -293,6 +293,11 @@ fn pcStyle(comptime fmt: []const u8) []Entry { test "keys" { const testing = std.testing; + switch (@import("terminal_options").artifact) { + .ghostty => {}, + // Don't want to bring in termio into libghostty-vt + .lib => return error.SkipZigTest, + } // Force resolution for comptime evaluation. _ = keys; diff --git a/src/input/key.zig b/src/input/key.zig index a3814fb55..54c7491ae 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const cimgui = @import("cimgui"); -const config = @import("../config.zig"); +const OptionAsAlt = @import("config.zig").OptionAsAlt; /// A generic key input event. This is the information that is necessary /// regardless of apprt in order to generate the proper terminal @@ -146,7 +146,7 @@ pub const Mods = packed struct(Mods.Backing) { /// Return the mods to use for key translation. This handles settings /// like macos-option-as-alt. The translation mods should be used for /// translation but never sent back in for the key callback. - pub fn translation(self: Mods, option_as_alt: config.OptionAsAlt) Mods { + pub fn translation(self: Mods, option_as_alt: OptionAsAlt) Mods { var result = self; // macos-option-as-alt for darwin diff --git a/src/input/KeyEncoder.zig b/src/input/key_encode.zig similarity index 63% rename from src/input/KeyEncoder.zig rename to src/input/key_encode.zig index b5f18b5a2..c35cdebaa 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/key_encode.zig @@ -1,86 +1,128 @@ -/// KeyEncoder is responsible for processing keyboard input and generating -/// the proper VT sequence for any events. -/// -/// A new KeyEncoder should be created for each individual key press. -/// These encoders are not meant to be reused. -const KeyEncoder = @This(); - const std = @import("std"); const builtin = @import("builtin"); const testing = std.testing; - -const key = @import("key.zig"); -const config = @import("../config.zig"); +const KittyFlags = @import("../terminal/kitty/key.zig").Flags; +const OptionAsAlt = @import("config.zig").OptionAsAlt; +const Terminal = @import("../terminal/Terminal.zig"); const function_keys = @import("function_keys.zig"); -const terminal = @import("../terminal/main.zig"); +const key = @import("key.zig"); const KittyEntry = @import("kitty.zig").Entry; const kitty_entries = @import("kitty.zig").entries; -const KittyFlags = terminal.kitty.KeyFlags; -const log = std.log.scoped(.key_encoder); +/// Options that affect key encoding behavior. This is a mix of behavior +/// from terminal state as well as application configuration. +pub const Options = struct { + /// Terminal DEC mode 1 + cursor_key_application: bool = false, -event: key.KeyEvent, + /// Terminal DEC mode 66 + keypad_key_application: bool = false, -/// The state of various modes of a terminal that impact encoding. -macos_option_as_alt: config.OptionAsAlt = .false, -alt_esc_prefix: bool = false, -cursor_key_application: bool = false, -keypad_key_application: bool = false, -ignore_keypad_with_numlock: bool = false, -modify_other_keys_state_2: bool = false, -kitty_flags: KittyFlags = .{}, + /// Terminal DEC mode 1035 + ignore_keypad_with_numlock: bool = false, -/// Perform the proper encoding depending on the terminal state. + /// Terminal DEC mode 1036 + alt_esc_prefix: bool = false, + + /// xterm "modifyOtherKeys mode 2". Details here: + /// https://invisible-island.net/xterm/modified-keys.html + modify_other_keys_state_2: bool = false, + + /// Kitty keyboard protocol flags. + kitty_flags: KittyFlags = .disabled, + + /// Determines whether the "option" key on macOS is treated + /// as "alt" or not. See the Ghostty `macos_option-as-alt` config + /// docs for a more detailed description of why this is needed. + macos_option_as_alt: OptionAsAlt = .false, + + /// Initialize our options from the terminal state. + /// + /// Note that `macos_option_as_alt` cannot be determined from + /// terminal state so it must be set manually after this call. + pub fn fromTerminal(t: *const Terminal) Options { + return .{ + .alt_esc_prefix = t.modes.get(.alt_esc_prefix), + .cursor_key_application = t.modes.get(.cursor_keys), + .keypad_key_application = t.modes.get(.keypad_keys), + .ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock), + .modify_other_keys_state_2 = t.flags.modify_other_keys_2, + .kitty_flags = t.screen.kitty_keyboard.current(), + + // These can't be known from the terminal state. + .macos_option_as_alt = .false, + }; + } +}; + +/// Encode the key event to the writer in the proper format given +/// the options. For example, this will properly encode a key press +/// such as "ctrl+A" to Kitty format if Kitty encoding is enabled. +/// +/// Not all key events will result in output. It is up to the caller +/// to use a writer that can track whether any output was written if +/// they care about that. pub fn encode( - self: *const KeyEncoder, - buf: []u8, -) ![]const u8 { + writer: *std.Io.Writer, + event: key.KeyEvent, + opts: Options, +) std.Io.Writer.Error!void { // log.warn("KEYENCODER self={}", .{self.*}); - if (self.kitty_flags.int() != 0) return try self.kitty(buf); - return try self.legacy(buf); + return if (opts.kitty_flags.int() != 0) try kitty( + writer, + event, + opts, + ) else try legacy( + writer, + event, + opts, + ); } /// Perform Kitty keyboard protocol encoding of the key event. fn kitty( - self: *const KeyEncoder, - buf: []u8, -) ![]const u8 { + writer: *std.Io.Writer, + event: key.KeyEvent, + opts: Options, +) std.Io.Writer.Error!void { // This should never happen but we'll check anyway. - if (self.kitty_flags.int() == 0) return try self.legacy(buf); + if (opts.kitty_flags.int() == 0) return try legacy( + writer, + event, + opts, + ); // We only processed "press" events unless report events is active - if (self.event.action == .release) { - if (!self.kitty_flags.report_events) { - return ""; - } + if (event.action == .release) { + if (!opts.kitty_flags.report_events) return; // Enter, backspace, and tab do not report release events unless "report // all" is set - if (!self.kitty_flags.report_all) { - switch (self.event.key) { - .enter, .backspace, .tab => return "", + if (!opts.kitty_flags.report_all) { + switch (event.key) { + .enter, .backspace, .tab => return, else => {}, } } } - const all_mods = self.event.mods; - const effective_mods = self.event.effectiveMods(); + const all_mods = event.mods; + const effective_mods = event.effectiveMods(); const binding_mods = effective_mods.binding(); // Find the entry for this key in the kitty table. const entry_: ?KittyEntry = entry: { // Functional or predefined keys for (kitty_entries) |entry| { - if (entry.key == self.event.key) break :entry entry; + if (entry.key == event.key) break :entry entry; } // Otherwise, we use our unicode codepoint from UTF8. We // always use the unshifted value. - if (self.event.unshifted_codepoint > 0) { + if (event.unshifted_codepoint > 0) { break :entry .{ - .key = self.event.key, - .code = self.event.unshifted_codepoint, + .key = event.key, + .code = event.unshifted_codepoint, .final = 'u', .modifier = false, }; @@ -91,32 +133,32 @@ fn kitty( preprocessing: { // When composing, the only keys sent are plain modifiers. - if (self.event.composing) { + if (event.composing) { if (entry_) |entry| { if (entry.modifier) break :preprocessing; } - return ""; + return; } // IME confirmation still sends an enter key so if we have enter // and UTF8 text we just send it directly since we assume that is // whats happening. See legacy()'s similar logic for more details // on how to verify this. - if (self.event.utf8.len > 0) utf8: { - switch (self.event.key) { + if (event.utf8.len > 0) utf8: { + switch (event.key) { else => {}, inline .enter, .backspace => |tag| { // See legacy for why we handle this this way. - if (isControlUtf8(self.event.utf8)) break :utf8; - if (comptime tag == .backspace) return ""; - return try copyToBuf(buf, self.event.utf8); + if (isControlUtf8(event.utf8)) break :utf8; + if (comptime tag == .backspace) return; + return try writer.writeAll(event.utf8); }, } } // If we're reporting all then we always send CSI sequences. - if (!self.kitty_flags.report_all) { + if (!opts.kitty_flags.report_all) { // Quote: // The only exceptions are the Enter, Tab and Backspace keys which // still generate the same bytes as in legacy mode this is to allow the @@ -127,63 +169,73 @@ fn kitty( // Note that all keys are reported as escape codes, including Enter, // Tab, Backspace etc. if (effective_mods.empty()) { - switch (self.event.key) { - .enter => return try copyToBuf(buf, "\r"), - .tab => return try copyToBuf(buf, "\t"), - .backspace => return try copyToBuf(buf, "\x7F"), + switch (event.key) { + .enter => return try writer.writeByte('\r'), + .tab => return try writer.writeByte('\t'), + .backspace => return try writer.writeByte(0x7F), else => {}, } } // Send plain-text non-modified text directly to the terminal. // We don't send release events because those are specially encoded. - if (self.event.utf8.len > 0 and + if (event.utf8.len > 0 and binding_mods.empty() and - self.event.action != .release) + event.action != .release) plain_text: { // We only do this for printable characters. We should // inspect the real unicode codepoint properties here but // the real world issue is usually control characters. - const view = try std.unicode.Utf8View.init(self.event.utf8); + const view = std.unicode.Utf8View.init(event.utf8) catch { + // Invalid UTF-8 so let's fallback to encoding the + // key press as if it didn't produce UTF-8 text. I'm + // not sure what should happen here according to the spec, + // since it doesn't specify this behavior. Presumably + // this is a caller bug. + break :plain_text; + }; var it = view.iterator(); while (it.nextCodepoint()) |cp| { if (isControl(cp)) break :plain_text; } - return try copyToBuf(buf, self.event.utf8); + return try writer.writeAll(event.utf8); } } } - const entry = entry_ orelse return ""; + const entry = entry_ orelse return; // If this is just a modifier we require "report all" to send the sequence. - if (entry.modifier and !self.kitty_flags.report_all) return ""; + if (entry.modifier and !opts.kitty_flags.report_all) return; const seq: KittySequence = seq: { var seq: KittySequence = .{ .key = entry.code, .final = entry.final, .mods = .fromInput( - self.event.action, - self.event.key, + event.action, + event.key, all_mods, ), }; - if (self.kitty_flags.report_events) { - seq.event = switch (self.event.action) { + if (opts.kitty_flags.report_events) { + seq.event = switch (event.action) { .press => .press, .release => .release, .repeat => .repeat, }; } - if (self.kitty_flags.report_alternates) alternates: { + if (opts.kitty_flags.report_alternates) alternates: { // Break early if this is a control key if (isControl(seq.key)) break :alternates; - const view = try std.unicode.Utf8View.init(self.event.utf8); + const view = std.unicode.Utf8View.init(event.utf8) catch { + // Assume invalid UTF-8 means no UTF-8. + break :alternates; + }; var it = view.iterator(); // If we have a codepoint in our UTF-8 sequence, then we can @@ -198,7 +250,7 @@ fn kitty( // Set the base layout key. We only report this if this codepoint // differs from our pressed key. - if (self.event.key.codepoint()) |base| { + if (event.key.codepoint()) |base| { if (base != seq.key and (cp1 != base and !has_cp2)) { @@ -208,20 +260,20 @@ fn kitty( } else { // No UTF-8 so we can't report a shifted key but we can still // report a base layout key. - if (self.event.key.codepoint()) |base| { + if (event.key.codepoint()) |base| { if (base != seq.key) seq.alternates[1] = base; } } } - if (self.kitty_flags.report_associated and + if (opts.kitty_flags.report_associated and seq.event != .release) associated: { // Determine if the Alt modifier should be treated as an actual // modifier (in which case it prevents associated text) or as // the macOS Option key, which does not prevent associated text. const alt_prevents_text = if (comptime builtin.os.tag == .macos) - switch (self.macos_option_as_alt) { + switch (opts.macos_option_as_alt) { .left => all_mods.sides.alt == .left, .right => all_mods.sides.alt == .right, .true => true, @@ -232,13 +284,13 @@ fn kitty( if (seq.mods.preventsText(alt_prevents_text)) break :associated; - seq.text = self.event.utf8; + seq.text = event.utf8; } break :seq seq; }; - return try seq.encode(buf); + return try seq.encode(writer); } /// Perform legacy encoding of the key event. "Legacy" in this case @@ -248,28 +300,28 @@ fn kitty( /// meant to be extensions that do not change any existing behavior /// and therefore safe to combine. fn legacy( - self: *const KeyEncoder, - buf: []u8, -) ![]const u8 { - const all_mods = self.event.mods; - const effective_mods = self.event.effectiveMods(); + writer: *std.Io.Writer, + event: key.KeyEvent, + opts: Options, +) std.Io.Writer.Error!void { + const all_mods = event.mods; + const effective_mods = event.effectiveMods(); const binding_mods = effective_mods.binding(); // Legacy encoding only does press/repeat - if (self.event.action != .press and - self.event.action != .repeat) return ""; + if (event.action != .press and event.action != .repeat) return; // If we're in a dead key state then we never emit a sequence. - if (self.event.composing) return ""; + if (event.composing) return; // If we match a PC style function key then that is our result. if (pcStyleFunctionKey( - self.event.key, + event.key, all_mods, - self.cursor_key_application, - self.keypad_key_application, - self.ignore_keypad_with_numlock, - self.modify_other_keys_state_2, + opts.cursor_key_application, + opts.keypad_key_application, + opts.ignore_keypad_with_numlock, + opts.modify_other_keys_state_2, )) |sequence| pc_style: { // If we have UTF-8 text, then we never emit PC style function // keys. Many function keys (escape, enter, backspace) have @@ -280,65 +332,68 @@ fn legacy( // - Korean: escape commits the dead key state // - Korean: backspace should delete a single preedit char // - if (self.event.utf8.len > 0) utf8: { - switch (self.event.key) { + if (event.utf8.len > 0) utf8: { + switch (event.key) { else => {}, inline .backspace, .enter, .escape => |tag| { // We want to ignore control characters. This is because // some apprts (macOS) will send control characters as // UTF-8 encodings and we handle that manually. - if (isControlUtf8(self.event.utf8)) break :utf8; + if (isControlUtf8(event.utf8)) break :utf8; // Backspace encodes nothing because we modified IME. // Enter/escape don't encode the PC-style encoding // because we want to encode committed text. - if (comptime tag == .backspace) return ""; + if (comptime tag == .backspace) return; break :pc_style; }, } } - return copyToBuf(buf, sequence); + return try writer.writeAll(sequence); } // If we match a control sequence, we output that directly. For // ctrlSeq we have to use all mods because we want it to only // match ctrl+. if (ctrlSeq( - self.event.key, - self.event.utf8, - self.event.unshifted_codepoint, + event.key, + event.utf8, + event.unshifted_codepoint, all_mods, )) |char| { // C0 sequences support alt-as-esc prefixing. if (binding_mods.alt) { - if (buf.len < 2) return error.OutOfMemory; - buf[0] = 0x1B; - buf[1] = char; - return buf[0..2]; + try writer.writeByte(0x1B); + try writer.writeByte(char); + return; } - if (buf.len < 1) return error.OutOfMemory; - buf[0] = char; - return buf[0..1]; + try writer.writeByte(char); + return; } // If we have no UTF8 text then the only possibility is the // alt-prefix handling of unshifted codepoints... so we process that. - const utf8 = self.event.utf8; + const utf8 = event.utf8; if (utf8.len == 0) { - if (try self.legacyAltPrefix(binding_mods, all_mods)) |byte| { - return try std.fmt.bufPrint(buf, "\x1B{c}", .{byte}); - } - - return ""; + if (try legacyAltPrefix( + event, + binding_mods, + all_mods, + opts, + )) |byte| try writer.print("\x1B{c}", .{byte}); + return; } // In modify other keys state 2, we send the CSI 27 sequence // for any char with a modifier. Ctrl sequences like Ctrl+a // are already handled above. - if (self.modify_other_keys_state_2) modify_other: { - const view = try std.unicode.Utf8View.init(utf8); + if (opts.modify_other_keys_state_2) modify_other: { + const view = std.unicode.Utf8View.init(utf8) catch { + // Assume invalid UTF-8 means we no UTF-8. + break :modify_other; + }; var it = view.iterator(); const codepoint = it.nextCodepoint() orelse break :modify_other; @@ -371,8 +426,7 @@ fn legacy( if (should_modify) { for (function_keys.modifiers, 2..) |modset, code| { if (!binding_mods.equal(modset)) continue; - return try std.fmt.bufPrint( - buf, + return try writer.print( "\x1B[27;{};{}~", .{ code, codepoint }, ); @@ -383,17 +437,17 @@ fn legacy( // Let's see if we should apply fixterms to this codepoint. // At this stage of key processing, we only need to apply fixterms // to unicode codepoints if we have ctrl set. - if (self.event.mods.ctrl) csiu: { + if (event.mods.ctrl) csiu: { // Important: we want to use the original mods here, not the // effective mods. The fixterms spec states the shifted chars // should be sent uppercase but Kitty changes that behavior // so we'll send all the mods. const csi_u_mods, const char = mods: { - var mods = CsiUMods.fromInput(self.event.mods); + var mods = CsiUMods.fromInput(event.mods); // Get our codepoint. If we have more than one codepoint this // can't be valid CSIu. - const view = std.unicode.Utf8View.init(self.event.utf8) catch break :csiu; + const view = std.unicode.Utf8View.init(event.utf8) catch break :csiu; var it = view.iterator(); var char = it.nextCodepoint() orelse break :csiu; if (it.nextCodepoint() != null) break :csiu; @@ -414,25 +468,27 @@ fn legacy( // then we consider shift. Otherwise, we do not because the // shift key was used to obtain the character. This is specified // by fixterms. - if (self.event.unshifted_codepoint != char) { + if (event.unshifted_codepoint != char) { mods.shift = false; } break :mods .{ mods, char }; }; - const result = try std.fmt.bufPrint( - buf, + return try writer.print( "\x1B[{};{}u", .{ char, csi_u_mods.seqInt() }, ); - // std.log.warn("CSI_U: {s}", .{result}); - return result; } // If we have alt-pressed and alt-esc-prefix is enabled, then // we need to prefix the utf8 sequence with an esc. - if (try self.legacyAltPrefix(binding_mods, all_mods)) |byte| { - return try std.fmt.bufPrint(buf, "\x1B{c}", .{byte}); + if (try legacyAltPrefix( + event, + binding_mods, + all_mods, + opts, + )) |byte| { + return try writer.print("\x1B{c}", .{byte}); } // If we are on macOS, command+keys do not encode text. It isn't @@ -445,25 +501,26 @@ fn legacy( // For example on Gnome Console Super+b will encode a "b" character // with legacy encoding. if ((comptime builtin.os.tag == .macos) and all_mods.super) { - return ""; + return; } - return try copyToBuf(buf, utf8); + return try writer.writeAll(utf8); } fn legacyAltPrefix( - self: *const KeyEncoder, + event: key.KeyEvent, binding_mods: key.Mods, mods: key.Mods, + opts: Options, ) !?u8 { // This only takes effect with alt pressed - if (!binding_mods.alt or !self.alt_esc_prefix) return null; + if (!binding_mods.alt or !opts.alt_esc_prefix) return null; // On macOS, we only handle option like alt in certain // circumstances. Otherwise, macOS does a unicode translation // and we allow that to happen. if (comptime builtin.os.tag == .macos) { - switch (self.macos_option_as_alt) { + switch (opts.macos_option_as_alt) { .false => return null, .left => if (mods.sides.alt == .right) return null, .right => if (mods.sides.alt == .left) return null, @@ -472,7 +529,7 @@ fn legacyAltPrefix( } // Otherwise, we require utf8 to already have the byte represented. - const utf8 = self.event.utf8; + const utf8 = event.utf8; if (utf8.len == 1) { if (std.math.cast(u8, utf8[0])) |byte| { return byte; @@ -480,10 +537,10 @@ fn legacyAltPrefix( } // If UTF8 isn't set, we will allow unshifted codepoints through. - if (self.event.unshifted_codepoint > 0) { + if (event.unshifted_codepoint > 0) { if (std.math.cast( u8, - self.event.unshifted_codepoint, + event.unshifted_codepoint, )) |byte| { return byte; } @@ -897,19 +954,18 @@ const KittySequence = struct { release = 3, }; - pub fn encode(self: KittySequence, buf: []u8) ![]const u8 { - if (self.final == 'u' or self.final == '~') return try self.encodeFull(buf); - return try self.encodeSpecial(buf); + pub fn encode( + self: KittySequence, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + if (self.final == 'u' or self.final == '~') return try self.encodeFull(writer); + return try self.encodeSpecial(writer); } - fn encodeFull(self: KittySequence, buf: []u8) ![]const u8 { - // Boilerplate to basically create a string builder that writes - // over our buffer (but no more). - var fba = std.heap.FixedBufferAllocator.init(buf); - const alloc = fba.allocator(); - var builder = try std.ArrayListUnmanaged(u8).initCapacity(alloc, buf.len); - const writer = builder.writer(alloc); - + fn encodeFull( + self: KittySequence, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { // Key section try writer.print("\x1B[{d}", .{self.key}); // Write our alternates @@ -937,8 +993,11 @@ const KittySequence = struct { } // Text section - if (self.text.len > 0) { - const view = try std.unicode.Utf8View.init(self.text); + if (self.text.len > 0) text: { + const view = std.unicode.Utf8View.init(self.text) catch { + // Assume invalid UTF-8 means we have no text. + break :text; + }; var it = view.iterator(); var count: usize = 0; while (it.nextCodepoint()) |cp| { @@ -960,13 +1019,15 @@ const KittySequence = struct { } try writer.print("{c}", .{self.final}); - return builder.items; } - fn encodeSpecial(self: KittySequence, buf: []u8) ![]const u8 { + fn encodeSpecial( + self: KittySequence, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { const mods = self.mods.seqInt(); if (self.event != .none) { - return try std.fmt.bufPrint(buf, "\x1B[1;{d}:{d}{c}", .{ + return try writer.print("\x1B[1;{d}:{d}{c}", .{ mods, @intFromEnum(self.event), self.final, @@ -974,13 +1035,13 @@ const KittySequence = struct { } if (mods > 1) { - return try std.fmt.bufPrint(buf, "\x1B[1;{d}{c}", .{ + return try writer.print("\x1B[1;{d}{c}", .{ mods, self.final, }); } - return try std.fmt.bufPrint(buf, "\x1B[{c}", .{self.final}); + return try writer.print("\x1B[{c}", .{self.final}); } }; @@ -989,27 +1050,30 @@ test "KittySequence: backspace" { // Plain { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u' }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127u", writer.buffered()); } // Release event { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;1:3u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;1:3u", writer.buffered()); } // Shift { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .mods = .{ .shift = true }, }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;2u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;2u", writer.buffered()); } } @@ -1018,221 +1082,214 @@ test "KittySequence: text" { // Plain { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "A", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;;65u", writer.buffered()); } // Release { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release, .text = "A", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;1:3;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;1:3;65u", writer.buffered()); } // Shift { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .mods = .{ .shift = true }, .text = "A", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;2;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;2;65u", writer.buffered()); } } - +// test "KittySequence: text with control characters" { var buf: [128]u8 = undefined; // By itself { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "\n", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1b[127u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1b[127u", writer.buffered()); } // With other printables { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "A\n", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1b[127;;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1b[127;;65u", writer.buffered()); } } - +// test "KittySequence: special no mods" { var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A' }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[A", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[A", writer.buffered()); } test "KittySequence: special mods only" { var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A', .mods = .{ .shift = true } }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[1;2A", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[1;2A", writer.buffered()); } test "KittySequence: special mods and event" { var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A', .event = .release, .mods = .{ .shift = true }, }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[1;2:3A", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[1;2:3A", writer.buffered()); } test "kitty: plain text" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{}, - .utf8 = "abcd", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{}, + .utf8 = "abcd", + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("abcd", actual); + }); + try testing.expectEqualStrings("abcd", writer.buffered()); } test "kitty: repeat with just disambiguate" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .action = .repeat, - .mods = .{}, - .utf8 = "a", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .action = .repeat, + .mods = .{}, + .utf8 = "a", + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("a", actual); + }); + try testing.expectEqualStrings("a", writer.buffered()); } - +// test "kitty: enter, backspace, tab" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ - .event = .{ .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\r", actual); + }); + try testing.expectEqualStrings("\r", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .key = .backspace, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x7f", actual); + }); + try testing.expectEqualStrings("\x7f", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .key = .tab, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\t", actual); + }); + try testing.expectEqualStrings("\t", writer.buffered()); } // No release events if "report_all" is not set { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } // Release events if "report_all" is set { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[13;1:3u", actual); + }); + try testing.expectEqualStrings("\x1b[13;1:3u", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[127;1:3u", actual); + }); + try testing.expectEqualStrings("\x1b[127;1:3u", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[9;1:3u", actual); + }); + try testing.expectEqualStrings("\x1b[9;1:3u", writer.buffered()); } } - +// test "kitty: enter with all flags" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1240,15 +1297,15 @@ test "kitty: enter with all flags" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[13u", actual[1..]); } - +// test "kitty: ctrl with all flags" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1256,20 +1313,20 @@ test "kitty: ctrl with all flags" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[57442;5u", actual[1..]); } test "kitty: ctrl release with ctrl mod set" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .action = .release, - .key = .control_left, - .mods = .{ .ctrl = true }, - .utf8 = "", - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .control_left, + .mods = .{ .ctrl = true }, + .utf8 = "", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1277,210 +1334,191 @@ test "kitty: ctrl release with ctrl mod set" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[57442;5:3u", actual[1..]); } test "kitty: delete" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ - .event = .{ .key = .delete, .mods = .{}, .utf8 = "\x7F" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .delete, .mods = .{}, .utf8 = "\x7F" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[3~", actual); + }); + try testing.expectEqualStrings("\x1b[3~", writer.buffered()); } } test "kitty: composing with no modifier" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{ .shift = true }, - .composing = true, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{ .shift = true }, + .composing = true, + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } test "kitty: composing with modifier" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .shift_left, - .mods = .{ .shift = true }, - .composing = true, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .shift_left, + .mods = .{ .shift = true }, + .composing = true, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[57441;2u", actual); + }); + try testing.expectEqualStrings("\x1b[57441;2u", writer.buffered()); } test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{ .shift = true }, - .utf8 = "A", - .unshifted_codepoint = 97, // lowercase A - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{ .shift = true }, + .utf8 = "A", + .unshifted_codepoint = 97, // lowercase A + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[97:65;2u", actual); + }); + try testing.expectEqualStrings("\x1b[97:65;2u", writer.buffered()); } test "kitty: matching unshifted codepoint" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{ .shift = true }, - .utf8 = "A", - .unshifted_codepoint = 65, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{ .shift = true }, + .utf8 = "A", + .unshifted_codepoint = 65, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, }, - }; - + }); // WARNING: This is not a valid encoding. This is a hypothetical encoding // just to test that our logic is correct around matching unshifted // codepoints. We get an alternate here because the unshifted_codepoint does // not match the base key - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[65::97;2u", actual); + try testing.expectEqualStrings("\x1b[65::97;2u", writer.buffered()); } test "kitty: report alternates with caps" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_j, - .mods = .{ .caps_lock = true }, - .utf8 = "J", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_j, + .mods = .{ .caps_lock = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[106;65;74u", actual); + }); + try testing.expectEqualStrings("\x1b[106;65;74u", writer.buffered()); } test "kitty: report alternates colon (shift+';')" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{ .shift = true }, - .utf8 = ":", - .unshifted_codepoint = ';', - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{ .shift = true }, + .utf8 = ":", + .unshifted_codepoint = ';', + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[59:58;2;58u", actual); + }); + try testing.expectEqualStrings("\x1b[59:58;2;58u", writer.buffered()); } test "kitty: report alternates with ru layout" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{}, - .utf8 = "ч", - .unshifted_codepoint = 1095, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{}, + .utf8 = "ч", + .unshifted_codepoint = 1095, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[1095::59;;1095u", actual); + }); + try testing.expectEqualStrings("\x1b[1095::59;;1095u", writer.buffered()); } test "kitty: report alternates with ru layout shifted" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{ .shift = true }, - .utf8 = "Ч", - .unshifted_codepoint = 1095, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{ .shift = true }, + .utf8 = "Ч", + .unshifted_codepoint = 1095, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", actual); + }); + try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", writer.buffered()); } test "kitty: report alternates with ru layout caps lock" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{ .caps_lock = true }, - .utf8 = "Ч", - .unshifted_codepoint = 1095, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{ .caps_lock = true }, + .utf8 = "Ч", + .unshifted_codepoint = 1095, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[1095::59;65;1063u", actual); + }); + try testing.expectEqualStrings("\x1b[1095::59;65;1063u", writer.buffered()); } test "kitty: report alternates with hu layout release" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .action = .release, - .key = .bracket_left, - .mods = .{ .ctrl = true }, - .utf8 = "", - .unshifted_codepoint = 337, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .bracket_left, + .mods = .{ .ctrl = true }, + .utf8 = "", + .unshifted_codepoint = 337, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1488,88 +1526,75 @@ test "kitty: report alternates with hu layout release" { .report_associated = true, .report_events = true, }, - }; - - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[337::91;5:3u", actual[1..]); } // macOS generates utf8 text for arrow keys. test "kitty: up arrow with utf8" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .arrow_up, - .mods = .{}, - .utf8 = &.{30}, - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .arrow_up, + .mods = .{}, + .utf8 = &.{30}, + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[A", actual); + }); + try testing.expectEqualStrings("\x1b[A", writer.buffered()); } test "kitty: shift+tab" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .tab, - .mods = .{ .shift = true }, - .utf8 = "", // tab - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .tab, + .mods = .{ .shift = true }, + .utf8 = "", // tab + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[9;2u", actual); + }); + try testing.expectEqualStrings("\x1b[9;2u", writer.buffered()); } test "kitty: left shift" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .shift_left, - .mods = .{}, - .utf8 = "", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .shift_left, + .mods = .{}, + .utf8 = "", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } test "kitty: left shift with report all" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .shift_left, - .mods = .{}, - .utf8 = "", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .shift_left, + .mods = .{}, + .utf8 = "", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[57441u", actual); + }); + try testing.expectEqualStrings("\x1b[57441u", writer.buffered()); } test "kitty: report associated with alt text on macOS with option" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_w, - .mods = .{ .alt = true }, - .utf8 = "∑", - .unshifted_codepoint = 119, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_w, + .mods = .{ .alt = true }, + .utf8 = "∑", + .unshifted_codepoint = 119, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1577,10 +1602,8 @@ test "kitty: report associated with alt text on macOS with option" { .report_associated = true, }, .macos_option_as_alt = .false, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[119;3;8721u", actual); + }); + try testing.expectEqualStrings("\x1b[119;3;8721u", writer.buffered()); } test "kitty: report associated with alt text on macOS with alt" { @@ -1589,13 +1612,13 @@ test "kitty: report associated with alt text on macOS with alt" { { // With Alt modifier var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_w, - .mods = .{ .alt = true }, - .utf8 = "∑", - .unshifted_codepoint = 119, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_w, + .mods = .{ .alt = true }, + .utf8 = "∑", + .unshifted_codepoint = 119, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1603,22 +1626,20 @@ test "kitty: report associated with alt text on macOS with alt" { .report_associated = true, }, .macos_option_as_alt = .true, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[119;3u", actual); + }); + try testing.expectEqualStrings("\x1b[119;3u", writer.buffered()); } { // Without Alt modifier var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_w, - .mods = .{}, - .utf8 = "∑", - .unshifted_codepoint = 119, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_w, + .mods = .{}, + .utf8 = "∑", + .unshifted_codepoint = 119, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1626,65 +1647,59 @@ test "kitty: report associated with alt text on macOS with alt" { .report_associated = true, }, .macos_option_as_alt = .true, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[119;;8721u", actual); + }); + try testing.expectEqualStrings("\x1b[119;;8721u", writer.buffered()); } } test "kitty: report associated with modifiers" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_j, - .mods = .{ .ctrl = true }, - .utf8 = "j", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_j, + .mods = .{ .ctrl = true }, + .utf8 = "j", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[106;5u", actual); + }); + try testing.expectEqualStrings("\x1b[106;5u", writer.buffered()); } test "kitty: report associated" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_j, - .mods = .{ .shift = true }, - .utf8 = "J", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_j, + .mods = .{ .shift = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[106:74;2;74u", actual); + }); + try testing.expectEqualStrings("\x1b[106:74;2;74u", writer.buffered()); } test "kitty: report associated on release" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .action = .release, - .key = .key_j, - .mods = .{ .shift = true }, - .utf8 = "J", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .key_j, + .mods = .{ .shift = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1692,54 +1707,53 @@ test "kitty: report associated on release" { .report_associated = true, .report_events = true, }, - }; - - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[106:74;2:3u", actual[1..]); } test "kitty: alternates omit control characters" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .delete, - .mods = .{}, - .utf8 = &.{0x7F}, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .delete, + .mods = .{}, + .utf8 = &.{0x7F}, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, .report_all = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[3~", actual); + }); + try testing.expectEqualStrings("\x1b[3~", writer.buffered()); } test "kitty: enter with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .enter, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .enter, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, .report_all = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("A", actual); + }); + try testing.expectEqualStrings("A", writer.buffered()); } test "kitty: keypad number" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ .key = .numpad_1, .mods = .{}, .utf8 = "1" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .numpad_1, + .mods = .{}, + .utf8 = "1", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1747,19 +1761,19 @@ test "kitty: keypad number" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[57400;;49u", actual[1..]); } test "kitty: backspace with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .backspace, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1767,261 +1781,223 @@ test "kitty: backspace with utf8 (dead key state)" { .report_all = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: backspace with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{}); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: enter with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .enter, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("A", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .enter, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{}); + try testing.expectEqualStrings("A", writer.buffered()); } test "legacy: esc with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .escape, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("A", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .escape, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{}); + try testing.expectEqualStrings("A", writer.buffered()); } test "legacy: ctrl+shift+minus (underscore on US)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .minus, - .mods = .{ .ctrl = true, .shift = true }, - .utf8 = "_", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1F", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .minus, + .mods = .{ .ctrl = true, .shift = true }, + .utf8 = "_", + }, .{}); + try testing.expectEqualStrings("\x1F", writer.buffered()); } test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .mods = .{ .ctrl = true, .alt = true }, - .utf8 = "c", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b\x03", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .mods = .{ .ctrl = true, .alt = true }, + .utf8 = "c", + }, .{}); + try testing.expectEqualStrings("\x1b\x03", writer.buffered()); } test "legacy: alt+c" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .utf8 = "c", - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .utf8 = "c", + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1Bc", actual); + }); + try testing.expectEqualStrings("\x1Bc", writer.buffered()); } test "legacy: alt+e only unshifted" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_e, - .unshifted_codepoint = 'e', - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_e, + .unshifted_codepoint = 'e', + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1Be", actual); + }); + try testing.expectEqualStrings("\x1Be", writer.buffered()); } test "legacy: alt+x macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .utf8 = "≈", - .unshifted_codepoint = 'c', - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .utf8 = "≈", + .unshifted_codepoint = 'c', + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1Bc", actual); + }); + try testing.expectEqualStrings("\x1Bc", writer.buffered()); } test "legacy: shift+alt+. macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .period, - .utf8 = ">", - .unshifted_codepoint = '.', - .mods = .{ .alt = true, .shift = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .period, + .utf8 = ">", + .unshifted_codepoint = '.', + .mods = .{ .alt = true, .shift = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1B>", actual); + }); + try testing.expectEqualStrings("\x1B>", writer.buffered()); } test "legacy: alt+ф" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_f, - .utf8 = "ф", - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_f, + .utf8 = "ф", + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("ф", actual); + }); + try testing.expectEqualStrings("ф", writer.buffered()); } test "legacy: ctrl+c" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .mods = .{ .ctrl = true }, - .utf8 = "c", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x03", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .mods = .{ .ctrl = true }, + .utf8 = "c", + }, .{}); + try testing.expectEqualStrings("\x03", writer.buffered()); } test "legacy: ctrl+space" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .space, - .mods = .{ .ctrl = true }, - .utf8 = " ", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x00", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .space, + .mods = .{ .ctrl = true }, + .utf8 = " ", + }, .{}); + try testing.expectEqualStrings("\x00", writer.buffered()); } test "legacy: ctrl+shift+backspace" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .mods = .{ .ctrl = true, .shift = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x08", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .mods = .{ .ctrl = true, .shift = true }, + }, .{}); + try testing.expectEqualStrings("\x08", writer.buffered()); } test "legacy: ctrl+shift+char with modify other state 2" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_h, - .mods = .{ .ctrl = true, .shift = true }, - .utf8 = "H", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_h, + .mods = .{ .ctrl = true, .shift = true }, + .utf8 = "H", + }, .{ .modify_other_keys_state_2 = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[27;6;72~", actual); + }); + try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered()); } test "legacy: fixterm awkward letters" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .key_i, .mods = .{ .ctrl = true }, .utf8 = "i", - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[105;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[105;5u", writer.buffered()); } { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .key_m, .mods = .{ .ctrl = true }, .utf8 = "m", - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[109;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[109;5u", writer.buffered()); } { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "[", - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[91;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[91;5u", writer.buffered()); } { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .digit_2, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "@", .unshifted_codepoint = '2', - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[64;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[64;5u", writer.buffered()); } } @@ -2030,199 +2006,189 @@ test "legacy: fixterm awkward letters" { test "legacy: ctrl+shift+letter ascii" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .key_m, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "M", .unshifted_codepoint = 'm', - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[109;6u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[109;6u", writer.buffered()); } } test "legacy: shift+function key should use all mods" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .arrow_up, - .mods = .{ .shift = true }, - .consumed_mods = .{ .shift = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;2A", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .arrow_up, + .mods = .{ .shift = true }, + .consumed_mods = .{ .shift = true }, + }, .{}); + try testing.expectEqualStrings("\x1b[1;2A", writer.buffered()); } test "legacy: keypad enter" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_enter, - .mods = .{}, - .consumed_mods = .{}, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\r", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_enter, + .mods = .{}, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\r", writer.buffered()); } test "legacy: keypad 1" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{}, - .consumed_mods = .{}, - .utf8 = "1", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("1", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{}, + .consumed_mods = .{}, + .utf8 = "1", + }, .{}); + try testing.expectEqualStrings("1", writer.buffered()); } test "legacy: keypad 1 with application keypad" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{}, - .consumed_mods = .{}, - .utf8 = "1", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{}, + .consumed_mods = .{}, + .utf8 = "1", + }, .{ .keypad_key_application = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1bOq", actual); + }); + try testing.expectEqualStrings("\x1bOq", writer.buffered()); } test "legacy: keypad 1 with application keypad and numlock" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{ .num_lock = true }, - .consumed_mods = .{}, - .utf8 = "1", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{ .num_lock = true }, + .consumed_mods = .{}, + .utf8 = "1", + }, .{ .keypad_key_application = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1bOq", actual); + }); + try testing.expectEqualStrings("\x1bOq", writer.buffered()); } test "legacy: keypad 1 with application keypad and numlock ignore" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{ .num_lock = false }, - .consumed_mods = .{}, - .utf8 = "1", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{ .num_lock = false }, + .consumed_mods = .{}, + .utf8 = "1", + }, .{ .keypad_key_application = true, .ignore_keypad_with_numlock = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("1", actual); + }); + try testing.expectEqualStrings("1", writer.buffered()); } test "legacy: f1" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .f1, - .mods = .{ .ctrl = true }, - .consumed_mods = .{}, - }, - }; // F1 { - enc.event.key = .f1; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;5P", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f1, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[1;5P", writer.buffered()); } // F2 { - enc.event.key = .f2; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;5Q", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f2, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[1;5Q", writer.buffered()); } // F3 { - enc.event.key = .f3; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[13;5~", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f3, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[13;5~", writer.buffered()); } // F4 { - enc.event.key = .f4; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;5S", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f4, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[1;5S", writer.buffered()); } // F5 uses new encoding { - enc.event.key = .f5; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[15;5~", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f5, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[15;5~", writer.buffered()); } } test "legacy: left_shift+tab" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .tab, - .mods = .{ - .shift = true, - .sides = .{ .shift = .left }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .tab, + .mods = .{ + .shift = true, + .sides = .{ .shift = .left }, }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[Z", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[Z", writer.buffered()); } test "legacy: right_shift+tab" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .tab, - .mods = .{ - .shift = true, - .sides = .{ .shift = .right }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .tab, + .mods = .{ + .shift = true, + .sides = .{ .shift = .right }, }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[Z", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[Z", writer.buffered()); } test "legacy: hu layout ctrl+ő sends proper codepoint" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .bracket_left, - .mods = .{ .ctrl = true }, - .utf8 = "ő", - .unshifted_codepoint = 337, - }, - }; - - const actual = try enc.legacy(&buf); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .bracket_left, + .mods = .{ .ctrl = true }, + .utf8 = "ő", + .unshifted_codepoint = 337, + }, .{}); + const actual = writer.buffered(); try testing.expectEqualStrings("[337;5u", actual[1..]); } @@ -2230,46 +2196,37 @@ test "legacy: super-only on macOS with text" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_b, - .utf8 = "b", - .mods = .{ .super = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_b, + .utf8 = "b", + .mods = .{ .super = true }, + }, .{}); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: super and other mods on macOS with text" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_b, - .utf8 = "B", - .mods = .{ .super = true, .shift = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_b, + .utf8 = "B", + .mods = .{ .super = true, .shift = true }, + }, .{}); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: backspace with DEL utf8" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .utf8 = &.{0x7F}, - .unshifted_codepoint = 0x08, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x7F", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .utf8 = &.{0x7F}, + .unshifted_codepoint = 0x08, + }, .{}); + try testing.expectEqualStrings("\x7F", writer.buffered()); } test "ctrlseq: normal ctrl c" { diff --git a/src/input/keyboard.zig b/src/input/keyboard.zig index 73674df2c..d2882a23a 100644 --- a/src/input/keyboard.zig +++ b/src/input/keyboard.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const OptionAsAlt = @import("../config.zig").OptionAsAlt; +const OptionAsAlt = @import("config.zig").OptionAsAlt; /// Keyboard layouts. /// diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 4b064dc0d..4d51d1062 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -72,10 +72,22 @@ pub const input = struct { // the input package because the full package brings in too many // other dependencies. const paste = @import("input/paste.zig"); + const key = @import("input/key.zig"); + const key_encode = @import("input/key_encode.zig"); + + // Paste-related APIs pub const PasteError = paste.Error; pub const PasteOptions = paste.Options; pub const isSafePaste = paste.isSafe; pub const encodePaste = paste.encode; + + // Key encoding + pub const Key = key.Key; + pub const KeyAction = key.Action; + pub const KeyEvent = key.KeyEvent; + pub const KeyMods = key.Mods; + pub const KeyEncodeOptions = key_encode.Options; + pub const encodeKey = key_encode.encode; }; comptime { diff --git a/src/terminal/kitty/key.zig b/src/terminal/kitty/key.zig index 0883c90f2..8594c4c39 100644 --- a/src/terminal/kitty/key.zig +++ b/src/terminal/kitty/key.zig @@ -8,7 +8,7 @@ const std = @import("std"); pub const FlagStack = struct { const len = 8; - flags: [len]Flags = @splat(.{}), + flags: [len]Flags = @splat(.disabled), idx: u3 = 0, /// Return the current stack value @@ -51,12 +51,12 @@ pub const FlagStack = struct { // could send a huge number of pop commands to waste cpu. if (n >= self.flags.len) { self.idx = 0; - self.flags = @splat(.{}); + self.flags = @splat(.disabled); return; } for (0..n) |_| { - self.flags[self.idx] = .{}; + self.flags[self.idx] = .disabled; self.idx -%= 1; } } @@ -83,6 +83,15 @@ pub const Flags = packed struct(u5) { report_all: bool = false, report_associated: bool = false, + /// Kitty keyboard protocol disabled (all flags off). + pub const disabled: Flags = .{ + .disambiguate = false, + .report_events = false, + .report_alternates = false, + .report_all = false, + .report_associated = false, + }; + /// Sets all modes on. pub const @"true": Flags = .{ .disambiguate = true, From 1c0282d658f494674b93fe9aa8ded3160fe6290d Mon Sep 17 00:00:00 2001 From: NikoMalik Date: Sun, 5 Oct 2025 12:30:51 +0300 Subject: [PATCH 170/319] fix:use builtin memmove --- src/fastmem.zig | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/fastmem.zig b/src/fastmem.zig index bdea44155..47009a0a7 100644 --- a/src/fastmem.zig +++ b/src/fastmem.zig @@ -5,18 +5,7 @@ const assert = std.debug.assert; /// Same as std.mem.copyForwards/Backwards but prefers libc memmove if it is /// available because it is generally much faster. pub inline fn move(comptime T: type, dest: []T, source: []const T) void { - if (builtin.link_libc) { - _ = memmove(dest.ptr, source.ptr, source.len * @sizeOf(T)); - } else { - // Depending on the ordering of the copy, we need to use the - // proper call here. Unfortunately this function call is - // too generic to know this at comptime. - if (@intFromPtr(dest.ptr) <= @intFromPtr(source.ptr)) { - std.mem.copyForwards(T, dest, source); - } else { - std.mem.copyBackwards(T, dest, source); - } - } + @memmove(dest, source); } /// Same as @memcpy but prefers libc memcpy if it is available From ff3a6d06503f22cd38b4f741c9c63327805df6ca Mon Sep 17 00:00:00 2001 From: NikoMalik Date: Sun, 5 Oct 2025 17:35:58 +0300 Subject: [PATCH 171/319] fix: do not remove libc memmove until performance comparisons have been conducted --- src/fastmem.zig | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/fastmem.zig b/src/fastmem.zig index 47009a0a7..d4a0a7750 100644 --- a/src/fastmem.zig +++ b/src/fastmem.zig @@ -2,10 +2,14 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; -/// Same as std.mem.copyForwards/Backwards but prefers libc memmove if it is -/// available because it is generally much faster. +/// Same as @memmove but prefers libc memmove if it is +/// available because it is generally much faster?. pub inline fn move(comptime T: type, dest: []T, source: []const T) void { - @memmove(dest, source); + if (builtin.link_libc) { + _ = memmove(dest.ptr, source.ptr, source.len * @sizeOf(T)); + } else { + @memmove(dest, source); + } } /// Same as @memcpy but prefers libc memcpy if it is available From 61fe78c1d304b31400609d8191e427466ebcec25 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Oct 2025 20:32:42 -0700 Subject: [PATCH 172/319] lib-vt: expose key encoding as a C API --- AGENTS.md | 9 +- example/c-vt-key-encode/README.md | 22 + example/c-vt-key-encode/build.zig | 42 ++ example/c-vt-key-encode/build.zig.zon | 24 + example/c-vt-key-encode/src/main.c | 59 +++ include/ghostty/vt.h | 639 +++++++++++++++++++++++++- src/input/key_encode.zig | 10 + src/lib_vt.zig | 20 + src/terminal/c/key_encode.zig | 269 +++++++++++ src/terminal/c/key_event.zig | 253 ++++++++++ src/terminal/c/main.zig | 26 ++ 11 files changed, 1371 insertions(+), 2 deletions(-) create mode 100644 example/c-vt-key-encode/README.md create mode 100644 example/c-vt-key-encode/build.zig create mode 100644 example/c-vt-key-encode/build.zig.zon create mode 100644 example/c-vt-key-encode/src/main.c create mode 100644 src/terminal/c/key_encode.zig create mode 100644 src/terminal/c/key_event.zig diff --git a/AGENTS.md b/AGENTS.md index 2e90fd94e..14fff7b3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,10 +13,17 @@ A file for [guiding coding agents](https://agents.md/). ## Directory Structure - Shared Zig core: `src/` -- C API: `include/ghostty.h` +- C API: `include` - macOS app: `macos/` - GTK (Linux and FreeBSD) app: `src/apprt/gtk` +## libghostty-vt + +- Build: `zig build lib-vt` +- Test: `zig build test-lib-vt` +- Test filter: `zig build test-lib-vt -Dtest-filter=` +- When working on libghostty-vt, do not build the full app. + ## macOS App - Do not use `xcodebuild` diff --git a/example/c-vt-key-encode/README.md b/example/c-vt-key-encode/README.md new file mode 100644 index 000000000..05ee3fc31 --- /dev/null +++ b/example/c-vt-key-encode/README.md @@ -0,0 +1,22 @@ +# Example: `ghostty-vt` C Key Encoding + +This example demonstrates how to use the `ghostty-vt` C library to encode key +events into terminal escape sequences. + +This example specifically shows how to: + +1. Create a key encoder with the C API +2. Configure Kitty keyboard protocol flags (this example uses KKP) +3. Create and configure a key event +4. Encode the key event into a terminal escape sequence + +The example encodes a Ctrl key release event with the Ctrl modifier set, +producing the escape sequence `\x1b[57442;5:3u`. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-key-encode/build.zig b/example/c-vt-key-encode/build.zig new file mode 100644 index 000000000..b4b759744 --- /dev/null +++ b/example/c-vt-key-encode/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_key_encode", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-key-encode/build.zig.zon b/example/c-vt-key-encode/build.zig.zon new file mode 100644 index 000000000..5da1a9168 --- /dev/null +++ b/example/c-vt-key-encode/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt, + .version = "0.0.0", + .fingerprint = 0x413a8529b1255f9a, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-key-encode/src/main.c b/example/c-vt-key-encode/src/main.c new file mode 100644 index 000000000..82444f99d --- /dev/null +++ b/example/c-vt-key-encode/src/main.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +int main() { + GhosttyKeyEncoder encoder; + GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + assert(result == GHOSTTY_SUCCESS); + + // Set kitty flags with all features enabled + ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + + // Create key event + GhosttyKeyEvent event; + result = ghostty_key_event_new(NULL, &event); + assert(result == GHOSTTY_SUCCESS); + ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_RELEASE); + ghostty_key_event_set_key(event, GHOSTTY_KEY_CONTROL_LEFT); + ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + printf("Encoding event: left ctrl release with all Kitty flags enabled\n"); + + // Optionally, encode with null buffer to get required size. You can + // skip this step and provide a sufficiently large buffer directly. + // If there isn't enoug hspace, the function will return an out of memory + // error. + size_t required = 0; + result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + assert(result == GHOSTTY_OUT_OF_MEMORY); + printf("Required buffer size: %zu bytes\n", required); + + // Encode the key event. We don't use our required size above because + // that was just an example; we know 128 bytes is enough. + char buf[128]; + size_t written = 0; + result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + assert(result == GHOSTTY_SUCCESS); + printf("Encoded %zu bytes\n", written); + + // Print the encoded sequence (hex and string) + printf("Hex: "); + for (size_t i = 0; i < written; i++) printf("%02x ", (unsigned char)buf[i]); + printf("\n"); + + printf("String: "); + for (size_t i = 0; i < written; i++) { + if (buf[i] == 0x1b) { + printf("\\x1b"); + } else { + printf("%c", buf[i]); + } + } + printf("\n"); + + ghostty_key_event_free(event); + ghostty_key_encoder_free(encoder); + return 0; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 4b930a96f..7815ebb81 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -27,6 +27,7 @@ * @section groups_sec API Reference * * The API is organized into the following groups: + * - @ref key "Key Encoding" - Encode key events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref allocator "Memory Management" - Memory management and custom allocators * @@ -319,7 +320,643 @@ typedef struct { /** @} */ // end of allocator group //------------------------------------------------------------------- -// Functions +// Key Encoding + +/** @defgroup key Key Encoding + * + * Utilities for encoding key events into terminal escape sequences, + * supporting both legacy encoding as well as Kitty Keyboard Protocol. + * + * @{ + */ + +/** + * Opaque handle to a key event. + * + * This handle represents a keyboard input event containing information about + * the physical key pressed, modifiers, and generated text. The event can be + * configured using the `ghostty_key_event_set_*` functions. + * + * @ingroup key + */ +typedef struct GhosttyKeyEvent *GhosttyKeyEvent; + +/** + * Keyboard input event types. + * + * @ingroup key + */ +typedef enum { + /** Key was released */ + GHOSTTY_KEY_ACTION_RELEASE = 0, + /** Key was pressed */ + GHOSTTY_KEY_ACTION_PRESS = 1, + /** Key is being repeated (held down) */ + GHOSTTY_KEY_ACTION_REPEAT = 2, +} GhosttyKeyAction; + +/** + * Keyboard modifier keys bitmask. + * + * A bitmask representing all keyboard modifiers. This tracks which modifier keys + * are pressed and, where supported by the platform, which side (left or right) + * of each modifier is active. + * + * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. + * + * Modifier side bits are only meaningful when the corresponding modifier bit is set. + * Not all platforms support distinguishing between left and right modifier + * keys and Ghostty is built to expect that some platforms may not provide this + * information. + * + * @ingroup key + */ +typedef uint16_t GhosttyMods; + +/** Shift key is pressed */ +#define GHOSTTY_MODS_SHIFT (1 << 0) +/** Control key is pressed */ +#define GHOSTTY_MODS_CTRL (1 << 1) +/** Alt/Option key is pressed */ +#define GHOSTTY_MODS_ALT (1 << 2) +/** Super/Command/Windows key is pressed */ +#define GHOSTTY_MODS_SUPER (1 << 3) +/** Caps Lock is active */ +#define GHOSTTY_MODS_CAPS_LOCK (1 << 4) +/** Num Lock is active */ +#define GHOSTTY_MODS_NUM_LOCK (1 << 5) + +/** + * Right shift is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SHIFT is set. + */ +#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) +/** + * Right ctrl is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_CTRL is set. + */ +#define GHOSTTY_MODS_CTRL_SIDE (1 << 7) +/** + * Right alt is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_ALT is set. + */ +#define GHOSTTY_MODS_ALT_SIDE (1 << 8) +/** + * Right super is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SUPER is set. + */ +#define GHOSTTY_MODS_SUPER_SIDE (1 << 9) + +/** + * Physical key codes. + * + * The set of key codes that Ghostty is aware of. These represent physical keys + * on the keyboard and are layout-independent. For example, the "a" key on a US + * keyboard is the same as the "ф" key on a Russian keyboard, but both will + * report the same key_a value. + * + * Layout-dependent strings are provided separately as UTF-8 text and are produced + * by the platform. These values are based on the W3C UI Events KeyboardEvent code + * standard. See: https://www.w3.org/TR/uievents-code + * + * @ingroup key + */ +typedef enum { + GHOSTTY_KEY_UNIDENTIFIED = 0, + + // Writing System Keys (W3C § 3.1.1) + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, + + // Functional Keys (W3C § 3.1.2) + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, + + // Control Pad Section (W3C § 3.2) + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // Arrow Pad Section (W3C § 3.3) + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // Numpad Section (W3C § 3.4) + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // Function Section (W3C § 3.5) + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, + + // Media Keys (W3C § 3.6) + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, + + // Legacy, Non-standard, and Special Keys (W3C § 3.7) + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, +} GhosttyKey; + +/** + * Kitty keyboard protocol flags. + * + * Bitflags representing the various modes of the Kitty keyboard protocol. + * These can be combined using bitwise OR operations. Valid values all + * start with `GHOSTTY_KITTY_KEY_`. + * + * @ingroup key + */ +typedef uint8_t GhosttyKittyKeyFlags; + +/** Kitty keyboard protocol disabled (all flags off) */ +#define GHOSTTY_KITTY_KEY_DISABLED 0 + +/** Disambiguate escape codes */ +#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) + +/** Report key press and release events */ +#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) + +/** Report alternate key codes */ +#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) + +/** Report all key events including those normally handled by the terminal */ +#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) + +/** Report associated text with key events */ +#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) + +/** All Kitty keyboard protocol flags enabled */ +#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) + +/** + * macOS option key behavior. + * + * Determines whether the "option" key on macOS is treated as "alt" or not. + * See the Ghostty `macos-option-as-alt` configuration option for more details. + * + * @ingroup key + */ +typedef enum { + /** Option key is not treated as alt */ + GHOSTTY_OPTION_AS_ALT_FALSE = 0, + /** Option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_TRUE = 1, + /** Only left option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_LEFT = 2, + /** Only right option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_RIGHT = 3, +} GhosttyOptionAsAlt; + +/** + * Create a new key event instance. + * + * Creates a new key event with default values. The event must be freed using + * ghostty_key_event_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param event Pointer to store the created key event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); + +/** + * Free a key event instance. + * + * Releases all resources associated with the key event. After this call, + * the event handle becomes invalid and must not be used. + * + * @param event The key event handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_event_free(GhosttyKeyEvent event); + +/** + * Set the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @param action The action to set + * + * @ingroup key + */ +void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); + +/** + * Get the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @return The key action + * + * @ingroup key + */ +GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); + +/** + * Set the physical key code. + * + * @param event The key event handle, must not be NULL + * @param key The physical key code to set + * + * @ingroup key + */ +void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); + +/** + * Get the physical key code. + * + * @param event The key event handle, must not be NULL + * @return The physical key code + * + * @ingroup key + */ +GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); + +/** + * Set the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @param mods The modifier keys bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); + +/** + * Get the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @return The modifier keys bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); + +/** + * Set the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @param consumed_mods The consumed modifiers bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); + +/** + * Get the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @return The consumed modifiers bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); + +/** + * Set whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @param composing Whether the key event is part of a composition sequence + * + * @ingroup key + */ +void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); + +/** + * Get whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @return Whether the key event is part of a composition sequence + * + * @ingroup key + */ +bool ghostty_key_event_get_composing(GhosttyKeyEvent event); + +/** + * Set the UTF-8 text generated by the key event. + * + * The key event does NOT take ownership of the text pointer. The caller + * must ensure the string remains valid for the lifetime needed by the event. + * + * @param event The key event handle, must not be NULL + * @param utf8 The UTF-8 text to set (or NULL for empty) + * @param len Length of the UTF-8 text in bytes + * + * @ingroup key + */ +void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); + +/** + * Get the UTF-8 text generated by the key event. + * + * The returned pointer is valid until the event is freed or the UTF-8 text is modified. + * + * @param event The key event handle, must not be NULL + * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) + * @return The UTF-8 text (or NULL for empty) + * + * @ingroup key + */ +const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); + +/** + * Set the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @param codepoint The unshifted Unicode codepoint to set + * + * @ingroup key + */ +void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); + +/** + * Get the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @return The unshifted Unicode codepoint + * + * @ingroup key + */ +uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); + +/** + * Opaque handle to a key encoder instance. + * + * This handle represents a key encoder that converts key events into terminal + * escape sequences. The encoder supports both legacy encoding and the Kitty + * Keyboard Protocol, depending on the options set. + * + * @ingroup key + */ +typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; + +/** + * Key encoder option identifiers. + * + * These values are used with ghostty_key_encoder_setopt() to configure + * the behavior of the key encoder. + * + * @ingroup key + */ +typedef enum { + /** Terminal DEC mode 1: cursor key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, + + /** Terminal DEC mode 66: keypad key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, + + /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, + + /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, + + /** xterm modifyOtherKeys mode 2 (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, + + /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ + GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, + + /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ + GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, +} GhosttyKeyEncoderOption; + +/** + * Create a new key encoder instance. + * + * Creates a new key encoder with default options. The encoder can be configured + * using ghostty_key_encoder_setopt() and must be freed using + * ghostty_key_encoder_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); + +/** + * Free a key encoder instance. + * + * Releases all resources associated with the key encoder. After this call, + * the encoder handle becomes invalid and must not be used. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); + +/** + * Set an option on the key encoder. + * + * Configures the behavior of the key encoder. Options control various aspects + * of encoding such as terminal modes (cursor key application mode, keypad mode), + * protocol selection (Kitty keyboard protocol flags), and platform-specific + * behaviors (macOS option-as-alt). + * + * A null pointer value does nothing. It does not reset the value to the + * default. The setopt call will do nothing. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option) + * + * @ingroup key + */ +void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); + +/** + * Encode a key event into a terminal escape sequence. + * + * Converts a key event into the appropriate terminal escape sequence based on + * the encoder's current options. The sequence is written to the provided buffer. + * + * Not all key events produce output. For example, unmodified modifier keys + * typically don't generate escape sequences. Check the out_len parameter to + * determine if any data was written. + * + * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY + * and out_len will contain the required buffer size. The caller can then + * allocate a larger buffer and call the function again. + * + * @param encoder The encoder handle, must not be NULL + * @param event The key event to encode, must not be NULL + * @param out_buf Buffer to write the encoded sequence to + * @param out_buf_size Size of the output buffer in bytes + * @param out_len Pointer to store the number of bytes written (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); + +/** @} */ // end of key group + +//------------------------------------------------------------------- +// OSC Parser /** @defgroup osc OSC Parser * diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index c35cdebaa..fa641c1aa 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -36,6 +36,16 @@ pub const Options = struct { /// docs for a more detailed description of why this is needed. macos_option_as_alt: OptionAsAlt = .false, + pub const default: Options = .{ + .cursor_key_application = false, + .keypad_key_application = false, + .ignore_keypad_with_numlock = false, + .alt_esc_prefix = false, + .modify_other_keys_state_2 = false, + .kitty_flags = .disabled, + .macos_option_as_alt = .false, + }; + /// Initialize our options from the terminal state. /// /// Note that `macos_option_as_alt` cannot be determined from diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 4d51d1062..73a030333 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -102,6 +102,26 @@ comptime { @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); + @export(&c.key_event_new, .{ .name = "ghostty_key_event_new" }); + @export(&c.key_event_free, .{ .name = "ghostty_key_event_free" }); + @export(&c.key_event_set_action, .{ .name = "ghostty_key_event_set_action" }); + @export(&c.key_event_get_action, .{ .name = "ghostty_key_event_get_action" }); + @export(&c.key_event_set_key, .{ .name = "ghostty_key_event_set_key" }); + @export(&c.key_event_get_key, .{ .name = "ghostty_key_event_get_key" }); + @export(&c.key_event_set_mods, .{ .name = "ghostty_key_event_set_mods" }); + @export(&c.key_event_get_mods, .{ .name = "ghostty_key_event_get_mods" }); + @export(&c.key_event_set_consumed_mods, .{ .name = "ghostty_key_event_set_consumed_mods" }); + @export(&c.key_event_get_consumed_mods, .{ .name = "ghostty_key_event_get_consumed_mods" }); + @export(&c.key_event_set_composing, .{ .name = "ghostty_key_event_set_composing" }); + @export(&c.key_event_get_composing, .{ .name = "ghostty_key_event_get_composing" }); + @export(&c.key_event_set_utf8, .{ .name = "ghostty_key_event_set_utf8" }); + @export(&c.key_event_get_utf8, .{ .name = "ghostty_key_event_get_utf8" }); + @export(&c.key_event_set_unshifted_codepoint, .{ .name = "ghostty_key_event_set_unshifted_codepoint" }); + @export(&c.key_event_get_unshifted_codepoint, .{ .name = "ghostty_key_event_get_unshifted_codepoint" }); + @export(&c.key_encoder_new, .{ .name = "ghostty_key_encoder_new" }); + @export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" }); + @export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" }); + @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); } } diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig new file mode 100644 index 000000000..96754d884 --- /dev/null +++ b/src/terminal/c/key_encode.zig @@ -0,0 +1,269 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key_encode = @import("../../input/key_encode.zig"); +const key_event = @import("key_event.zig"); +const KittyFlags = @import("../../terminal/kitty/key.zig").Flags; +const OptionAsAlt = @import("../../input/config.zig").OptionAsAlt; +const Result = @import("result.zig").Result; +const KeyEvent = @import("key_event.zig").Event; + +/// Wrapper around key encoding options that tracks the allocator for C API usage. +const KeyEncoderWrapper = struct { + opts: key_encode.Options, + alloc: Allocator, +}; + +/// C: GhosttyKeyEncoder +pub const Encoder = ?*KeyEncoderWrapper; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Encoder, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(KeyEncoderWrapper) catch + return .out_of_memory; + ptr.* = .{ + .opts = .{}, + .alloc = alloc, + }; + result.* = ptr; + return .success; +} + +pub fn free(encoder_: Encoder) callconv(.c) void { + const wrapper = encoder_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +/// C: GhosttyKeyEncoderOption +pub const Option = enum(c_int) { + cursor_key_application = 0, + keypad_key_application = 1, + ignore_keypad_with_numlock = 2, + alt_esc_prefix = 3, + modify_other_keys_state_2 = 4, + kitty_flags = 5, + macos_option_as_alt = 6, + + /// Input type expected for setting the option. + pub fn InType(comptime self: Option) type { + return switch (self) { + .cursor_key_application, + .keypad_key_application, + .ignore_keypad_with_numlock, + .alt_esc_prefix, + .modify_other_keys_state_2, + => bool, + .kitty_flags => u8, + .macos_option_as_alt => OptionAsAlt, + }; + } +}; + +pub fn setopt( + encoder_: Encoder, + option: Option, + value: ?*const anyopaque, +) callconv(.c) void { + return switch (option) { + inline else => |comptime_option| setoptTyped( + encoder_, + comptime_option, + @ptrCast(@alignCast(value orelse return)), + ), + }; +} + +fn setoptTyped( + encoder_: Encoder, + comptime option: Option, + value: *const option.InType(), +) void { + const opts = &encoder_.?.opts; + switch (option) { + .cursor_key_application => opts.cursor_key_application = value.*, + .keypad_key_application => opts.keypad_key_application = value.*, + .ignore_keypad_with_numlock => opts.ignore_keypad_with_numlock = value.*, + .alt_esc_prefix => opts.alt_esc_prefix = value.*, + .modify_other_keys_state_2 => opts.modify_other_keys_state_2 = value.*, + .kitty_flags => opts.kitty_flags = flags: { + const bits: u5 = @truncate(value.*); + break :flags @bitCast(bits); + }, + .macos_option_as_alt => opts.macos_option_as_alt = value.*, + } +} + +pub fn encode( + encoder_: Encoder, + event_: KeyEvent, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(.c) Result { + // Attempt to write to this buffer + var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{}); + key_encode.encode( + &writer, + event_.?.event, + encoder_.?.opts, + ) catch |err| switch (err) { + error.WriteFailed => { + // If we don't have space, use a discarding writer to count + // how much space we would have needed. + var discarding: std.Io.Writer.Discarding = .init(&.{}); + key_encode.encode( + &discarding.writer, + event_.?.event, + encoder_.?.opts, + ) catch unreachable; + + out_written.* = discarding.count; + return .out_of_memory; + }, + }; + + out_written.* = writer.end; + return .success; +} + +test "alloc" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "setopt bool" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting bool options + const val_true: bool = true; + setopt(e, .cursor_key_application, &val_true); + try testing.expect(e.?.opts.cursor_key_application); + + const val_false: bool = false; + setopt(e, .cursor_key_application, &val_false); + try testing.expect(!e.?.opts.cursor_key_application); + + setopt(e, .keypad_key_application, &val_true); + try testing.expect(e.?.opts.keypad_key_application); +} + +test "setopt kitty flags" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting kitty flags + const flags: KittyFlags = .{ + .disambiguate = true, + .report_events = true, + }; + const flags_int: u8 = @intCast(flags.int()); + setopt(e, .kitty_flags, &flags_int); + try testing.expect(e.?.opts.kitty_flags.disambiguate); + try testing.expect(e.?.opts.kitty_flags.report_events); + try testing.expect(!e.?.opts.kitty_flags.report_alternates); +} + +test "setopt macos option as alt" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting option as alt + const opt_left: OptionAsAlt = .left; + setopt(e, .macos_option_as_alt, &opt_left); + try testing.expectEqual(OptionAsAlt.left, e.?.opts.macos_option_as_alt); + + const opt_true: OptionAsAlt = .true; + setopt(e, .macos_option_as_alt, &opt_true); + try testing.expectEqual(OptionAsAlt.true, e.?.opts.macos_option_as_alt); +} + +test "encode: kitty ctrl release with ctrl mod set" { + const testing = std.testing; + + // Create encoder + var encoder: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &encoder, + )); + defer free(encoder); + + // Set kitty flags with all features enabled + { + const flags: KittyFlags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = true, + .report_associated = true, + }; + const flags_int: u8 = @intCast(flags.int()); + setopt(encoder, .kitty_flags, &flags_int); + } + + // Create key event + var event: key_event.Event = undefined; + try testing.expectEqual(Result.success, key_event.new( + &lib_alloc.test_allocator, + &event, + )); + defer key_event.free(event); + + // Set event properties: release action, ctrl key, ctrl modifier + key_event.set_action(event, .release); + key_event.set_key(event, .control_left); + key_event.set_mods(event, .{ .ctrl = true }); + + // Encode null should give us the length required + var required: usize = 0; + try testing.expectEqual(Result.out_of_memory, encode( + encoder, + event, + null, + 0, + &required, + )); + + // Encode the key event + var buf: [128]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expectEqual(required, written); + + // Expected: ESC[57442;5:3u (ctrl key code with mods and release event) + const actual = buf[0..written]; + try testing.expectEqualStrings("\x1b[57442;5:3u", actual); +} diff --git a/src/terminal/c/key_event.zig b/src/terminal/c/key_event.zig new file mode 100644 index 000000000..5befe4384 --- /dev/null +++ b/src/terminal/c/key_event.zig @@ -0,0 +1,253 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key = @import("../../input/key.zig"); +const Result = @import("result.zig").Result; + +/// Wrapper around KeyEvent that tracks the allocator for C API usage. +/// The UTF-8 text is not owned by this wrapper - the caller is responsible +/// for ensuring the lifetime of any UTF-8 text set via set_utf8. +const KeyEventWrapper = struct { + event: key.KeyEvent = .{}, + alloc: Allocator, +}; + +/// C: GhosttyKeyEvent +pub const Event = ?*KeyEventWrapper; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Event, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(KeyEventWrapper) catch + return .out_of_memory; + ptr.* = .{ .alloc = alloc }; + result.* = ptr; + return .success; +} + +pub fn free(event_: Event) callconv(.c) void { + const wrapper = event_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +pub fn set_action(event_: Event, action: key.Action) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.action = action; +} + +pub fn get_action(event_: Event) callconv(.c) key.Action { + const event: *key.KeyEvent = &event_.?.event; + return event.action; +} + +pub fn set_key(event_: Event, k: key.Key) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.key = k; +} + +pub fn get_key(event_: Event) callconv(.c) key.Key { + const event: *key.KeyEvent = &event_.?.event; + return event.key; +} + +pub fn set_mods(event_: Event, mods: key.Mods) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.mods = mods; +} + +pub fn get_mods(event_: Event) callconv(.c) key.Mods { + const event: *key.KeyEvent = &event_.?.event; + return event.mods; +} + +pub fn set_consumed_mods(event_: Event, consumed_mods: key.Mods) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.consumed_mods = consumed_mods; +} + +pub fn get_consumed_mods(event_: Event) callconv(.c) key.Mods { + const event: *key.KeyEvent = &event_.?.event; + return event.consumed_mods; +} + +pub fn set_composing(event_: Event, composing: bool) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.composing = composing; +} + +pub fn get_composing(event_: Event) callconv(.c) bool { + const event: *key.KeyEvent = &event_.?.event; + return event.composing; +} + +pub fn set_utf8(event_: Event, utf8: ?[*]const u8, len: usize) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.utf8 = if (utf8) |ptr| ptr[0..len] else ""; +} + +pub fn get_utf8(event_: Event, len: ?*usize) callconv(.c) ?[*]const u8 { + const event: *key.KeyEvent = &event_.?.event; + if (len) |l| l.* = event.utf8.len; + return if (event.utf8.len == 0) null else event.utf8.ptr; +} + +pub fn set_unshifted_codepoint(event_: Event, codepoint: u32) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.unshifted_codepoint = @truncate(codepoint); +} + +pub fn get_unshifted_codepoint(event_: Event) callconv(.c) u32 { + const event: *key.KeyEvent = &event_.?.event; + return event.unshifted_codepoint; +} + +test "alloc" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "set" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test action + set_action(e, .press); + try testing.expectEqual(key.Action.press, e.?.event.action); + + // Test key + set_key(e, .key_a); + try testing.expectEqual(key.Key.key_a, e.?.event.key); + + // Test mods + const mods: key.Mods = .{ .shift = true, .ctrl = true }; + set_mods(e, mods); + try testing.expect(e.?.event.mods.shift); + try testing.expect(e.?.event.mods.ctrl); + + // Test consumed mods + const consumed: key.Mods = .{ .shift = true }; + set_consumed_mods(e, consumed); + try testing.expect(e.?.event.consumed_mods.shift); + try testing.expect(!e.?.event.consumed_mods.ctrl); + + // Test composing + set_composing(e, true); + try testing.expect(e.?.event.composing); + + // Test UTF-8 + const text = "hello"; + set_utf8(e, text.ptr, text.len); + try testing.expectEqualStrings(text, e.?.event.utf8); + + // Test UTF-8 null + set_utf8(e, null, 0); + try testing.expectEqualStrings("", e.?.event.utf8); + + // Test unshifted codepoint + set_unshifted_codepoint(e, 'a'); + try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint); +} + +test "get" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Set some values + set_action(e, .repeat); + set_key(e, .key_z); + + const mods: key.Mods = .{ .alt = true, .super = true }; + set_mods(e, mods); + + const consumed: key.Mods = .{ .alt = true }; + set_consumed_mods(e, consumed); + + set_composing(e, true); + + const text = "test"; + set_utf8(e, text.ptr, text.len); + + set_unshifted_codepoint(e, 'z'); + + // Get them back + try testing.expectEqual(key.Action.repeat, get_action(e)); + try testing.expectEqual(key.Key.key_z, get_key(e)); + + const got_mods = get_mods(e); + try testing.expect(got_mods.alt); + try testing.expect(got_mods.super); + + const got_consumed = get_consumed_mods(e); + try testing.expect(got_consumed.alt); + try testing.expect(!got_consumed.super); + + try testing.expect(get_composing(e)); + + var utf8_len: usize = undefined; + const got_utf8 = get_utf8(e, &utf8_len); + try testing.expect(got_utf8 != null); + try testing.expectEqual(@as(usize, 4), utf8_len); + try testing.expectEqualStrings("test", got_utf8.?[0..utf8_len]); + + try testing.expectEqual(@as(u32, 'z'), get_unshifted_codepoint(e)); +} + +test "complete key event" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Build a complete key event for shift+a + set_action(e, .press); + set_key(e, .key_a); + + const mods: key.Mods = .{ .shift = true }; + set_mods(e, mods); + + const consumed: key.Mods = .{ .shift = true }; + set_consumed_mods(e, consumed); + + const text = "A"; + set_utf8(e, text.ptr, text.len); + + set_unshifted_codepoint(e, 'a'); + + // Verify all fields + try testing.expectEqual(key.Action.press, e.?.event.action); + try testing.expectEqual(key.Key.key_a, e.?.event.key); + try testing.expect(e.?.event.mods.shift); + try testing.expect(e.?.event.consumed_mods.shift); + try testing.expectEqualStrings("A", e.?.event.utf8); + try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint); + + // Also test the getter + var utf8_len: usize = undefined; + const got_utf8 = get_utf8(e, &utf8_len); + try testing.expect(got_utf8 != null); + try testing.expectEqual(@as(usize, 1), utf8_len); + try testing.expectEqualStrings("A", got_utf8.?[0..utf8_len]); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 68fd77edd..500dbf56c 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,4 +1,6 @@ pub const osc = @import("osc.zig"); +pub const key_event = @import("key_event.zig"); +pub const key_encode = @import("key_encode.zig"); // The full C API, unexported. pub const osc_new = osc.new; @@ -9,8 +11,32 @@ pub const osc_end = osc.end; pub const osc_command_type = osc.commandType; pub const osc_command_data = osc.commandData; +pub const key_event_new = key_event.new; +pub const key_event_free = key_event.free; +pub const key_event_set_action = key_event.set_action; +pub const key_event_get_action = key_event.get_action; +pub const key_event_set_key = key_event.set_key; +pub const key_event_get_key = key_event.get_key; +pub const key_event_set_mods = key_event.set_mods; +pub const key_event_get_mods = key_event.get_mods; +pub const key_event_set_consumed_mods = key_event.set_consumed_mods; +pub const key_event_get_consumed_mods = key_event.get_consumed_mods; +pub const key_event_set_composing = key_event.set_composing; +pub const key_event_get_composing = key_event.get_composing; +pub const key_event_set_utf8 = key_event.set_utf8; +pub const key_event_get_utf8 = key_event.get_utf8; +pub const key_event_set_unshifted_codepoint = key_event.set_unshifted_codepoint; +pub const key_event_get_unshifted_codepoint = key_event.get_unshifted_codepoint; + +pub const key_encoder_new = key_encode.new; +pub const key_encoder_free = key_encode.free; +pub const key_encoder_setopt = key_encode.setopt; +pub const key_encoder_encode = key_encode.encode; + test { _ = osc; + _ = key_event; + _ = key_encode; // We want to make sure we run the tests for the C allocator interface. _ = @import("../../lib/allocator.zig"); From fd0851bae7c7a05086bc0761b63b3dbe33a82c1c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 14:48:40 -0700 Subject: [PATCH 173/319] lib-vt: update documentation with more examples --- DoxygenLayout.xml | 269 +++++++++++++++++++++++++++++++++++++++++++ include/ghostty/vt.h | 99 ++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 DoxygenLayout.xml diff --git a/DoxygenLayout.xml b/DoxygenLayout.xml new file mode 100644 index 000000000..67497e83f --- /dev/null +++ b/DoxygenLayout.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 7815ebb81..ebb41e300 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -327,6 +327,63 @@ typedef struct { * Utilities for encoding key events into terminal escape sequences, * supporting both legacy encoding as well as Kitty Keyboard Protocol. * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_key_encoder_new() + * 2. Configure encoder options with ghostty_key_encoder_setopt(). + * 3. For each key event: + * - Create a key event with ghostty_key_event_new() + * - Set event properties (action, key, modifiers, etc.) + * - Encode with ghostty_key_encoder_encode() + * - Free the event with ghostty_key_event_free() + * - Note: You can also reuse the same key event multiple times by + * changing its properties. + * 4. Free the encoder with ghostty_key_encoder_free() when done + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create encoder + * GhosttyKeyEncoder encoder; + * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + * assert(result == GHOSTTY_SUCCESS); + * + * // Enable Kitty keyboard protocol with all features + * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, + * &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + * + * // Create and configure key event for Ctrl+C press + * GhosttyKeyEvent event; + * result = ghostty_key_event_new(NULL, &event); + * assert(result == GHOSTTY_SUCCESS); + * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); + * ghostty_key_event_set_key(event, GHOSTTY_KEY_C); + * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + * + * // Encode the key event + * char buf[128]; + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence (e.g., write to terminal) + * fwrite(buf, 1, written, stdout); + * + * // Cleanup + * ghostty_key_event_free(event); + * ghostty_key_encoder_free(encoder); + * return 0; + * } + * @endcode + * + * For a complete working example, see example/c-vt-key-encode in the + * repository. + * * @{ */ @@ -949,6 +1006,48 @@ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOpti * @param out_len Pointer to store the number of bytes written (may be NULL) * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code * + * ## Example: Calculate required buffer size + * + * @code{.c} + * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) + * size_t required = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + * assert(result == GHOSTTY_OUT_OF_MEMORY); + * + * // Allocate buffer of required size + * char *buf = malloc(required); + * + * // Encode with properly sized buffer + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence... + * + * free(buf); + * @endcode + * + * ## Example: Direct encoding with static buffer + * + * @code{.c} + * // Most escape sequences are short, so a static buffer often suffices + * char buf[128]; + * size_t written = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * if (result == GHOSTTY_SUCCESS) { + * // Write the encoded sequence to the terminal + * write(pty_fd, buf, written); + * } else if (result == GHOSTTY_OUT_OF_MEMORY) { + * // Buffer too small, written contains required size + * char *dynamic_buf = malloc(written); + * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); + * assert(result == GHOSTTY_SUCCESS); + * write(pty_fd, dynamic_buf, written); + * free(dynamic_buf); + * } + * @endcode + * * @ingroup key */ GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); From aeb6647aa6e151873e0f5cd312148306315b0c82 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 15:00:59 -0700 Subject: [PATCH 174/319] libghostty docs: use latest Doxygen --- src/build/docker/lib-c-docs/Dockerfile | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index f30dfba90..76f29ad21 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -1,3 +1,26 @@ +#-------------------------------------------------------------------- +# Build Doxygen from source +#-------------------------------------------------------------------- +FROM ubuntu:24.04 AS doxygen-builder + +ARG DOXYGEN_VERSION=1.14.0 +RUN apt-get update && apt-get install -y \ + wget \ + cmake \ + g++ \ + flex \ + bison \ + python3 \ + && rm -rf /var/lib/apt/lists/* \ + && wget -q https://www.doxygen.nl/files/doxygen-${DOXYGEN_VERSION}.src.tar.gz \ + && tar -xzf doxygen-${DOXYGEN_VERSION}.src.tar.gz \ + && cd doxygen-${DOXYGEN_VERSION} \ + && cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \ + && make -j$(nproc) \ + && make install \ + && cd .. \ + && rm -rf doxygen-${DOXYGEN_VERSION}* + #-------------------------------------------------------------------- # Generate documentation with Doxygen #-------------------------------------------------------------------- @@ -6,9 +29,9 @@ FROM ubuntu:24.04 AS builder # Build argument for noindex header ARG ADD_NOINDEX_HEADER=false RUN apt-get update && apt-get install -y \ - doxygen \ graphviz \ && rm -rf /var/lib/apt/lists/* +COPY --from=doxygen-builder /usr/local/bin/doxygen /usr/local/bin/doxygen WORKDIR /ghostty COPY include/ ./include/ COPY Doxyfile ./ From c5ea4a807907306c82017ec73a9e41805c2f0714 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 15:19:16 -0700 Subject: [PATCH 175/319] libghostty: use Arch for docs container to get later Doxygen --- src/build/docker/lib-c-docs/Dockerfile | 33 ++++---------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index 76f29ad21..5667c6607 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -1,40 +1,17 @@ -#-------------------------------------------------------------------- -# Build Doxygen from source -#-------------------------------------------------------------------- -FROM ubuntu:24.04 AS doxygen-builder - -ARG DOXYGEN_VERSION=1.14.0 -RUN apt-get update && apt-get install -y \ - wget \ - cmake \ - g++ \ - flex \ - bison \ - python3 \ - && rm -rf /var/lib/apt/lists/* \ - && wget -q https://www.doxygen.nl/files/doxygen-${DOXYGEN_VERSION}.src.tar.gz \ - && tar -xzf doxygen-${DOXYGEN_VERSION}.src.tar.gz \ - && cd doxygen-${DOXYGEN_VERSION} \ - && cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \ - && make -j$(nproc) \ - && make install \ - && cd .. \ - && rm -rf doxygen-${DOXYGEN_VERSION}* - #-------------------------------------------------------------------- # Generate documentation with Doxygen #-------------------------------------------------------------------- -FROM ubuntu:24.04 AS builder +FROM alpine:latest AS builder # Build argument for noindex header ARG ADD_NOINDEX_HEADER=false -RUN apt-get update && apt-get install -y \ - graphviz \ - && rm -rf /var/lib/apt/lists/* -COPY --from=doxygen-builder /usr/local/bin/doxygen /usr/local/bin/doxygen +RUN apk add --no-cache \ + doxygen \ + graphviz WORKDIR /ghostty COPY include/ ./include/ COPY Doxyfile ./ +COPY DoxygenLayout.xml ./ RUN mkdir -p zig-out/share/ghostty/doc/libghostty RUN doxygen From d6ef048cd7d9327cdae25d76da5820947d79646c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 5 Oct 2025 14:20:05 -0500 Subject: [PATCH 176/319] freebsd: fix CI for Zig 0.15 and enable FreeBSD 15.0 --- .github/workflows/test.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9893106dd..a0750bd90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -563,6 +563,8 @@ jobs: test: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-md + outputs: + zig_version: ${{ steps.zig.outputs.version }} env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache @@ -570,6 +572,11 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Get required Zig version + id: zig + run: | + echo "version=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig)" >> $GITHUB_OUTPUT + - name: Setup Cache uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: @@ -1137,12 +1144,11 @@ jobs: name: Build on FreeBSD needs: test runs-on: namespace-profile-mitchellh-sm-systemd - if: false # FIXME: FreeBSD does not yet ship with Zig 0.15 strategy: matrix: release: - "14.3" - # - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108 + - "15.0" steps: - name: Checkout Ghostty uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -1163,14 +1169,19 @@ jobs: devel/gettext \ devel/git \ devel/pkgconf \ + ftp/curl \ graphics/wayland \ - lang/zig \ security/ca_root_nss \ textproc/hs-pandoc \ x11-fonts/jetbrains-mono \ x11-toolkits/libadwaita \ x11-toolkits/gtk40 \ x11-toolkits/gtk4-layer-shell + curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/${{ needs.test.outputs.zig_version }}/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}.tar.xz" && \ + mkdir /opt && \ + tar -xf /tmp/zig.tar.xz -C /opt && \ + rm /tmp/zig.tar.xz && \ + ln -s "/opt/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}/zig" /usr/local/bin/zig run: | zig env From 67357c663db1f44f329f729b36c1b7d04d363e1d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 5 Oct 2025 19:08:20 -0500 Subject: [PATCH 177/319] freebsd: add a timeout to the freebsd job --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0750bd90..59556f58e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1149,6 +1149,7 @@ jobs: release: - "14.3" - "15.0" + timeout-minutes: 10 steps: - name: Checkout Ghostty uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 From 34cb77c9f207d73077dbe87e7484ba3f56c884a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 00:10:09 +0000 Subject: [PATCH 178/319] build(deps): bump softprops/action-gh-release from 2.3.3 to 2.3.4 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.3 to 2.3.4. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/6cbd405e2c4e67a21c47fa9e383d020e4e28b836...62c96d0c4e8a889135c1f3a25910db8dbe0e85f7) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.3.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 7f7b85e2f..eca678244 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -188,7 +188,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -359,7 +359,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -590,7 +590,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -775,7 +775,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From f03344b1c61b1867afae4df859f099460c2319a0 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 5 Oct 2025 19:46:04 -0500 Subject: [PATCH 179/319] osc: reorder osc tests and name them consistently --- src/terminal/osc.zig | 1706 ++++++++++++++++++------------------ src/terminal/osc/color.zig | 22 +- 2 files changed, 888 insertions(+), 840 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 897a5ef0f..1d41d95f2 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1691,7 +1691,7 @@ test { _ = osc_color; } -test "OSC: change_window_title" { +test "OSC 0: change_window_title" { const testing = std.testing; var p: Parser = .init(); @@ -1704,7 +1704,62 @@ test "OSC: change_window_title" { try testing.expectEqualStrings("ab", cmd.change_window_title); } -test "OSC: change_window_title with 2" { +test "OSC 0: longer than buffer" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); + for (input) |ch| p.next(ch); + + try testing.expect(p.end(null) == null); + try testing.expect(p.complete == false); +} + +test "OSC 0: one shorter than buffer length" { + const testing = std.testing; + + var p: Parser = .init(); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings(title, cmd.change_window_title); +} + +test "OSC 0: exactly at buffer length" { + const testing = std.testing; + + var p: Parser = .init(); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - prefix.len); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + // This should be null because we always reserve space for a null terminator. + try testing.expect(p.end(null) == null); + try testing.expect(p.complete == false); +} + +test "OSC 1: change_window_icon" { + const testing = std.testing; + + var p: Parser = .init(); + p.next('1'); + p.next(';'); + p.next('a'); + p.next('b'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_icon); + try testing.expectEqualStrings("ab", cmd.change_window_icon); +} + +test "OSC 2: change_window_title with 2" { const testing = std.testing; var p: Parser = .init(); @@ -1717,7 +1772,7 @@ test "OSC: change_window_title with 2" { try testing.expectEqualStrings("ab", cmd.change_window_title); } -test "OSC: change_window_title with utf8" { +test "OSC 2: change_window_title with utf8" { const testing = std.testing; var p: Parser = .init(); @@ -1739,7 +1794,7 @@ test "OSC: change_window_title with utf8" { try testing.expectEqualStrings("— ‐", cmd.change_window_title); } -test "OSC: change_window_title empty" { +test "OSC 2: change_window_title empty" { const testing = std.testing; var p: Parser = .init(); @@ -1750,207 +1805,23 @@ test "OSC: change_window_title empty" { try testing.expectEqualStrings("", cmd.change_window_title); } -test "OSC: change_window_icon" { - const testing = std.testing; - - var p: Parser = .init(); - p.next('1'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_icon); - try testing.expectEqualStrings("ab", cmd.change_window_icon); -} - -test "OSC: prompt_start" { +test "OSC 4: empty param" { const testing = std.testing; var p: Parser = .init(); - const input = "133;A"; + const input = "4;;"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.aid == null); - try testing.expect(cmd.prompt_start.redraw); + const cmd = p.end('\x1b'); + try testing.expect(cmd == null); } -test "OSC: prompt_start with single option" { - const testing = std.testing; +// See src/terminal/osc/color.zig for more OSC 4 tests. - var p: Parser = .init(); +// See src/terminal/osc/color.zig for OSC 5 tests. - const input = "133;A;aid=14"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); -} - -test "OSC: prompt_start with redraw disabled" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;A;redraw=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(!cmd.prompt_start.redraw); -} - -test "OSC: prompt_start with redraw invalid value" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;A;redraw=42"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.redraw); - try testing.expect(cmd.prompt_start.kind == .primary); -} - -test "OSC: prompt_start with continuation" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;A;k=c"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .continuation); -} - -test "OSC: prompt_start with secondary" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;A;k=s"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .secondary); -} - -test "OSC: end_of_command no exit code" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;D"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_command); -} - -test "OSC: end_of_command with exit code" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;D;25"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_command); - try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); -} - -test "OSC: prompt_end" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;B"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_end); -} - -test "OSC: end_of_input" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;C"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); -} - -test "OSC: get/set clipboard" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "52;s;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 's'); - try testing.expectEqualStrings("?", cmd.clipboard_contents.data); -} - -test "OSC: get/set clipboard (optional parameter)" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "52;;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 'c'); - try testing.expectEqualStrings("?", cmd.clipboard_contents.data); -} - -test "OSC: get/set clipboard with allocator" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "52;s;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 's'); - try testing.expectEqualStrings("?", cmd.clipboard_contents.data); -} - -test "OSC: clear clipboard" { - const testing = std.testing; - - var p: Parser = .init(); - defer p.deinit(); - - const input = "52;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 'c'); - try testing.expectEqualStrings("", cmd.clipboard_contents.data); -} - -test "OSC: report pwd" { +test "OSC 7: report pwd" { const testing = std.testing; var p: Parser = .init(); @@ -1963,7 +1834,7 @@ test "OSC: report pwd" { try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); } -test "OSC: report pwd empty" { +test "OSC 7: report pwd empty" { const testing = std.testing; var p: Parser = .init(); @@ -1975,610 +1846,7 @@ test "OSC: report pwd empty" { try testing.expectEqualStrings("", cmd.report_pwd.value); } -test "OSC: pointer cursor" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "22;pointer"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .mouse_shape); - try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); -} - -test "OSC: longer than buffer" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); - for (input) |ch| p.next(ch); - - try testing.expect(p.end(null) == null); - try testing.expect(p.complete == false); -} - -test "OSC: one shorter than buffer length" { - const testing = std.testing; - - var p: Parser = .init(); - - const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); - const input = prefix ++ title; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings(title, cmd.change_window_title); -} - -test "OSC: exactly at buffer length" { - const testing = std.testing; - - var p: Parser = .init(); - - const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len); - const input = prefix ++ title; - for (input) |ch| p.next(ch); - - // This should be null because we always reserve space for a null terminator. - try testing.expect(p.end(null) == null); - try testing.expect(p.complete == false); -} - -test "OSC: OSC 9;1 ConEmu sleep" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;1;420"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); -} - -test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;1;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); -} - -test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;1;12345"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); -} - -test "OSC: OSC 9;1 conemu sleep invalid input" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;1;foo"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); -} - -test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;1"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;1a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9 show desktop notification" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;Hello world"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("", cmd.show_desktop_notification.title); - try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9 show single character desktop notification" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;H"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("", cmd.show_desktop_notification.title); - try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 777 show desktop notification with title" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "777;notify;Title;Body"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); - try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); -} - -test "OSC: OSC 9;2 ConEmu message box" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;2;hello world"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_show_message_box); - try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); -} - -test "OSC: 9;2 ConEmu message box invalid input" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;2"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); -} - -test "OSC: 9;2 ConEmu message box empty message" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;2;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_show_message_box); - try testing.expectEqualStrings("", cmd.conemu_show_message_box); -} - -test "OSC: 9;2 ConEmu message box spaces only message" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;2; "; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_show_message_box); - try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); -} - -test "OSC: OSC 9;2 message box -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;2"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;2 message box -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;2a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); -} - -test "OSC: 9;3 ConEmu change tab title" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;3;foo bar"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_change_tab_title); - try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); -} - -test "OSC: 9;3 ConEmu change tab title reset" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;3;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - const expected_command: Command = .{ .conemu_change_tab_title = .reset }; - try testing.expectEqual(expected_command, cmd); -} - -test "OSC: 9;3 ConEmu change tab title spaces only" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;3; "; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_change_tab_title); - try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); -} - -test "OSC: OSC 9;3 change tab title -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;3"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;3 message box -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;3a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;4 ConEmu progress set" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;1;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expect(cmd.conemu_progress_report.progress == 100); -} - -test "OSC: OSC 9;4 ConEmu progress set overflow" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;1;900"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expectEqual(100, cmd.conemu_progress_report.progress); -} - -test "OSC: OSC 9;4 ConEmu progress set single digit" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;1;9"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expect(cmd.conemu_progress_report.progress == 9); -} - -test "OSC: OSC 9;4 ConEmu progress set double digit" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;1;94"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expectEqual(94, cmd.conemu_progress_report.progress); -} - -test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;1;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expectEqual(100, cmd.conemu_progress_report.progress); -} - -test "OSC: OSC 9;4 ConEmu progress remove with no progress" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;0;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;0;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC: OSC 9;4 ConEmu progress remove ignores progress" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;0;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;0;100;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); -} - -test "OSC: OSC 9;4 ConEmu progress error" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;2"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .@"error"); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC: OSC 9;4 ConEmu progress error with progress" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;2;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .@"error"); - try testing.expect(cmd.conemu_progress_report.progress == 100); -} - -test "OSC: OSC 9;4 progress pause" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;4"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .pause); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC: OSC 9;4 ConEmu progress pause with progress" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;4;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .pause); - try testing.expect(cmd.conemu_progress_report.progress == 100); -} - -test "OSC: OSC 9;4 progress -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;4 progress -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;4 progress -> desktop notification 3" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;5"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;4 progress -> desktop notification 4" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;4;5a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); -} - -test "OSC: OSC 9;5 ConEmu wait input" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;5"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_wait_input); -} - -test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;5;foo"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_wait_input); -} - -test "OSC: empty param" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "4;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); -} - -test "OSC: hyperlink" { +test "OSC 8: hyperlink" { const testing = std.testing; var p: Parser = .init(); @@ -2591,7 +1859,7 @@ test "OSC: hyperlink" { try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with id set" { +test "OSC 8: hyperlink with id set" { const testing = std.testing; var p: Parser = .init(); @@ -2605,7 +1873,7 @@ test "OSC: hyperlink with id set" { try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with empty id" { +test "OSC 8: hyperlink with empty id" { const testing = std.testing; var p: Parser = .init(); @@ -2619,7 +1887,7 @@ test "OSC: hyperlink with empty id" { try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with incomplete key" { +test "OSC 8: hyperlink with incomplete key" { const testing = std.testing; var p: Parser = .init(); @@ -2633,7 +1901,7 @@ test "OSC: hyperlink with incomplete key" { try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with empty key" { +test "OSC 8: hyperlink with empty key" { const testing = std.testing; var p: Parser = .init(); @@ -2647,7 +1915,7 @@ test "OSC: hyperlink with empty key" { try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with empty key and id" { +test "OSC 8: hyperlink with empty key and id" { const testing = std.testing; var p: Parser = .init(); @@ -2661,7 +1929,7 @@ test "OSC: hyperlink with empty key and id" { try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with empty uri" { +test "OSC 8: hyperlink with empty uri" { const testing = std.testing; var p: Parser = .init(); @@ -2673,7 +1941,7 @@ test "OSC: hyperlink with empty uri" { try testing.expect(cmd == null); } -test "OSC: hyperlink end" { +test "OSC 8: hyperlink end" { const testing = std.testing; var p: Parser = .init(); @@ -2685,7 +1953,591 @@ test "OSC: hyperlink end" { try testing.expect(cmd == .hyperlink_end); } -test "OSC: kitty color protocol" { +test "OSC 9: show desktop notification" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;Hello world"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); +} + +test "OSC 9: show single character desktop notification" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;H"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); +} + +test "OSC 9;1: ConEmu sleep" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1;420"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: ConEmu sleep with no value default to 100ms" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: conemu sleep cannot exceed 10000ms" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1;12345"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: conemu sleep invalid input" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1;foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: conemu sleep -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); +} + +test "OSC 9;1: conemu sleep -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); +} + +test "OSC 9;2: ConEmu message box" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2;hello world"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); +} + +test "OSC 9;2: ConEmu message box invalid input" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); +} + +test "OSC 9;2: ConEmu message box empty message" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("", cmd.conemu_show_message_box); +} + +test "OSC 9;2: ConEmu message box spaces only message" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2; "; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); +} + +test "OSC 9;2: message box -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); +} + +test "OSC 9;2: message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); +} + +test "OSC 9;3: ConEmu change tab title" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3;foo bar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); +} + +test "OSC 9;3: ConEmu change tab title reset" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + const expected_command: Command = .{ .conemu_change_tab_title = .reset }; + try testing.expectEqual(expected_command, cmd); +} + +test "OSC 9;3: ConEmu change tab title spaces only" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3; "; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); +} + +test "OSC 9;3: change tab title -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); +} + +test "OSC 9;3: message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: ConEmu progress set" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;1;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expect(cmd.conemu_progress_report.progress == 100); +} + +test "OSC 9;4: ConEmu progress set overflow" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;1;900"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(100, cmd.conemu_progress_report.progress); +} + +test "OSC 9;4: ConEmu progress set single digit" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;1;9"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expect(cmd.conemu_progress_report.progress == 9); +} + +test "OSC 9;4: ConEmu progress set double digit" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;1;94"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(94, cmd.conemu_progress_report.progress); +} + +test "OSC 9;4: ConEmu progress set extra semicolon ignored" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;1;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(100, cmd.conemu_progress_report.progress); +} + +test "OSC 9;4: ConEmu progress remove with no progress" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;0;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress remove with double semicolon" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;0;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress remove ignores progress" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;0;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress remove extra semicolon" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;0;100;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); +} + +test "OSC 9;4: ConEmu progress error" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .@"error"); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress error with progress" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;2;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .@"error"); + try testing.expect(cmd.conemu_progress_report.progress == 100); +} + +test "OSC 9;4: progress pause" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .pause); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress pause with progress" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;4;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .pause); + try testing.expect(cmd.conemu_progress_report.progress == 100); +} + +test "OSC 9;4: progress -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: progress -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: progress -> desktop notification 3" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;5"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: progress -> desktop notification 4" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;5a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); +} + +test "OSC 9;5: ConEmu wait input" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;5"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_wait_input); +} + +test "OSC 9;5: ConEmu wait ignores trailing characters" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;5;foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_wait_input); +} + +test "OSC 9;6: ConEmu guimacro 1" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6;a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("a", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 2" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("ab", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); +} + +// See src/terminal/osc/color.zig for OSC 10 tests. + +// See src/terminal/osc/color.zig for OSC 11 tests. + +// See src/terminal/osc/color.zig for OSC 12 tests. + +// See src/terminal/osc/color.zig for OSC 13 tests. + +// See src/terminal/osc/color.zig for OSC 14 tests. + +// See src/terminal/osc/color.zig for OSC 15 tests. + +// See src/terminal/osc/color.zig for OSC 16 tests. + +// See src/terminal/osc/color.zig for OSC 17 tests. + +// See src/terminal/osc/color.zig for OSC 18 tests. + +// See src/terminal/osc/color.zig for OSC 19 tests. + +test "OSC 21: kitty color protocol" { const testing = std.testing; const Kind = kitty_color.Kind; @@ -2757,7 +2609,7 @@ test "OSC: kitty color protocol" { } } -test "OSC: kitty color protocol without allocator" { +test "OSC 21: kitty color protocol without allocator" { const testing = std.testing; var p: Parser = .init(); @@ -2768,7 +2620,7 @@ test "OSC: kitty color protocol without allocator" { try testing.expect(p.end('\x1b') == null); } -test "OSC: kitty color protocol double reset" { +test "OSC 21: kitty color protocol double reset" { const testing = std.testing; var p: Parser = .initAlloc(testing.allocator); @@ -2784,7 +2636,7 @@ test "OSC: kitty color protocol double reset" { p.reset(); } -test "OSC: kitty color protocol reset after invalid" { +test "OSC 21: kitty color protocol reset after invalid" { const testing = std.testing; var p: Parser = .initAlloc(testing.allocator); @@ -2805,7 +2657,7 @@ test "OSC: kitty color protocol reset after invalid" { p.reset(); } -test "OSC: kitty color protocol no key" { +test "OSC 21: kitty color protocol no key" { const testing = std.testing; var p: Parser = .initAlloc(testing.allocator); @@ -2819,44 +2671,240 @@ test "OSC: kitty color protocol no key" { try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); } -test "OSC: 9;6: ConEmu guimacro 1" { +test "OSC 22: pointer cursor" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); + var p: Parser = .init(); - const input = "9;6;a"; + const input = "22;pointer"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_guimacro); - try testing.expectEqualStrings("a", cmd.conemu_guimacro); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .mouse_shape); + try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); } -test "OSC: 9;6: ConEmu guimacro 2" { +test "OSC 52: get/set clipboard" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); + var p: Parser = .init(); - const input = "9;6;ab"; + const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_guimacro); - try testing.expectEqualStrings("ab", cmd.conemu_guimacro); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 's'); + try testing.expectEqualStrings("?", cmd.clipboard_contents.data); } -test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { +test "OSC 52: get/set clipboard (optional parameter)" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "52;;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 'c'); + try testing.expectEqualStrings("?", cmd.clipboard_contents.data); +} + +test "OSC 52: get/set clipboard with allocator" { const testing = std.testing; var p: Parser = .initAlloc(testing.allocator); defer p.deinit(); - const input = "9;6"; + const input = "52;s;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 's'); + try testing.expectEqualStrings("?", cmd.clipboard_contents.data); +} + +test "OSC 52: clear clipboard" { + const testing = std.testing; + + var p: Parser = .init(); + defer p.deinit(); + + const input = "52;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 'c'); + try testing.expectEqualStrings("", cmd.clipboard_contents.data); +} + +// See src/terminal/osc/color.zig for OSC 104 tests. + +// See src/terminal/osc/color.zig for OSC 105 tests. + +// See src/terminal/osc/color.zig for OSC 110 tests. + +// See src/terminal/osc/color.zig for OSC 111 tests. + +// See src/terminal/osc/color.zig for OSC 112 tests. + +// See src/terminal/osc/color.zig for OSC 113 tests. + +// See src/terminal/osc/color.zig for OSC 114 tests. + +// See src/terminal/osc/color.zig for OSC 115 tests. + +// See src/terminal/osc/color.zig for OSC 116 tests. + +// See src/terminal/osc/color.zig for OSC 117 tests. + +// See src/terminal/osc/color.zig for OSC 118 tests. + +// See src/terminal/osc/color.zig for OSC 119 tests. + +test "OSC 133: prompt_start" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.aid == null); + try testing.expect(cmd.prompt_start.redraw); +} + +test "OSC 133: prompt_start with single option" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;aid=14"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); +} + +test "OSC 133: prompt_start with redraw disabled" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;redraw=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(!cmd.prompt_start.redraw); +} + +test "OSC 133: prompt_start with redraw invalid value" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;redraw=42"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.redraw); + try testing.expect(cmd.prompt_start.kind == .primary); +} + +test "OSC 133: prompt_start with continuation" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;k=c"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.kind == .continuation); +} + +test "OSC 133: prompt_start with secondary" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;k=s"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.kind == .secondary); +} + +test "OSC 133: end_of_command no exit code" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;D"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_command); +} + +test "OSC 133: end_of_command with exit code" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;D;25"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_command); + try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); +} + +test "OSC 133: prompt_end" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;B"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_end); +} + +test "OSC 133: end_of_input" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); +} + +test "OSC: OSC 777 show desktop notification with title" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "777;notify;Title;Body"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); + try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); + try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); } diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 8a8e8b942..9fd81ed63 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -279,7 +279,7 @@ pub const ColoredTarget = struct { color: RGB, }; -test "osc4" { +test "OSC 4:" { const testing = std.testing; const alloc = testing.allocator; @@ -401,7 +401,7 @@ test "osc4" { } } -test "osc5" { +test "OSC 5:" { const testing = std.testing; const alloc = testing.allocator; @@ -433,7 +433,7 @@ test "osc5" { } } -test "osc4: multiple requests" { +test "OSC 4: multiple requests" { const testing = std.testing; const alloc = testing.allocator; @@ -489,7 +489,7 @@ test "osc4: multiple requests" { } } -test "osc104" { +test "OSC 104:" { const testing = std.testing; const alloc = testing.allocator; @@ -540,7 +540,7 @@ test "osc104" { } } -test "osc104 empty index" { +test "OSC 104: empty index" { const testing = std.testing; const alloc = testing.allocator; @@ -557,7 +557,7 @@ test "osc104 empty index" { ); } -test "osc104 invalid index" { +test "OSC 104: invalid index" { const testing = std.testing; const alloc = testing.allocator; @@ -570,7 +570,7 @@ test "osc104 invalid index" { ); } -test "osc104 reset all" { +test "OSC 104: reset all" { const testing = std.testing; const alloc = testing.allocator; @@ -583,7 +583,7 @@ test "osc104 reset all" { ); } -test "osc105 reset all" { +test "OSC 105: reset all" { const testing = std.testing; const alloc = testing.allocator; @@ -597,7 +597,7 @@ test "osc105 reset all" { } // OSC 10-19: Get/Set Dynamic Colors -test "dynamic" { +test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: OSC 19: dynamic" { const testing = std.testing; const alloc = testing.allocator; @@ -625,7 +625,7 @@ test "dynamic" { } } -test "dynamic multiple" { +test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: OSC 19: dynamic multiple" { const testing = std.testing; const alloc = testing.allocator; @@ -657,7 +657,7 @@ test "dynamic multiple" { } // OSC 110-119: Reset Dynamic Colors -test "reset dynamic" { +test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: OSC 118: OSC 119: reset dynamic" { const testing = std.testing; const alloc = testing.allocator; From a249b3da3a1c84de94d26dd83aa0b190bdd29739 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 5 Oct 2025 20:44:52 -0500 Subject: [PATCH 180/319] linux cgroup: fix initialization --- src/os/cgroup.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 97c796f8b..761386a2f 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -19,9 +19,7 @@ pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { defer file.close(); // Read it all into memory -- we don't expect this file to ever be that large. - var reader_buf: [4096]u8 = undefined; - var reader = file.reader(&reader_buf); - const contents = try reader.interface.readAlloc( + const contents = try file.readToEndAlloc( alloc, 1 * 1024 * 1024, // 1MB ); From d9de5909d9b3f95c3a91ed008d2246eb1aebcd99 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 5 Oct 2025 20:55:12 -0500 Subject: [PATCH 181/319] linux cgroup: also fix controllers() This fix was found by Claude Code, but I manually reviewed this change and removed extraneous changes made by the AI tool. Co-authored-by: moderation --- src/os/cgroup.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 761386a2f..4b5ccc4d3 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -185,9 +185,7 @@ pub fn controllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { // Read it all into memory -- we don't expect this file to ever // be that large. - var reader_buf: [4096]u8 = undefined; - var reader = file.reader(&reader_buf); - const contents = try reader.interface.readAlloc( + const contents = try file.readToEndAlloc( alloc, 1 * 1024 * 1024, // 1MB ); From a73a67d252678fafc7cfbb8cb0333434b01af01c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 19:59:58 -0700 Subject: [PATCH 182/319] doxygen improvements --- Doxyfile | 45 ++++++++++++++++++ DoxygenLayout.xml | 28 ++--------- dist/doxygen/favicon.png | Bin 0 -> 1562 bytes dist/doxygen/ghostty.css | 99 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 dist/doxygen/favicon.png create mode 100644 dist/doxygen/ghostty.css diff --git a/Doxyfile b/Doxyfile index fccd4a493..d0c0414dc 100644 --- a/Doxyfile +++ b/Doxyfile @@ -2,9 +2,38 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "libghostty" +PROJECT_LOGO = images/gnome/64.png INPUT = include/ghostty/vt.h INPUT_ENCODING = UTF-8 RECURSIVE = NO +FULL_PATH_NAMES = NO +STRIP_FROM_INC_PATH = include +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +REFERENCES_RELATION = YES +REFERENCED_BY_RELATION = YES + +#--------------------------------------------------------------------------- +# C API Optimization +#--------------------------------------------------------------------------- + +# Optimize output for C API documentation +OPTIMIZE_OUTPUT_FOR_C = YES +TYPEDEF_HIDES_STRUCT = YES +HIDE_SCOPE_NAMES = YES + +# Clean path names +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = . +STRIP_FROM_INC_PATH = include + +# Hide undocumented and internal APIs +HIDE_UNDOC_MEMBERS = YES +HIDE_UNDOC_CLASSES = YES +EXTRACT_ALL = NO +INTERNAL_DOCS = NO +EXTRACT_PRIVATE = NO +EXTRACT_LOCAL_CLASSES = NO #--------------------------------------------------------------------------- # HTML Output @@ -12,6 +41,21 @@ RECURSIVE = NO GENERATE_HTML = YES HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty +HTML_EXTRA_STYLESHEET = dist/doxygen/ghostty.css +HTML_EXTRA_FILES = dist/doxygen/favicon.png +HTML_COLORSTYLE = DARK +LAYOUT_FILE = DoxygenLayout.xml +GENERATE_TREEVIEW = YES +HTML_DYNAMIC_SECTIONS = YES +SEARCHENGINE = YES +ALPHABETICAL_INDEX = YES +HTML_TIMESTAMP = NO + +#--------------------------------------------------------------------------- +# Graphs and Diagrams +#--------------------------------------------------------------------------- + +HAVE_DOT = NO #--------------------------------------------------------------------------- # Man Output @@ -20,6 +64,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty GENERATE_MAN = YES MAN_OUTPUT = zig-out/share/man MAN_EXTENSION = .3 +MAN_LINKS = YES #--------------------------------------------------------------------------- # Other Output diff --git a/DoxygenLayout.xml b/DoxygenLayout.xml index 67497e83f..ae9c52684 100644 --- a/DoxygenLayout.xml +++ b/DoxygenLayout.xml @@ -6,37 +6,15 @@ - + - - - - - - - - - - - - - - - - - - + - - - - - - + diff --git a/dist/doxygen/favicon.png b/dist/doxygen/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b647bcf358546e6f9b54c9c423579db9205f2bcc GIT binary patch literal 1562 zcmV+#2IcvQP)A#`s<*=)YsQ{)B5%6hXw`)-q+sVUKWwiT8oGXAiiystqZVz zkj~G~S5BQeb>`5aLnCL;o_+he0e0-z@x9^U;co%K#fy`~am`N)jlLF(AT(2j3EnB|!-R}&u_a{#k@Rws_V{cqFfy0Ln|LX2ltG;m4vSoBG zy_>;RU*_Dz4E?t+=ib$~5~zST#{S7`e>}zFMMc`82xDn10vbk$3V?{Q(F6SF+#eVp zKf~zgi9_qwt=n+b1p52?2Y>tOaR7Sz?%?!WZ}PeOKf%s#f0A}jTcw{T_LbTD^JnQ< z7(wnSz9w=SH!c**EL!@02k^UBU*>~%-Zj{C3(_<#)r$^ba%PfyKDv_6tX;{!XDvDq zk_2w7B)Uqib_sx~sf(B_D>Vg(<5=lL128{7 z$Lcj7V|M-$9~s!dBYS>`GATcP={MZ_+57nT+BF<~;V9L34iH}x`6+aL0Yrt=lFNWl zpSxPCsipwcYE{LtFSepkqsUxb<>uu}n72igjR?w%nNHfs!hnti5%X67u?kV*mrdRR z1fh_mDF9mQx`%BAfKC!XNV9~Q=^3ia`}oGbXNVP~8g5&CJ5dO~Kl(RBidYID3Q!>a zUXc7;pwow0VGL+P7@#?H34&%FVnuHm$xr~Xiy3&m!PW$3hS zj3x*pL*E(fu^<*uC}J{{+k%z=wK(uI^g?D3EKZob=ids+;F7nw@| z2@y$r0*R}cRsc3l-N%TO5sAR2SQn$p0i2Zh;aZyQFvkTmyZZen1=Y3??-rIlH@wJKqo!{2M}PzuM}}c4!{b4AG_@HG1vxk zKq3UeUqp2PX_bKc$N{JVA`0NV8e`B}r%eGWmCEG8&fZ(#1lFcV(1XpU0cmcIZ2+=(<+UI| zHzZTQ1f5+Ua2`zoPM$n@?9=ys>cfE?A^z8X)Ut0P;f-LkhCNk{9u4mWe0n*8D6EO8 zoqrld#;v!l76a2)O#qla@bptd4?g(SV-G*Pnb-gHB2!Z*Fqy?Ro_FwLT0jXwAoTRy zz+J08PgLw+-%t0hb>__z@Kj+qI5af0ZN-WeYr-%nTWj(D{!d5V36Q@N5zkQXE7B~R z9UmV*J~A@$XqIJvX@cBT? Date: Sun, 5 Oct 2025 20:18:47 -0700 Subject: [PATCH 183/319] lib-vt: fix dockerfile to include assets for web --- src/build/docker/lib-c-docs/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index 5667c6607..6522ca4f6 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -10,6 +10,8 @@ RUN apk add --no-cache \ graphviz WORKDIR /ghostty COPY include/ ./include/ +COPY images/ ./images/ +COPY dist/doxygen/ ./dist/doxygen/ COPY Doxyfile ./ COPY DoxygenLayout.xml ./ RUN mkdir -p zig-out/share/ghostty/doc/libghostty From 6ef0be758043d18f6ca9374ea0c4158da62c4dbd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 20:26:17 -0700 Subject: [PATCH 184/319] libghostty website: update to use arch for doxygen for latest --- src/build/docker/lib-c-docs/Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index 6522ca4f6..8b4f2f6df 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -1,13 +1,15 @@ #-------------------------------------------------------------------- # Generate documentation with Doxygen #-------------------------------------------------------------------- -FROM alpine:latest AS builder +FROM --platform=linux/amd64 archlinux:latest AS builder # Build argument for noindex header ARG ADD_NOINDEX_HEADER=false -RUN apk add --no-cache \ +RUN pacman -Syu --noconfirm && \ + pacman -S --noconfirm \ doxygen \ - graphviz + graphviz && \ + pacman -Scc --noconfirm WORKDIR /ghostty COPY include/ ./include/ COPY images/ ./images/ From 21d545c3b46de4242250929b58bac4b06b587525 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 20:28:33 -0700 Subject: [PATCH 185/319] doxygen prettier --- dist/doxygen/ghostty.css | 83 ++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/dist/doxygen/ghostty.css b/dist/doxygen/ghostty.css index cac21e689..f9f1bd6d7 100644 --- a/dist/doxygen/ghostty.css +++ b/dist/doxygen/ghostty.css @@ -4,96 +4,115 @@ */ /* Ghostty brand color for links and accents - high contrast for dark bg */ -a, a:link { - color: #99B3FF; +a, +a:link { + color: #99b3ff; } a:visited { - color: #99B3FF; + color: #99b3ff; } a:hover { - color: #C2D4FF; + color: #c2d4ff; } /* High contrast text colors */ -body, div.contents, div.header, .title, .summary, td, th, p, li { - color: #E8E8E8 !important; +body, +div.contents, +div.header, +.title, +.summary, +td, +th, +p, +li { + color: #e8e8e8 !important; } -h1, h2, h3, h4, h5, h6, .groupheader { - color: #FFFFFF !important; +h1, +h2, +h3, +h4, +h5, +h6, +.groupheader { + color: #ffffff !important; } -.memtitle, .memname { - color: #FFFFFF !important; +.memtitle, +.memname { + color: #ffffff !important; } .memdoc { - color: #E8E8E8 !important; + color: #e8e8e8 !important; } /* Selection color */ ::selection { - background: rgba(53, 81, 243, 0.6); + background: rgba(53, 81, 243, 0.6); } /* Tree view selected item */ #nav-tree .selected { - background-color: #3551F3 !important; + background-color: #3551f3 !important; } /* Custom syntax highlighting optimized for dark backgrounds with high contrast */ -.fragment, div.line { - color: #F0F0F0 !important; +.fragment, +div.line { + color: #f0f0f0 !important; } /* Keywords (int, void, const, static, etc.) */ -.keyword, .keywordtype { - color: #FF8BE6 !important; - font-weight: 500; +.keyword, +.keywordtype { + color: #ff8be6 !important; + font-weight: 500; } /* Control flow (if, else, return, for, while, etc.) */ .keywordflow { - color: #FF8BE6 !important; - font-weight: 500; + color: #ff8be6 !important; + font-weight: 500; } /* Comments */ .comment { - color: #8BC34A !important; - font-style: italic; + color: #8bc34a !important; + font-style: italic; } /* Preprocessor directives (#include, #define, etc.) */ .preprocessor { - color: #FFCC66 !important; + color: #ffcc66 !important; } /* String and character literals */ -.stringliteral, .charliteral { - color: #B8E986 !important; +.stringliteral, +.charliteral { + color: #b8e986 !important; } /* Numbers */ span.charliteral { - color: #D4A5FF !important; + color: #d4a5ff !important; } /* Function names */ .functionname { - color: #6FE87C !important; - font-weight: 500; + color: #6fe87c !important; + font-weight: 500; } /* Line numbers */ span.lineno { - color: #8A8A8A !important; - background-color: transparent !important; + color: #8a8a8a !important; + background-color: transparent !important; } span.lineno a { - color: #8A8A8A !important; - background-color: transparent !important; + color: #8a8a8a !important; + background-color: transparent !important; } From 86421c9e091df5972da3f374df2276e51022e7b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Oct 2025 20:36:20 -0700 Subject: [PATCH 186/319] lib-vt: trying to fix up hosted docs --- Doxyfile | 1 + src/build/docker/lib-c-docs/entrypoint.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/Doxyfile b/Doxyfile index d0c0414dc..7c9a9c689 100644 --- a/Doxyfile +++ b/Doxyfile @@ -44,6 +44,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty HTML_EXTRA_STYLESHEET = dist/doxygen/ghostty.css HTML_EXTRA_FILES = dist/doxygen/favicon.png HTML_COLORSTYLE = DARK +HTML_CODE_FOLDING = NO LAYOUT_FILE = DoxygenLayout.xml GENERATE_TREEVIEW = YES HTML_DYNAMIC_SECTIONS = YES diff --git a/src/build/docker/lib-c-docs/entrypoint.sh b/src/build/docker/lib-c-docs/entrypoint.sh index 928d6e163..67cc44b03 100755 --- a/src/build/docker/lib-c-docs/entrypoint.sh +++ b/src/build/docker/lib-c-docs/entrypoint.sh @@ -7,10 +7,22 @@ server { root /usr/share/nginx/html; index index.html; add_header X-Robots-Tag "noindex, nofollow" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; } } EOF # Remove default server config rm -f /etc/nginx/conf.d/default.conf +else + cat > /etc/nginx/conf.d/default.conf << 'EOF' +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + } +} +EOF fi exec nginx -g "daemon off;" From d2ee80bc49dd0aaabcffed2cf4d9c3ef8cd90c02 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 6 Oct 2025 09:39:50 -0500 Subject: [PATCH 187/319] gtk: use std.Io.Writer to generate runtime CSS --- src/apprt/gtk/class/application.zig | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index af56130d3..07663fec9 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -806,19 +806,19 @@ pub const Application = extern struct { } } - fn loadRuntimeCss(self: *Self) Allocator.Error!void { + fn loadRuntimeCss(self: *Self) (Allocator.Error || std.Io.Writer.Error)!void { const alloc = self.allocator(); const config = self.private().config.get(); - var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048); - defer buf.deinit(alloc); + var buf: std.Io.Writer.Allocating = try .initCapacity(alloc, 2048); + defer buf.deinit(); - const writer = buf.writer(alloc); + const writer = &buf.writer; // Load standard css first as it can override some of the user configured styling. - try loadRuntimeCss414(config, &writer); - try loadRuntimeCss416(config, &writer); + try loadRuntimeCss414(config, writer); + try loadRuntimeCss416(config, writer); const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; @@ -861,7 +861,8 @@ pub const Application = extern struct { // ensure that we have a sentinel try writer.writeByte(0); - const data = buf.items[0 .. buf.items.len - 1 :0]; + const data_ = buf.written(); + const data = data_[0 .. data_.len - 1 :0]; log.debug("runtime CSS is {d} bytes", .{data.len + 1}); @@ -875,8 +876,8 @@ pub const Application = extern struct { /// Load runtime CSS for older than GTK 4.16 fn loadRuntimeCss414( config: *const CoreConfig, - writer: *const std.ArrayListUnmanaged(u8).Writer, - ) Allocator.Error!void { + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { if (gtk_version.runtimeAtLeast(4, 16, 0)) return; const window_theme = config.@"window-theme"; @@ -911,8 +912,8 @@ pub const Application = extern struct { /// Load runtime for GTK 4.16 and newer fn loadRuntimeCss416( config: *const CoreConfig, - writer: *const std.ArrayListUnmanaged(u8).Writer, - ) Allocator.Error!void { + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { if (gtk_version.runtimeUntil(4, 16, 0)) return; const window_theme = config.@"window-theme"; @@ -1044,9 +1045,7 @@ pub const Application = extern struct { defer file.close(); log.info("loading gtk-custom-css path={s}", .{path}); - var buf: [4096]u8 = undefined; - var reader = file.reader(&buf); - const contents = try reader.interface.readAlloc( + const contents = try file.readToEndAlloc( alloc, 5 * 1024 * 1024, // 5MB, ); @@ -1164,7 +1163,7 @@ pub const Application = extern struct { // just stuck with the old CSS but we don't want to fail the entire // config change operation. self.loadRuntimeCss() catch |err| switch (err) { - error.OutOfMemory => log.warn( + error.WriteFailed, error.OutOfMemory => log.warn( "out of memory loading runtime CSS, no runtime CSS applied", .{}, ), From 76d9d731f0b1cc6bb900583c1d6844cd7cbeccc1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 6 Oct 2025 09:53:10 -0500 Subject: [PATCH 188/319] update to the latest zf --- build.zig.zon | 4 ++-- build.zig.zon.json | 26 ++++++++------------------ build.zig.zon.nix | 38 +++++++++++--------------------------- build.zig.zon.txt | 6 ++---- flatpak/zig-packages.json | 30 +++++++++--------------------- 5 files changed, 32 insertions(+), 72 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 4039c6bdd..71e7cd1b4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -50,8 +50,8 @@ }, .zf = .{ // natecraddock/zf - .url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", - .hash = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", + .url = "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz", + .hash = "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg", .lazy = true, }, .gobject = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index ad9763f62..b587ee5ef 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -114,16 +114,16 @@ "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", "hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" }, + "vaxis-0.5.1-BWNV_BMgCQDXdZzABeY4F_xwgE7nHFtYEP07KgEwJWo8": { + "name": "vaxis", + "url": "git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a", + "hash": "sha256-5c+TjmiH4071lKI+U8SIJ0M+4ezzcAtLI2ZSfZYdXSA=" + }, "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": { "name": "vaxis", "url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", "hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=" }, - "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA": { - "name": "vaxis", - "url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", - "hash": "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM=" - }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "name": "wayland", "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", @@ -144,15 +144,10 @@ "url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", "hash": "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg=" }, - "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR": { + "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg": { "name": "zf", - "url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", - "hash": "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ=" - }, - "zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM": { - "name": "zg", - "url": "git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9", - "hash": "sha256-P0ieLuOQ05wKVaMmeNKJIxCWMIdyeKkmhsj8Ps80BGU=" + "url": "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz", + "hash": "sha256-Kaduui4Llb9Fq1lCKLOeG8cWTknjhos8cSTeJ50KP/I=" }, "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9": { "name": "zg", @@ -174,11 +169,6 @@ "url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", "hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=" }, - "zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL": { - "name": "zigimg", - "url": "git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726", - "hash": "sha256-Ko5RuxxTAvpUHCnWEdHqNl7b+PVUAxg1/OPmzGGjdt0=" - }, "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": { "name": "zigimg", "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 9d189cfdc..dd59e709a 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -266,6 +266,14 @@ in hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; }; } + { + name = "vaxis-0.5.1-BWNV_BMgCQDXdZzABeY4F_xwgE7nHFtYEP07KgEwJWo8"; + path = fetchZigArtifact { + name = "vaxis"; + url = "git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a"; + hash = "sha256-5c+TjmiH4071lKI+U8SIJ0M+4ezzcAtLI2ZSfZYdXSA="; + }; + } { name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ"; path = fetchZigArtifact { @@ -274,14 +282,6 @@ in hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="; }; } - { - name = "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA"; - path = fetchZigArtifact { - name = "vaxis"; - url = "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz"; - hash = "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM="; - }; - } { name = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t"; path = fetchZigArtifact { @@ -315,19 +315,11 @@ in }; } { - name = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR"; + name = "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg"; path = fetchZigArtifact { name = "zf"; - url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz"; - hash = "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ="; - }; - } - { - name = "zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM"; - path = fetchZigArtifact { - name = "zg"; - url = "git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9"; - hash = "sha256-P0ieLuOQ05wKVaMmeNKJIxCWMIdyeKkmhsj8Ps80BGU="; + url = "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz"; + hash = "sha256-Kaduui4Llb9Fq1lCKLOeG8cWTknjhos8cSTeJ50KP/I="; }; } { @@ -362,14 +354,6 @@ in hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="; }; } - { - name = "zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL"; - path = fetchZigArtifact { - name = "zigimg"; - url = "git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726"; - hash = "sha256-Ko5RuxxTAvpUHCnWEdHqNl7b+PVUAxg1/OPmzGGjdt0="; - }; - } { name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms"; path = fetchZigArtifact { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index c3727f1e5..9db796546 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,5 +1,4 @@ -git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9 -git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726 +git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz @@ -28,11 +27,10 @@ https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d. https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz -https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz -https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz +https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 1eba46fc2..eb8f57028 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -137,18 +137,18 @@ "dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", "sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" }, + { + "type": "git", + "url": "https://github.com/rockorager/libvaxis", + "commit": "6eb16bb4190dc074dafaf4f0ce7dadd50e81192a", + "dest": "vendor/p/vaxis-0.5.1-BWNV_BMgCQDXdZzABeY4F_xwgE7nHFtYEP07KgEwJWo8" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", "dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", "sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2" }, - { - "type": "archive", - "url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", - "dest": "vendor/p/vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA", - "sha256": "7aae580b6e8e6348b671d409d195cc67ea36bc740b10534d1b342de59bb3e013" - }, { "type": "archive", "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", @@ -175,15 +175,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", - "dest": "vendor/p/zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", - "sha256": "f018a76da9d27d978103c481028a55c7024e6cddfafc14e9c551c004a89cb0c4" - }, - { - "type": "git", - "url": "https://codeberg.org/ivanstepanovftw/zg", - "commit": "4fe689e56ce2ed5a8f59308b471bccd7da89fac9", - "dest": "vendor/p/zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM" + "url": "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz", + "dest": "vendor/p/zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg", + "sha256": "29a76eba2e0b95bf45ab594228b39e1bc7164e49e3868b3c7124de279d0a3ff2" }, { "type": "archive", @@ -209,12 +203,6 @@ "dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", "sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e" }, - { - "type": "git", - "url": "https://github.com/ivanstepanovftw/zigimg", - "commit": "aa4c31db872612c39edbb79f753b3cd9a79fe726", - "dest": "vendor/p/zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL" - }, { "type": "archive", "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", From 992e9e2a6e282ef44be5d2371ddc4e69223c651d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 08:14:27 -0700 Subject: [PATCH 189/319] doxygen: mobile styling --- Doxyfile | 6 +- dist/doxygen/footer.html | 18 + dist/doxygen/ghostty.css | 222 ++++ dist/doxygen/header.html | 77 ++ dist/doxygen/mobile-nav.js | 65 + dist/doxygen/stylesheet.css | 2461 +++++++++++++++++++++++++++++++++++ 6 files changed, 2848 insertions(+), 1 deletion(-) create mode 100644 dist/doxygen/footer.html create mode 100644 dist/doxygen/header.html create mode 100644 dist/doxygen/mobile-nav.js create mode 100644 dist/doxygen/stylesheet.css diff --git a/Doxyfile b/Doxyfile index 7c9a9c689..0c8688987 100644 --- a/Doxyfile +++ b/Doxyfile @@ -42,15 +42,19 @@ EXTRACT_LOCAL_CLASSES = NO GENERATE_HTML = YES HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty HTML_EXTRA_STYLESHEET = dist/doxygen/ghostty.css -HTML_EXTRA_FILES = dist/doxygen/favicon.png +HTML_EXTRA_FILES = dist/doxygen/favicon.png \ + dist/doxygen/mobile-nav.js HTML_COLORSTYLE = DARK HTML_CODE_FOLDING = NO +HTML_HEADER = dist/doxygen/header.html LAYOUT_FILE = DoxygenLayout.xml GENERATE_TREEVIEW = YES HTML_DYNAMIC_SECTIONS = YES SEARCHENGINE = YES ALPHABETICAL_INDEX = YES HTML_TIMESTAMP = NO +DISABLE_INDEX = NO +FULL_SIDEBAR = NO #--------------------------------------------------------------------------- # Graphs and Diagrams diff --git a/dist/doxygen/footer.html b/dist/doxygen/footer.html new file mode 100644 index 000000000..fca4b87d9 --- /dev/null +++ b/dist/doxygen/footer.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/dist/doxygen/ghostty.css b/dist/doxygen/ghostty.css index f9f1bd6d7..9670a70ca 100644 --- a/dist/doxygen/ghostty.css +++ b/dist/doxygen/ghostty.css @@ -116,3 +116,225 @@ span.lineno a { color: #8a8a8a !important; background-color: transparent !important; } + +/* Desktop: ensure page-nav maintains default width */ +@media screen and (min-width: 768px) { + #page-nav-toggle { + display: none !important; + } + + #page-nav { + position: relative !important; + width: 250px !important; + height: auto !important; + right: auto !important; + top: auto !important; + box-shadow: none !important; + } +} + +/* Mobile-friendly responsive styles */ +@media screen and (max-width: 767px) { + body { + font-size: 14px !important; + } + + /* Make navigation tree collapsible on mobile */ + #side-nav { + display: none; + } + + #doc-content { + margin-left: 0 !important; + margin-right: 0 !important; + } + + /* Make right sidebar (page-nav) overlay on mobile */ + #page-nav { + position: fixed !important; + top: 0 !important; + right: -280px !important; + width: 280px !important; + height: 100vh !important; + z-index: 10000 !important; + background: #101826 !important; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5) !important; + transition: right 0.3s ease !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; + } + + #page-nav.mobile-open { + right: 0 !important; + } + + /* Hamburger menu button for page nav */ + #page-nav-toggle { + display: block !important; + position: fixed !important; + top: 10px !important; + right: 15px !important; + z-index: 10001 !important; + width: 40px !important; + height: 40px !important; + background: rgba(53, 81, 243, 0.9) !important; + border: none !important; + border-radius: 5px !important; + cursor: pointer !important; + padding: 8px !important; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important; + } + + #page-nav-toggle span { + display: block !important; + width: 24px !important; + height: 3px !important; + background: #fff !important; + margin: 4px 0 !important; + border-radius: 2px !important; + transition: 0.3s !important; + } + + /* Mobile overlay backdrop */ + #page-nav-backdrop { + display: none !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + background: rgba(0, 0, 0, 0.5) !important; + z-index: 9999 !important; + } + + #page-nav-backdrop.active { + display: block !important; + } + + /* Improve header and navigation */ + #top { + height: auto !important; + } + + #titlearea { + padding: 10px !important; + } + + #projectname { + font-size: 18px !important; + } + + #projectbrief, + #projectnumber { + font-size: 12px !important; + } + + /* Make tabs stack better on mobile */ + #navrow1, + #navrow2, + #navrow3, + #navrow4 { + overflow-x: auto !important; + white-space: nowrap !important; + -webkit-overflow-scrolling: touch !important; + } + + .tablist li { + display: inline-block !important; + } + + /* Content adjustments */ + .contents { + padding: 10px !important; + width: 100% !important; + box-sizing: border-box !important; + } + + .header { + padding: 5px !important; + } + + /* Code blocks */ + .fragment { + font-size: 12px !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + } + + div.line { + font-size: 12px !important; + } + + /* Tables */ + table { + display: block !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + width: 100% !important; + } + + .memberdecls table, + .fieldtable { + font-size: 12px !important; + } + + .memtitle { + font-size: 14px !important; + padding: 8px !important; + } + + .memname { + font-size: 13px !important; + word-break: break-word !important; + } + + .memitem { + margin: 5px 0 !important; + } + + /* Search box */ + #MSearchBox { + width: 100% !important; + right: 0 !important; + } + + /* Reduce padding and margins */ + h1, h2, h3, h4, h5, h6 { + margin-top: 10px !important; + margin-bottom: 8px !important; + } + + h1 { font-size: 22px !important; } + h2 { font-size: 18px !important; } + h3 { font-size: 16px !important; } + h4 { font-size: 14px !important; } + + /* Directory/file listings */ + .directory .levels span { + display: none !important; + } + + .directory .arrow { + margin-right: 5px !important; + } + + /* Treeview adjustments */ + #nav-tree { + width: 100% !important; + } +} + +/* Tablet adjustments */ +@media screen and (min-width: 768px) and (max-width: 1024px) { + .contents { + padding: 15px !important; + } + + #side-nav { + width: 200px !important; + } + + #doc-content { + margin-left: 200px !important; + } +} diff --git a/dist/doxygen/header.html b/dist/doxygen/header.html new file mode 100644 index 000000000..223ec4953 --- /dev/null +++ b/dist/doxygen/header.html @@ -0,0 +1,77 @@ + + + + + + + + +$projectname: $title +$title + + + + + + + + + + + + +$treeview +$search +$mathjax +$darkmode + +$extrastylesheet + + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber +
+
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/dist/doxygen/mobile-nav.js b/dist/doxygen/mobile-nav.js new file mode 100644 index 000000000..1262360b8 --- /dev/null +++ b/dist/doxygen/mobile-nav.js @@ -0,0 +1,65 @@ +/** + * Mobile navigation toggle for Doxygen documentation + */ + +(function() { + // Only run on mobile devices + function isMobile() { + return window.innerWidth <= 767; + } + + function initMobileNav() { + if (!isMobile()) return; + + const pageNav = document.getElementById('page-nav'); + if (!pageNav) return; + + // Create toggle button + const toggleBtn = document.createElement('button'); + toggleBtn.id = 'page-nav-toggle'; + toggleBtn.setAttribute('aria-label', 'Toggle page navigation'); + toggleBtn.innerHTML = ''; + document.body.appendChild(toggleBtn); + + // Create backdrop + const backdrop = document.createElement('div'); + backdrop.id = 'page-nav-backdrop'; + document.body.appendChild(backdrop); + + // Toggle function + function toggleNav() { + const isOpen = pageNav.classList.toggle('mobile-open'); + backdrop.classList.toggle('active', isOpen); + document.body.style.overflow = isOpen ? 'hidden' : ''; + } + + // Event listeners + toggleBtn.addEventListener('click', toggleNav); + backdrop.addEventListener('click', toggleNav); + + // Close on escape key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && pageNav.classList.contains('mobile-open')) { + toggleNav(); + } + }); + } + + // Initialize on load and resize + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initMobileNav); + } else { + initMobileNav(); + } + + window.addEventListener('resize', function() { + const pageNav = document.getElementById('page-nav'); + const backdrop = document.getElementById('page-nav-backdrop'); + + if (!isMobile() && pageNav) { + pageNav.classList.remove('mobile-open'); + if (backdrop) backdrop.classList.remove('active'); + document.body.style.overflow = ''; + } + }); +})(); diff --git a/dist/doxygen/stylesheet.css b/dist/doxygen/stylesheet.css new file mode 100644 index 000000000..5d2eecd6d --- /dev/null +++ b/dist/doxygen/stylesheet.css @@ -0,0 +1,2461 @@ +/* The standard CSS for doxygen 1.14.0*/ + +html { +/* page base colors */ +--page-background-color: white; +--page-foreground-color: black; +--page-link-color: #3D578C; +--page-visited-link-color: #3D578C; +--page-external-link-color: #334975; + +/* index */ +--index-odd-item-bg-color: #F8F9FC; +--index-even-item-bg-color: white; +--index-header-color: black; +--index-separator-color: #A0A0A0; + +/* header */ +--header-background-color: #F9FAFC; +--header-separator-color: #C4CFE5; +--group-header-separator-color: #D9E0EE; +--group-header-color: #354C7B; +--inherit-header-color: gray; + +--footer-foreground-color: #2A3D61; +--footer-logo-width: 75px; +--citation-label-color: #334975; +--glow-color: cyan; + +--title-background-color: white; +--title-separator-color: #C4CFE5; +--directory-separator-color: #9CAFD4; +--separator-color: #4A6AAA; + +--blockquote-background-color: #F7F8FB; +--blockquote-border-color: #9CAFD4; + +--scrollbar-thumb-color: #C4CFE5; +--scrollbar-background-color: #F9FAFC; + +--icon-background-color: #728DC1; +--icon-foreground-color: white; +/* +--icon-doc-image: url('doc.svg'); +--icon-folder-open-image: url('folderopen.svg'); +--icon-folder-closed-image: url('folderclosed.svg');*/ +--icon-folder-open-fill-color: #C4CFE5; +--icon-folder-fill-color: #D8DFEE; +--icon-folder-border-color: #4665A2; +--icon-doc-fill-color: #D8DFEE; +--icon-doc-border-color: #4665A2; + +/* brief member declaration list */ +--memdecl-background-color: #F9FAFC; +--memdecl-separator-color: #DEE4F0; +--memdecl-foreground-color: #555; +--memdecl-template-color: #4665A2; +--memdecl-border-color: #D5DDEC; + +/* detailed member list */ +--memdef-border-color: #A8B8D9; +--memdef-title-background-color: #E2E8F2; +--memdef-proto-background-color: #EEF1F7; +--memdef-proto-text-color: #253555; +--memdef-doc-background-color: white; +--memdef-param-name-color: #602020; +--memdef-template-color: #4665A2; + +/* tables */ +--table-cell-border-color: #2D4068; +--table-header-background-color: #374F7F; +--table-header-foreground-color: #FFFFFF; + +/* labels */ +--label-background-color: #728DC1; +--label-left-top-border-color: #5373B4; +--label-right-bottom-border-color: #C4CFE5; +--label-foreground-color: white; + +/** navigation bar/tree/menu */ +--nav-background-color: #F9FAFC; +--nav-foreground-color: #364D7C; +--nav-border-color: #C4CFE5; +--nav-breadcrumb-separator-color: #C4CFE5; +--nav-breadcrumb-active-bg: #EEF1F7; +--nav-breadcrumb-color: #354C7B; +--nav-breadcrumb-border-color: #E1E7F2; +--nav-splitbar-bg-color: #DCE2EF; +--nav-splitbar-handle-color: #9CAFD4; +--nav-font-size-level1: 13px; +--nav-font-size-level2: 10px; +--nav-font-size-level3: 9px; +--nav-text-normal-color: #283A5D; +--nav-text-hover-color: white; +--nav-text-active-color: white; +--nav-menu-button-color: #364D7C; +--nav-menu-background-color: white; +--nav-menu-foreground-color: #555555; +--nav-menu-active-bg: #DCE2EF; +--nav-menu-active-color: #9CAFD4; +--nav-menu-toggle-color: rgba(255, 255, 255, 0.5); +--nav-arrow-color: #B6C4DF; +--nav-arrow-selected-color: #90A5CE; + +/* sync icon */ +--sync-icon-border-color: #C4CFE5; +--sync-icon-background-color: #F9FAFC; +--sync-icon-selected-background-color: #EEF1F7; +--sync-icon-color: #C4CFE5; +--sync-icon-selected-color: #6884BD; + +/* table of contents */ +--toc-background-color: #F4F6FA; +--toc-border-color: #D8DFEE; +--toc-header-color: #4665A2; +--toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + +/** search field */ +--search-background-color: white; +--search-foreground-color: #909090; +--search-active-color: black; +--search-filter-background-color: rgba(255,255,255,.7); +--search-filter-backdrop-filter: blur(4px); +--search-filter-foreground-color: black; +--search-filter-border-color: rgba(150,150,150,.4); +--search-filter-highlight-text-color: white; +--search-filter-highlight-bg-color: #3D578C; +--search-results-foreground-color: #425E97; +--search-results-background-color: rgba(255,255,255,.8); +--search-results-backdrop-filter: blur(4px); +--search-results-border-color: rgba(150,150,150,.4); +--search-box-border-color: #B6C4DF; +--search-close-icon-bg-color: #A0A0A0; +--search-close-icon-fg-color: white; + +/** code fragments */ +--code-keyword-color: #008000; +--code-type-keyword-color: #604020; +--code-flow-keyword-color: #E08000; +--code-comment-color: #800000; +--code-preprocessor-color: #806020; +--code-string-literal-color: #002080; +--code-char-literal-color: #008080; +--code-xml-cdata-color: black; +--code-vhdl-digit-color: #FF00FF; +--code-vhdl-char-color: #000000; +--code-vhdl-keyword-color: #700070; +--code-vhdl-logic-color: #FF0000; +--code-link-color: #4665A2; +--code-external-link-color: #4665A2; +--fragment-foreground-color: black; +--fragment-background-color: #FBFCFD; +--fragment-border-color: #C4CFE5; +--fragment-lineno-border-color: #00FF00; +--fragment-lineno-background-color: #E8E8E8; +--fragment-lineno-foreground-color: black; +--fragment-lineno-link-fg-color: #4665A2; +--fragment-lineno-link-bg-color: #D8D8D8; +--fragment-lineno-link-hover-fg-color: #4665A2; +--fragment-lineno-link-hover-bg-color: #C8C8C8; +--fragment-copy-ok-color: #2EC82E; +--tooltip-foreground-color: black; +--tooltip-background-color: rgba(255,255,255,0.8); +--tooltip-arrow-background-color: white; +--tooltip-border-color: rgba(150,150,150,0.7); +--tooltip-backdrop-filter: blur(3px); +--tooltip-doc-color: grey; +--tooltip-declaration-color: #006318; +--tooltip-link-color: #4665A2; +--tooltip-shadow: 0 4px 8px 0 rgba(0,0,0,.25); +--fold-line-color: #808080; + +/** font-family */ +--font-family-normal: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; +--font-family-monospace: 'JetBrains Mono',Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace,fixed; +--font-family-nav: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; +--font-family-title: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; +--font-family-toc: Verdana,'DejaVu Sans',Geneva,sans-serif; +--font-family-search: Arial,Verdana,sans-serif; +--font-family-icon: Arial,Helvetica; +--font-family-tooltip: Roboto,sans-serif; + +/** special sections */ +--warning-color-bg: #f8d1cc; +--warning-color-hl: #b61825; +--warning-color-text: #75070f; +--note-color-bg: #faf3d8; +--note-color-hl: #f3a600; +--note-color-text: #5f4204; +--todo-color-bg: #e4f3ff; +--todo-color-hl: #1879C4; +--todo-color-text: #274a5c; +--test-color-bg: #e8e8ff; +--test-color-hl: #3939C4; +--test-color-text: #1a1a5c; +--deprecated-color-bg: #ecf0f3; +--deprecated-color-hl: #5b6269; +--deprecated-color-text: #43454a; +--bug-color-bg: #e4dafd; +--bug-color-hl: #5b2bdd; +--bug-color-text: #2a0d72; +--invariant-color-bg: #d8f1e3; +--invariant-color-hl: #44b86f; +--invariant-color-text: #265532; +} + +@media (prefers-color-scheme: dark) { + html:not(.dark-mode) { + color-scheme: dark; + +/* page base colors */ +--page-background-color: black; +--page-foreground-color: #C9D1D9; +--page-link-color: #90A5CE; +--page-visited-link-color: #90A5CE; +--page-external-link-color: #A3B4D7; + +/* index */ +--index-odd-item-bg-color: #0B101A; +--index-even-item-bg-color: black; +--index-header-color: #C4CFE5; +--index-separator-color: #334975; + +/* header */ +--header-background-color: #070B11; +--header-separator-color: #141C2E; +--group-header-separator-color: #1D2A43; +--group-header-color: #90A5CE; +--inherit-header-color: #A0A0A0; + +--footer-foreground-color: #5B7AB7; +--footer-logo-width: 60px; +--citation-label-color: #90A5CE; +--glow-color: cyan; + +--title-background-color: #090D16; +--title-separator-color: #212F4B; +--directory-separator-color: #283A5D; +--separator-color: #283A5D; + +--blockquote-background-color: #101826; +--blockquote-border-color: #283A5D; + +--scrollbar-thumb-color: #2C3F65; +--scrollbar-background-color: #070B11; + +--icon-background-color: #334975; +--icon-foreground-color: #C4CFE5; +--icon-folder-open-fill-color: #4665A2; +--icon-folder-fill-color: #5373B4; +--icon-folder-border-color: #C4CFE5; +--icon-doc-fill-color: #6884BD; +--icon-doc-border-color: #C4CFE5; + +/* brief member declaration list */ +--memdecl-background-color: #0B101A; +--memdecl-separator-color: #2C3F65; +--memdecl-foreground-color: #BBB; +--memdecl-template-color: #7C95C6; +--memdecl-border-color: #233250; + +/* detailed member list */ +--memdef-border-color: #233250; +--memdef-title-background-color: #1B2840; +--memdef-proto-background-color: #19243A; +--memdef-proto-text-color: #9DB0D4; +--memdef-doc-background-color: black; +--memdef-param-name-color: #D28757; +--memdef-template-color: #7C95C6; + +/* tables */ +--table-cell-border-color: #283A5D; +--table-header-background-color: #283A5D; +--table-header-foreground-color: #C4CFE5; + +/* labels */ +--label-background-color: #354C7B; +--label-left-top-border-color: #4665A2; +--label-right-bottom-border-color: #283A5D; +--label-foreground-color: #CCCCCC; + +/** navigation bar/tree/menu */ +--nav-background-color: #101826; +--nav-foreground-color: #364D7C; +--nav-border-color: #212F4B; +--nav-breadcrumb-separator-color: #212F4B; +--nav-breadcrumb-active-bg: #1D2A43; +--nav-breadcrumb-color: #90A5CE; +--nav-breadcrumb-border-color: #2A3D61; +--nav-splitbar-bg-color: #283A5D; +--nav-splitbar-handle-color: #4665A2; +--nav-font-size-level1: 13px; +--nav-font-size-level2: 10px; +--nav-font-size-level3: 9px; +--nav-text-normal-color: #B6C4DF; +--nav-text-hover-color: #DCE2EF; +--nav-text-active-color: #DCE2EF; +--nav-menu-button-color: #B6C4DF; +--nav-menu-background-color: #05070C; +--nav-menu-foreground-color: #BBBBBB; +--nav-menu-active-bg: #1D2A43; +--nav-menu-active-color: #C9D3E7; +--nav-menu-toggle-color: rgba(255, 255, 255, 0.2); +--nav-arrow-color: #4665A2; +--nav-arrow-selected-color: #6884BD; + +/* sync icon */ +--sync-icon-border-color: #212F4B; +--sync-icon-background-color: #101826; +--sync-icon-selected-background-color: #1D2A43; +--sync-icon-color: #4665A2; +--sync-icon-selected-color: #5373B4; + +/* table of contents */ +--toc-background-color: #151E30; +--toc-border-color: #202E4A; +--toc-header-color: #A3B4D7; +--toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + +/** search field */ +--search-background-color: black; +--search-foreground-color: #C5C5C5; +--search-active-color: #F5F5F5; +--search-filter-background-color: #101826; +--search-filter-foreground-color: #90A5CE; +--search-filter-backdrop-filter: none; +--search-filter-border-color: #7C95C6; +--search-filter-highlight-text-color: #BCC9E2; +--search-filter-highlight-bg-color: #283A5D; +--search-results-background-color: black; +--search-results-foreground-color: #90A5CE; +--search-results-backdrop-filter: none; +--search-results-border-color: #334975; +--search-box-border-color: #334975; +--search-close-icon-bg-color: #909090; +--search-close-icon-fg-color: black; + +/** code fragments */ +--code-keyword-color: #CC99CD; +--code-type-keyword-color: #AB99CD; +--code-flow-keyword-color: #E08000; +--code-comment-color: #717790; +--code-preprocessor-color: #65CABE; +--code-string-literal-color: #7EC699; +--code-char-literal-color: #00E0F0; +--code-xml-cdata-color: #C9D1D9; +--code-vhdl-digit-color: #FF00FF; +--code-vhdl-char-color: #C0C0C0; +--code-vhdl-keyword-color: #CF53C9; +--code-vhdl-logic-color: #FF0000; +--code-link-color: #79C0FF; +--code-external-link-color: #79C0FF; +--fragment-foreground-color: #C9D1D9; +--fragment-background-color: #090D16; +--fragment-border-color: #30363D; +--fragment-lineno-border-color: #30363D; +--fragment-lineno-background-color: black; +--fragment-lineno-foreground-color: #6E7681; +--fragment-lineno-link-fg-color: #6E7681; +--fragment-lineno-link-bg-color: #303030; +--fragment-lineno-link-hover-fg-color: #8E96A1; +--fragment-lineno-link-hover-bg-color: #505050; +--fragment-copy-ok-color: #0EA80E; +--tooltip-foreground-color: #C9D1D9; +--tooltip-background-color: #202020; +--tooltip-arrow-background-color: #202020; +--tooltip-backdrop-filter: none; +--tooltip-border-color: #C9D1D9; +--tooltip-doc-color: #D9E1E9; +--tooltip-declaration-color: #20C348; +--tooltip-link-color: #79C0FF; +--tooltip-shadow: none; +--fold-line-color: #808080; + +/** font-family */ +--font-family-normal: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; +--font-family-monospace: 'JetBrains Mono',Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace,fixed; +--font-family-nav: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; +--font-family-title: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; +--font-family-toc: Verdana,'DejaVu Sans',Geneva,sans-serif; +--font-family-search: Arial,Verdana,sans-serif; +--font-family-icon: Arial,Helvetica; +--font-family-tooltip: Roboto,sans-serif; + +/** special sections */ +--warning-color-bg: #2e1917; +--warning-color-hl: #ad2617; +--warning-color-text: #f5b1aa; +--note-color-bg: #3b2e04; +--note-color-hl: #f1b602; +--note-color-text: #ceb670; +--todo-color-bg: #163750; +--todo-color-hl: #1982D2; +--todo-color-text: #dcf0fa; +--test-color-bg: #121258; +--test-color-hl: #4242cf; +--test-color-text: #c0c0da; +--deprecated-color-bg: #2e323b; +--deprecated-color-hl: #738396; +--deprecated-color-text: #abb0bd; +--bug-color-bg: #2a2536; +--bug-color-hl: #7661b3; +--bug-color-text: #ae9ed6; +--invariant-color-bg: #303a35; +--invariant-color-hl: #76ce96; +--invariant-color-text: #cceed5; +}} +body { + background-color: var(--page-background-color); + color: var(--page-foreground-color); +} + +body, table, div, p, dl { + font-weight: 400; + font-size: 14px; + font-family: var(--font-family-normal); + line-height: 22px; +} + +body.resizing { + user-select: none; + -webkit-user-select: none; +} + +#doc-content { + scrollbar-width: thin; +} + +/* @group Heading Levels */ + +.title { + font-family: var(--font-family-normal); + line-height: 28px; + font-size: 160%; + font-weight: 400; + margin: 10px 2px; +} + +h1.groupheader { + font-size: 150%; +} + +h2.groupheader { + box-shadow: 12px 0 var(--page-background-color), + -12px 0 var(--page-background-color), + 12px 1px var(--group-header-separator-color), + -12px 1px var(--group-header-separator-color); + color: var(--group-header-color); + font-size: 150%; + font-weight: normal; + margin-top: 1.75em; + padding-top: 8px; + padding-bottom: 4px; + width: 100%; +} + +td h2.groupheader { + box-shadow: 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); +} + +h3.groupheader { + font-size: 100%; +} + +h1, h2, h3, h4, h5, h6 { + -webkit-transition: text-shadow 0.5s linear; + -moz-transition: text-shadow 0.5s linear; + -ms-transition: text-shadow 0.5s linear; + -o-transition: text-shadow 0.5s linear; + transition: text-shadow 0.5s linear; + margin-right: 15px; +} + +h1.glow, h2.glow, h3.glow, h4.glow, h5.glow, h6.glow { + text-shadow: 0 0 15px var(--glow-color); +} + +dt { + font-weight: bold; +} + +p.startli, p.startdd { + margin-top: 2px; +} + +th p.starttd, th p.intertd, th p.endtd { + font-size: 100%; + font-weight: 700; +} + +p.starttd { + margin-top: 0px; +} + +p.endli { + margin-bottom: 0px; +} + +p.enddd { + margin-bottom: 4px; +} + +p.endtd { + margin-bottom: 2px; +} + +p.interli { +} + +p.interdd { +} + +p.intertd { +} + +/* @end */ + +caption { + font-weight: bold; +} + +span.legend { + font-size: 70%; + text-align: center; +} + +h3.version { + font-size: 90%; + text-align: center; +} + +div.navtab { + margin-right: 6px; + padding-right: 6px; + text-align: right; + line-height: 110%; + background-color: var(--nav-background-color); +} + +div.navtab table { + border-spacing: 0; +} + +td.navtab { + padding-right: 6px; + padding-left: 6px; +} + +td.navtabHL { + padding-right: 6px; + padding-left: 6px; + border-radius: 0 6px 6px 0; + background-color: var(--nav-menu-active-bg); +} + +div.qindex{ + text-align: center; + width: 100%; + line-height: 140%; + font-size: 130%; + color: var(--index-separator-color); +} + +#main-menu a:focus { + outline: auto; + z-index: 10; + position: relative; +} + +dt.alphachar{ + font-size: 180%; + font-weight: bold; +} + +.alphachar a{ + color: var(--index-header-color); +} + +.alphachar a:hover, .alphachar a:visited{ + text-decoration: none; +} + +.classindex dl { + padding: 25px; + column-count:1 +} + +.classindex dd { + display:inline-block; + margin-left: 50px; + width: 90%; + line-height: 1.15em; +} + +.classindex dl.even { + background-color: var(--index-even-item-bg-color); +} + +.classindex dl.odd { + background-color: var(--index-odd-item-bg-color); +} + +@media(min-width: 1120px) { + .classindex dl { + column-count:2 + } +} + +@media(min-width: 1320px) { + .classindex dl { + column-count:3 + } +} + + +/* @group Link Styling */ + +a { + color: var(--page-link-color); + font-weight: normal; + text-decoration: none; +} + +.contents a:visited { + color: var(--page-visited-link-color); +} + +span.label a:hover { + text-decoration: none; + background: linear-gradient(to bottom, transparent 0,transparent calc(100% - 1px), currentColor 100%); +} + +a.el { + font-weight: bold; +} + +a.elRef { +} + +a.el, a.el:visited, a.code, a.code:visited, a.line, a.line:visited { + color: var(--page-link-color); +} + +a.codeRef, a.codeRef:visited, a.lineRef, a.lineRef:visited { + color: var(--page-external-link-color); +} + +a.code.hl_class { /* style for links to class names in code snippets */ } +a.code.hl_struct { /* style for links to struct names in code snippets */ } +a.code.hl_union { /* style for links to union names in code snippets */ } +a.code.hl_interface { /* style for links to interface names in code snippets */ } +a.code.hl_protocol { /* style for links to protocol names in code snippets */ } +a.code.hl_category { /* style for links to category names in code snippets */ } +a.code.hl_exception { /* style for links to exception names in code snippets */ } +a.code.hl_service { /* style for links to service names in code snippets */ } +a.code.hl_singleton { /* style for links to singleton names in code snippets */ } +a.code.hl_concept { /* style for links to concept names in code snippets */ } +a.code.hl_namespace { /* style for links to namespace names in code snippets */ } +a.code.hl_package { /* style for links to package names in code snippets */ } +a.code.hl_define { /* style for links to macro names in code snippets */ } +a.code.hl_function { /* style for links to function names in code snippets */ } +a.code.hl_variable { /* style for links to variable names in code snippets */ } +a.code.hl_typedef { /* style for links to typedef names in code snippets */ } +a.code.hl_enumvalue { /* style for links to enum value names in code snippets */ } +a.code.hl_enumeration { /* style for links to enumeration names in code snippets */ } +a.code.hl_signal { /* style for links to Qt signal names in code snippets */ } +a.code.hl_slot { /* style for links to Qt slot names in code snippets */ } +a.code.hl_friend { /* style for links to friend names in code snippets */ } +a.code.hl_dcop { /* style for links to KDE3 DCOP names in code snippets */ } +a.code.hl_property { /* style for links to property names in code snippets */ } +a.code.hl_event { /* style for links to event names in code snippets */ } +a.code.hl_sequence { /* style for links to sequence names in code snippets */ } +a.code.hl_dictionary { /* style for links to dictionary names in code snippets */ } + +/* @end */ + +dl.el { + margin-left: -1cm; +} + +ul.check { + list-style:none; + text-indent: -16px; + padding-left: 38px; +} +li.unchecked:before { + content: "\2610\A0"; +} +li.checked:before { + content: "\2611\A0"; +} + +ol { + text-indent: 0px; +} + +ul { + text-indent: 0px; + overflow: visible; +} + +ul.multicol { + -moz-column-gap: 1em; + -webkit-column-gap: 1em; + column-gap: 1em; + -moz-column-count: 3; + -webkit-column-count: 3; + column-count: 3; + list-style-type: none; +} + +#side-nav ul { + overflow: visible; /* reset ul rule for scroll bar in GENERATE_TREEVIEW window */ +} + +#main-nav ul { + overflow: visible; /* reset ul rule for the navigation bar drop down lists */ +} + +.fragment { + text-align: left; + direction: ltr; + overflow-x: auto; + overflow-y: hidden; + position: relative; + min-height: 12px; + margin: 10px 0px; + padding: 10px 10px; + border: 1px solid var(--fragment-border-color); + border-radius: 4px; + background-color: var(--fragment-background-color); + color: var(--fragment-foreground-color); +} + +pre.fragment { + word-wrap: break-word; + font-size: 10pt; + line-height: 125%; + font-family: var(--font-family-monospace); +} + +span.tt { + white-space: pre; + font-family: var(--font-family-monospace); +} + +.clipboard { + width: 24px; + height: 24px; + right: 5px; + top: 5px; + opacity: 0; + position: absolute; + display: inline; + overflow: hidden; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.clipboard.success { + border: 1px solid var(--fragment-foreground-color); + border-radius: 4px; +} + +.fragment:hover .clipboard, .clipboard.success { + opacity: .4; +} + +.clipboard:hover, .clipboard.success { + opacity: 1 !important; +} + +.clipboard:active:not([class~=success]) svg { + transform: scale(.91); +} + +.clipboard.success svg { + fill: var(--fragment-copy-ok-color); +} + +.clipboard.success { + border-color: var(--fragment-copy-ok-color); +} + +div.line { + font-family: var(--font-family-monospace); + font-size: 13px; + min-height: 13px; + line-height: 1.2; + text-wrap: wrap; + word-break: break-all; + white-space: -moz-pre-wrap; /* Moz */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ + text-indent: -62px; + padding-left: 62px; + padding-bottom: 0px; + margin: 0px; + -webkit-transition-property: background-color, box-shadow; + -webkit-transition-duration: 0.5s; + -moz-transition-property: background-color, box-shadow; + -moz-transition-duration: 0.5s; + -ms-transition-property: background-color, box-shadow; + -ms-transition-duration: 0.5s; + -o-transition-property: background-color, box-shadow; + -o-transition-duration: 0.5s; + transition-property: background-color, box-shadow; + transition-duration: 0.5s; +} + +div.line:after { + content:"\000A"; + white-space: pre; +} + +div.line.glow { + background-color: var(--glow-color); + box-shadow: 0 0 10px var(--glow-color); +} + +span.fold { + display: inline-block; + width: 12px; + height: 12px; + margin-left: 4px; + margin-right: 1px; +} + +span.foldnone { + display: inline-block; + position: relative; + cursor: pointer; + user-select: none; +} + +span.fold.plus, span.fold.minus { + width: 10px; + height: 10px; + background-color: var(--fragment-background-color); + position: relative; + border: 1px solid var(--fold-line-color); + margin-right: 1px; +} + +span.fold.plus::before, span.fold.minus::before { + content: ''; + position: absolute; + background-color: var(--fold-line-color); +} + +span.fold.plus::before { + width: 2px; + height: 6px; + top: 2px; + left: 4px; +} + +span.fold.plus::after { + content: ''; + position: absolute; + width: 6px; + height: 2px; + top: 4px; + left: 2px; + background-color: var(--fold-line-color); +} + +span.fold.minus::before { + width: 6px; + height: 2px; + top: 4px; + left: 2px; +} + +span.lineno { + padding-right: 4px; + margin-right: 9px; + text-align: right; + border-right: 2px solid var(--fragment-lineno-border-color); + color: var(--fragment-lineno-foreground-color); + background-color: var(--fragment-lineno-background-color); + white-space: pre; +} +span.lineno a, span.lineno a:visited { + color: var(--fragment-lineno-link-fg-color); + background-color: var(--fragment-lineno-link-bg-color); +} + +span.lineno a:hover { + color: var(--fragment-lineno-link-hover-fg-color); + background-color: var(--fragment-lineno-link-hover-bg-color); +} + +.lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.classindex ul { + list-style: none; + padding-left: 0; +} + +div.classindex span.ai { + display: inline-block; +} + +div.groupHeader { + box-shadow: 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); + color: var(--group-header-color); + font-size: 110%; + font-weight: 500; + margin-left: 0px; + margin-top: 0em; + margin-bottom: 6px; + padding-top: 8px; + padding-bottom: 4px; +} + +div.groupText { + margin-left: 16px; + font-style: italic; +} + +body { + color: var(--page-foreground-color); + margin: 0; +} + +div.contents { + margin-top: 10px; + margin-left: 12px; + margin-right: 12px; +} + +p.formulaDsp { + text-align: center; +} + +img.dark-mode-visible { + display: none; +} +img.light-mode-visible { + display: none; +} + +img.formulaInl, img.inline { + vertical-align: middle; +} + +div.center { + text-align: center; + margin-top: 0px; + margin-bottom: 0px; + padding: 0px; +} + +div.center img { + border: 0px; +} + +address.footer { + text-align: right; + padding-right: 12px; +} + +img.footer { + border: 0px; + vertical-align: middle; + width: var(--footer-logo-width); +} + +.compoundTemplParams { + color: var(--memdecl-template-color); + font-size: 80%; + line-height: 120%; +} + +/* @group Code Colorization */ + +span.keyword { + color: var(--code-keyword-color); +} + +span.keywordtype { + color: var(--code-type-keyword-color); +} + +span.keywordflow { + color: var(--code-flow-keyword-color); +} + +span.comment { + color: var(--code-comment-color); +} + +span.preprocessor { + color: var(--code-preprocessor-color); +} + +span.stringliteral { + color: var(--code-string-literal-color); +} + +span.charliteral { + color: var(--code-char-literal-color); +} + +span.xmlcdata { + color: var(--code-xml-cdata-color); +} + +span.vhdldigit { + color: var(--code-vhdl-digit-color); +} + +span.vhdlchar { + color: var(--code-vhdl-char-color); +} + +span.vhdlkeyword { + color: var(--code-vhdl-keyword-color); +} + +span.vhdllogic { + color: var(--code-vhdl-logic-color); +} + +blockquote { + background-color: var(--blockquote-background-color); + border-left: 2px solid var(--blockquote-border-color); + margin: 0 24px 0 4px; + padding: 0 12px 0 16px; +} + +/* @end */ + +td.tiny { + font-size: 75%; +} + +.dirtab { + padding: 4px; + border-collapse: collapse; + border: 1px solid var(--table-cell-border-color); +} + +th.dirtab { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-weight: bold; +} + +hr { + border: none; + margin-top: 16px; + margin-bottom: 16px; + height: 1px; + box-shadow: 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); +} + +hr.footer { + height: 1px; +} + +/* @group Member Descriptions */ + +table.memberdecls { + border-spacing: 0px; + padding: 0px; +} + +.memberdecls td, .fieldtable tr { + transition-property: background-color, box-shadow; + transition-duration: 0.5s; +} + +.memberdecls td.glow, .fieldtable tr.glow { + background-color: var(--glow-color); + box-shadow: 0 0 15px var(--glow-color); +} + +.memberdecls tr[class^='memitem'] { + font-family: var(--font-family-monospace); +} + +.mdescLeft, .mdescRight, +.memItemLeft, .memItemRight { + padding-top: 2px; + padding-bottom: 2px; +} + +.memTemplParams { + padding-left: 10px; + padding-top: 5px; +} + +.memItemLeft, .memItemRight, .memTemplParams { + background-color: var(--memdecl-background-color); +} + +.mdescLeft, .mdescRight { + padding: 0px 8px 4px 8px; + color: var(--memdecl-foreground-color); +} + +tr[class^='memdesc'] { + box-shadow: inset 0px 1px 3px 0px rgba(0,0,0,.075); +} + +.mdescLeft { + border-left: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); +} + +.mdescRight { + border-right: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); +} + +.memTemplParams { + color: var(--memdecl-template-color); + white-space: nowrap; + font-size: 80%; + border-left: 1px solid var(--memdecl-border-color); + border-right: 1px solid var(--memdecl-border-color); +} + +td.ititle { + border: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding-left: 10px; +} + +tr:not(:first-child) > td.ititle { + border-top: 0; + border-radius: 0; +} + +.memItemLeft { + white-space: nowrap; + border-left: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); + padding-left: 10px; + transition: none; +} + +.memItemRight { + width: 100%; + border-right: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); + padding-right: 10px; + transition: none; +} + +tr.heading + tr[class^='memitem'] td.memItemLeft, +tr.groupHeader + tr[class^='memitem'] td.memItemLeft, +tr.inherit_header + tr[class^='memitem'] td.memItemLeft { + border-top: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; +} + +tr.heading + tr[class^='memitem'] td.memItemRight, +tr.groupHeader + tr[class^='memitem'] td.memItemRight, +tr.inherit_header + tr[class^='memitem'] td.memItemRight { + border-top: 1px solid var(--memdecl-border-color); + border-top-right-radius: 4px; +} + +tr.heading + tr[class^='memitem'] td.memTemplParams, +tr.heading + tr td.ititle, +tr.groupHeader + tr[class^='memitem'] td.memTemplParams, +tr.groupHeader + tr td.ititle, +tr.inherit_header + tr[class^='memitem'] td.memTemplParams { + border-top: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +table.memberdecls tr:last-child td.memItemLeft, +table.memberdecls tr:last-child td.mdescLeft, +table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemLeft, +table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemLeft, +table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescLeft, +table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescLeft { + border-bottom-left-radius: 4px; +} + +table.memberdecls tr:last-child td.memItemRight, +table.memberdecls tr:last-child td.mdescRight, +table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemRight, +table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemRight, +table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescRight, +table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescRight { + border-bottom-right-radius: 4px; +} + +tr.template .memItemLeft, tr.template .memItemRight { + border-top: none; + padding-top: 0; +} + + +/* @end */ + +/* @group Member Details */ + +/* Styles for detailed member documentation */ + +.memtitle { + padding: 8px; + border-top: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + border-top-right-radius: 4px; + border-top-left-radius: 4px; + margin-bottom: -1px; + background-color: var(--memdef-proto-background-color); + line-height: 1.25; + font-family: var(--font-family-monospace); + font-weight: 500; + font-size: 16px; + float:left; + box-shadow: 0 10px 0 -1px var(--memdef-proto-background-color), + 0 2px 8px 0 rgba(0,0,0,.075); + position: relative; +} + +.memtitle:after { + content: ''; + display: block; + background: var(--memdef-proto-background-color); + height: 10px; + bottom: -10px; + left: 0px; + right: -14px; + position: absolute; + border-top-right-radius: 6px; +} + +.permalink +{ + font-family: var(--font-family-monospace); + font-weight: 500; + line-height: 1.25; + font-size: 16px; + display: inline-block; + vertical-align: middle; +} + +.memtemplate { + font-size: 80%; + color: var(--memdef-template-color); + font-family: var(--font-family-monospace); + font-weight: normal; + margin-left: 9px; +} + +.mempage { + width: 100%; +} + +.memitem { + padding: 0; + margin-bottom: 10px; + margin-right: 5px; + display: table !important; + width: 100%; + box-shadow: 0 2px 8px 0 rgba(0,0,0,.075); + border-radius: 4px; +} + +.memitem.glow { + box-shadow: 0 0 15px var(--glow-color); +} + +.memname { + font-family: var(--font-family-monospace); + font-size: 13px; + font-weight: 400; + margin-left: 6px; +} + +.memname td { + vertical-align: bottom; +} + +.memproto, dl.reflist dt { + border-top: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + padding: 6px 0px 6px 0px; + color: var(--memdef-proto-text-color); + font-weight: bold; + background-color: var(--memdef-proto-background-color); + border-top-right-radius: 4px; + border-bottom: 1px solid var(--memdef-border-color); +} + +.overload { + font-family: var(--font-family-monospace); + font-size: 65%; +} + +.memdoc, dl.reflist dd { + border-bottom: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + padding: 6px 10px 2px 10px; + border-top-width: 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +dl.reflist dt { + padding: 5px; +} + +dl.reflist dd { + margin: 0px 0px 10px 0px; + padding: 5px; +} + +.paramkey { + text-align: right; +} + +.paramtype { + white-space: nowrap; + padding: 0px; + padding-bottom: 1px; +} + +.paramname { + white-space: nowrap; + padding: 0px; + padding-bottom: 1px; + margin-left: 2px; +} + +.paramname em { + color: var(--memdef-param-name-color); + font-style: normal; + margin-right: 1px; +} + +.paramname .paramdefval { + font-family: var(--font-family-monospace); +} + +.params, .retval, .exception, .tparams { + margin-left: 0px; + padding-left: 0px; +} + +.params .paramname, .retval .paramname, .tparams .paramname, .exception .paramname { + font-weight: bold; + vertical-align: top; +} + +.params .paramtype, .tparams .paramtype { + font-style: italic; + vertical-align: top; +} + +.params .paramdir, .tparams .paramdir { + font-family: var(--font-family-monospace); + vertical-align: top; +} + +table.mlabels { + border-spacing: 0px; +} + +td.mlabels-left { + width: 100%; + padding: 0px; +} + +td.mlabels-right { + vertical-align: bottom; + padding: 0px; + white-space: nowrap; +} + +span.mlabels { + margin-left: 8px; +} + +span.mlabel { + background-color: var(--label-background-color); + border-top:1px solid var(--label-left-top-border-color); + border-left:1px solid var(--label-left-top-border-color); + border-right:1px solid var(--label-right-bottom-border-color); + border-bottom:1px solid var(--label-right-bottom-border-color); + text-shadow: none; + color: var(--label-foreground-color); + margin-right: 4px; + padding: 2px 3px; + border-radius: 3px; + font-size: 7pt; + white-space: nowrap; + vertical-align: middle; +} + + + +/* @end */ + +/* these are for tree view inside a (index) page */ + +div.directory { + margin: 10px 0px; + width: 100%; +} + +.directory table { + border-collapse:collapse; +} + +.directory td { + margin: 0px; + padding: 0px; + vertical-align: top; +} + +.directory td.entry { + white-space: nowrap; + padding-right: 6px; + padding-top: 3px; +} + +.directory td.entry a { + outline:none; +} + +.directory td.entry a img { + border: none; +} + +.directory td.desc { + width: 100%; + padding-left: 6px; + padding-right: 6px; + padding-top: 3px; + border-left: 1px solid rgba(0,0,0,0.05); +} + +.directory tr.odd { + padding-left: 6px; + background-color: var(--index-odd-item-bg-color); +} + +.directory tr.even { + padding-left: 6px; + background-color: var(--index-even-item-bg-color); +} + +.directory img { + vertical-align: -30%; +} + +.directory .levels { + white-space: nowrap; + width: 100%; + text-align: right; + font-size: 9pt; +} + +.directory .levels span { + cursor: pointer; + padding-left: 2px; + padding-right: 2px; + color: var(--page-link-color); +} + +.arrow { + color: var(--nav-background-color); + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + font-size: 80%; + display: inline-block; + width: 16px; + height: 14px; + transition: opacity 0.3s ease; +} + +span.arrowhead { + position: relative; + padding: 0; + margin: 0 0 0 2px; + display: inline-block; + width: 5px; + height: 5px; + border-right: 2px solid var(--nav-arrow-color); + border-bottom: 2px solid var(--nav-arrow-color); + transform: rotate(-45deg); + transition: transform 0.3s ease; +} + +span.arrowhead.opened { + transform: rotate(45deg); +} + +.selected span.arrowhead { + border-right: 2px solid var(--nav-arrow-selected-color); + border-bottom: 2px solid var(--nav-arrow-selected-color); +} + +.icon { + font-family: var(--font-family-icon); + line-height: normal; + font-weight: bold; + font-size: 12px; + height: 14px; + width: 16px; + display: inline-block; + background-color: var(--icon-background-color); + color: var(--icon-foreground-color); + text-align: center; + border-radius: 4px; + margin-left: 2px; + margin-right: 2px; +} + +.icona { + width: 24px; + height: 22px; + display: inline-block; +} + +.iconfolder { + width: 24px; + height: 18px; + margin-top: 6px; + vertical-align:top; + display: inline-block; + position: relative; +} + +.icondoc { + width: 24px; + height: 18px; + margin-top: 3px; + vertical-align:top; + display: inline-block; + position: relative; +} + +.folder-icon { + width: 16px; + height: 11px; + background-color: var(--icon-folder-fill-color); + border: 1px solid var(--icon-folder-border-color); + border-radius: 0 2px 2px 2px; + position: relative; + box-sizing: content-box; +} + +.folder-icon::after { + content: ''; + position: absolute; + top: 2px; + left: -1px; + width: 16px; + height: 7px; + background-color: var(--icon-folder-open-fill-color); + border: 1px solid var(--icon-folder-border-color); + border-radius: 7px 7px 2px 2px; + transform-origin: top left; + opacity: 0; + transition: all 0.3s linear; +} + +.folder-icon::before { + content: ''; + position: absolute; + top: -3px; + left: -1px; + width: 6px; + height: 2px; + background-color: var(--icon-folder-fill-color); + border-top: 1px solid var(--icon-folder-border-color); + border-left: 1px solid var(--icon-folder-border-color); + border-right: 1px solid var(--icon-folder-border-color); + border-radius: 2px 2px 0 0; +} + +.folder-icon.open::after { + top: 3px; + opacity: 1; +} + +.doc-icon { + left: 6px; + width: 12px; + height: 16px; + background-color: var(--icon-doc-border-color); + clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); + position: relative; + display: inline-block; +} +.doc-icon::before { + content: ""; + left: 1px; + top: 1px; + width: 10px; + height: 14px; + background-color: var(--icon-doc-fill-color); + clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); + position: absolute; + box-sizing: border-box; +} +.doc-icon::after { + content: ""; + left: 7px; + top: 0px; + width: 3px; + height: 3px; + background-color: transparent; + position: absolute; + border: 1px solid var(--icon-doc-border-color); +} + + + + +/* @end */ + +div.dynheader { + margin-top: 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.dynarrow { + position: relative; + display: inline-block; + width: 12px; + bottom: 1px; +} + +address { + font-style: normal; + color: var(--footer-foreground-color); +} + +table.doxtable caption { + caption-side: top; +} + +table.doxtable { + border-collapse:collapse; + margin-top: 4px; + margin-bottom: 4px; +} + +table.doxtable td, table.doxtable th { + border: 1px solid var(--table-cell-border-color); + padding: 3px 7px 2px; +} + +table.doxtable th { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; +} + +table.fieldtable { + margin-bottom: 10px; + border: 1px solid var(--memdef-border-color); + border-spacing: 0px; + border-radius: 4px; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); +} + +.fieldtable td, .fieldtable th { + padding: 3px 7px 2px; +} + +.fieldtable td.fieldtype, .fieldtable td.fieldname, .fieldtable td.fieldinit { + white-space: nowrap; + border-right: 1px solid var(--memdef-border-color); + border-bottom: 1px solid var(--memdef-border-color); + vertical-align: top; +} + +.fieldtable td.fieldname { + padding-top: 3px; +} + +.fieldtable td.fieldinit { + padding-top: 3px; + text-align: right; +} + + +.fieldtable td.fielddoc { + border-bottom: 1px solid var(--memdef-border-color); +} + +.fieldtable td.fielddoc p:first-child { + margin-top: 0px; +} + +.fieldtable td.fielddoc p:last-child { + margin-bottom: 2px; +} + +.fieldtable tr:last-child td { + border-bottom: none; +} + +.fieldtable th { + background-color: var(--memdef-title-background-color); + font-size: 90%; + color: var(--memdef-proto-text-color); + padding-bottom: 4px; + padding-top: 5px; + text-align:left; + font-weight: 400; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 1px solid var(--memdef-border-color); +} + +/* ----------- navigation breadcrumb styling ----------- */ + +#nav-path ul { + height: 30px; + line-height: 30px; + color: var(--nav-text-normal-color); + overflow: hidden; + margin: 0px; + padding-left: 4px; + background-image: none; + background: var(--page-background-color); + border-bottom: 1px solid var(--nav-breadcrumb-separator-color); + font-size: var(--nav-font-size-level1); + font-family: var(--font-family-nav); + position: relative; + z-index: 100; +} + +#main-nav { + border-bottom: 1px solid var(--nav-border-color); +} + +.navpath li { + list-style-type:none; + float:left; + color: var(--nav-foreground-color); +} + +.navpath li.footer { + list-style-type:none; + float:right; + padding-left:10px; + padding-right:15px; + background-image:none; + background-repeat:no-repeat; + background-position:right; + font-size: 8pt; + color: var(--footer-foreground-color); +} + +#nav-path li.navelem { + background-image: none; + display: flex; + align-items: center; + padding-left: 15px; +} + +.navpath li.navelem a { + text-shadow: none; + display: inline-block; + color: var(--nav-breadcrumb-color); + position: relative; + top: 0px; + height: 30px; + margin-right: -20px; +} + +#nav-path li.navelem:after { + content: ''; + display: inline-block; + position: relative; + top: 0; + right: -15px; + width: 30px; + height: 30px; + transform: scaleX(0.5) scale(0.707) rotate(45deg); + z-index: 10; + background: var(--page-background-color); + box-shadow: 2px -2px 0 2px var(--nav-breadcrumb-separator-color); + border-radius: 0 5px 0 50px; +} + +#nav-path li.navelem:first-child { + margin-left: -6px; +} + +#nav-path li.navelem:hover, +#nav-path li.navelem:hover:after { + background-color: var(--nav-breadcrumb-active-bg); +} + +/* ---------------------- */ + +div.summary +{ + float: right; + font-size: 8pt; + padding-right: 5px; + width: 50%; + text-align: right; +} + +div.summary a +{ + white-space: nowrap; +} + +table.classindex +{ + margin: 10px; + white-space: nowrap; + margin-left: 3%; + margin-right: 3%; + width: 94%; + border: 0; + border-spacing: 0; + padding: 0; +} + +div.ingroups +{ + font-size: 8pt; + width: 50%; + text-align: left; +} + +div.ingroups a +{ + white-space: nowrap; +} + +div.header +{ + margin: 0px; + background-color: var(--header-background-color); + border-bottom: 1px solid var(--header-separator-color); +} + +div.headertitle +{ + padding: 5px 5px 5px 10px; +} + +dl { + padding: 0 0 0 0; +} + +dl.bug dt a, dl.deprecated dt a, dl.todo dt a, dl.test a { + font-weight: bold !important; +} + +dl.warning, dl.attention, dl.important, dl.note, dl.deprecated, dl.bug, +dl.invariant, dl.pre, dl.post, dl.todo, dl.test, dl.remark { + padding: 10px; + margin: 10px 0px; + overflow: hidden; + margin-left: 0; + border-radius: 4px; +} + +dl.section dd { + margin-bottom: 2px; +} + +dl.warning, dl.attention, dl.important { + background: var(--warning-color-bg); + border-left: 8px solid var(--warning-color-hl); + color: var(--warning-color-text); +} + +dl.warning dt, dl.attention dt, dl.important dt { + color: var(--warning-color-hl); +} + +dl.note, dl.remark { + background: var(--note-color-bg); + border-left: 8px solid var(--note-color-hl); + color: var(--note-color-text); +} + +dl.note dt, dl.remark dt { + color: var(--note-color-hl); +} + +dl.todo { + background: var(--todo-color-bg); + border-left: 8px solid var(--todo-color-hl); + color: var(--todo-color-text); +} + +dl.todo dt { + color: var(--todo-color-hl); +} + +dl.test { + background: var(--test-color-bg); + border-left: 8px solid var(--test-color-hl); + color: var(--test-color-text); +} + +dl.test dt { + color: var(--test-color-hl); +} + +dl.bug dt a { + color: var(--bug-color-hl) !important; +} + +dl.bug { + background: var(--bug-color-bg); + border-left: 8px solid var(--bug-color-hl); + color: var(--bug-color-text); +} + +dl.bug dt a { + color: var(--bug-color-hl) !important; +} + +dl.deprecated { + background: var(--deprecated-color-bg); + border-left: 8px solid var(--deprecated-color-hl); + color: var(--deprecated-color-text); +} + +dl.deprecated dt a { + color: var(--deprecated-color-hl) !important; +} + +dl.note dd, dl.warning dd, dl.pre dd, dl.post dd, +dl.remark dd, dl.attention dd, dl.important dd, dl.invariant dd, +dl.bug dd, dl.deprecated dd, dl.todo dd, dl.test dd { + margin-inline-start: 0px; +} + +dl.invariant, dl.pre, dl.post { + background: var(--invariant-color-bg); + border-left: 8px solid var(--invariant-color-hl); + color: var(--invariant-color-text); +} + +dl.invariant dt, dl.pre dt, dl.post dt { + color: var(--invariant-color-hl); +} + + +#projectrow +{ + height: 56px; +} + +#projectlogo +{ + text-align: center; + vertical-align: bottom; + border-collapse: separate; +} + +#projectlogo img +{ + border: 0px none; +} + +#projectalign +{ + vertical-align: middle; + padding-left: 0.5em; +} + +#projectname +{ + font-size: 200%; + font-family: var(--font-family-title); + margin: 0; + padding: 0; +} + +#side-nav #projectname +{ + font-size: 130%; +} + +#projectbrief +{ + font-size: 90%; + font-family: var(--font-family-title); + margin: 0px; + padding: 0px; +} + +#projectnumber +{ + font-size: 50%; + font-family: var(--font-family-title); + margin: 0px; + padding: 0px; +} + +#titlearea +{ + padding: 0 0 0 5px; + margin: 0px; + border-bottom: 1px solid var(--title-separator-color); + background-color: var(--title-background-color); +} + +.image +{ + text-align: center; +} + +.dotgraph +{ + text-align: center; +} + +.mscgraph +{ + text-align: center; +} + +.plantumlgraph +{ + text-align: center; +} + +.diagraph +{ + text-align: center; +} + +.caption +{ + font-weight: bold; +} + +dl.citelist { + margin-bottom:50px; +} + +dl.citelist dt { + color:var(--citation-label-color); + float:left; + font-weight:bold; + margin-right:10px; + padding:5px; + text-align:right; + width:52px; +} + +dl.citelist dd { + margin:2px 0 2px 72px; + padding:5px 0; +} + +div.toc { + padding: 14px 25px; + background-color: var(--toc-background-color); + border: 1px solid var(--toc-border-color); + border-radius: 7px 7px 7px 7px; + float: right; + height: auto; + margin: 0 8px 10px 10px; + width: 200px; +} + +div.toc li { + background: var(--toc-down-arrow-image) no-repeat scroll 0 5px transparent; + font: 10px/1.2 var(--font-family-toc); + margin-top: 5px; + padding-left: 10px; + padding-top: 2px; +} + +div.toc h3 { + font: bold 12px/1.2 var(--font-family-toc); + color: var(--toc-header-color); + border-bottom: 0 none; + margin: 0; +} + +div.toc ul { + list-style: none outside none; + border: medium none; + padding: 0px; +} + +div.toc li[class^='level'] { + margin-left: 15px; +} + +div.toc li.level1 { + margin-left: 0px; +} + +div.toc li.empty { + background-image: none; + margin-top: 0px; +} + +span.emoji { + /* font family used at the site: https://unicode.org/emoji/charts/full-emoji-list.html + * font-family: "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort; + */ +} + +span.obfuscator { + display: none; +} + +.inherit_header { + font-weight: 400; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.inherit_header td { + padding: 6px 0 2px 0; +} + +.inherit { + display: none; +} + +tr.heading h2 { + margin-top: 12px; + margin-bottom: 12px; +} + +/* tooltip related style info */ + +.ttc { + position: absolute; + display: none; +} + +#powerTip { + cursor: default; + color: var(--tooltip-foreground-color); + background-color: var(--tooltip-background-color); + backdrop-filter: var(--tooltip-backdrop-filter); + -webkit-backdrop-filter: var(--tooltip-backdrop-filter); + border: 1px solid var(--tooltip-border-color); + border-radius: 4px; + box-shadow: var(--tooltip-shadow); + display: none; + font-size: smaller; + max-width: 80%; + padding: 1ex 1em 1em; + position: absolute; + z-index: 2147483647; +} + +#powerTip div.ttdoc { + color: var(--tooltip-doc-color); + font-style: italic; +} + +#powerTip div.ttname a { + font-weight: bold; +} + +#powerTip a { + color: var(--tooltip-link-color); +} + +#powerTip div.ttname { + font-weight: bold; +} + +#powerTip div.ttdeci { + color: var(--tooltip-declaration-color); +} + +#powerTip div { + margin: 0px; + padding: 0px; + font-size: 12px; + font-family: var(--font-family-tooltip); + line-height: 16px; +} + +#powerTip:before, #powerTip:after { + content: ""; + position: absolute; + margin: 0px; +} + +#powerTip.n:after, #powerTip.n:before, +#powerTip.s:after, #powerTip.s:before, +#powerTip.w:after, #powerTip.w:before, +#powerTip.e:after, #powerTip.e:before, +#powerTip.ne:after, #powerTip.ne:before, +#powerTip.se:after, #powerTip.se:before, +#powerTip.nw:after, #powerTip.nw:before, +#powerTip.sw:after, #powerTip.sw:before { + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; +} + +#powerTip.n:after, #powerTip.s:after, +#powerTip.w:after, #powerTip.e:after, +#powerTip.nw:after, #powerTip.ne:after, +#powerTip.sw:after, #powerTip.se:after { + border-color: rgba(255, 255, 255, 0); +} + +#powerTip.n:before, #powerTip.s:before, +#powerTip.w:before, #powerTip.e:before, +#powerTip.nw:before, #powerTip.ne:before, +#powerTip.sw:before, #powerTip.se:before { + border-color: rgba(128, 128, 128, 0); +} + +#powerTip.n:after, #powerTip.n:before, +#powerTip.ne:after, #powerTip.ne:before, +#powerTip.nw:after, #powerTip.nw:before { + top: 100%; +} + +#powerTip.n:after, #powerTip.ne:after, #powerTip.nw:after { + border-top-color: var(--tooltip-arrow-background-color); + border-width: 10px; + margin: 0px -10px; +} +#powerTip.n:before, #powerTip.ne:before, #powerTip.nw:before { + border-top-color: var(--tooltip-border-color); + border-width: 11px; + margin: 0px -11px; +} +#powerTip.n:after, #powerTip.n:before { + left: 50%; +} + +#powerTip.nw:after, #powerTip.nw:before { + right: 14px; +} + +#powerTip.ne:after, #powerTip.ne:before { + left: 14px; +} + +#powerTip.s:after, #powerTip.s:before, +#powerTip.se:after, #powerTip.se:before, +#powerTip.sw:after, #powerTip.sw:before { + bottom: 100%; +} + +#powerTip.s:after, #powerTip.se:after, #powerTip.sw:after { + border-bottom-color: var(--tooltip-arrow-background-color); + border-width: 10px; + margin: 0px -10px; +} + +#powerTip.s:before, #powerTip.se:before, #powerTip.sw:before { + border-bottom-color: var(--tooltip-border-color); + border-width: 11px; + margin: 0px -11px; +} + +#powerTip.s:after, #powerTip.s:before { + left: 50%; +} + +#powerTip.sw:after, #powerTip.sw:before { + right: 14px; +} + +#powerTip.se:after, #powerTip.se:before { + left: 14px; +} + +#powerTip.e:after, #powerTip.e:before { + left: 100%; +} +#powerTip.e:after { + border-left-color: var(--tooltip-border-color); + border-width: 10px; + top: 50%; + margin-top: -10px; +} +#powerTip.e:before { + border-left-color: var(--tooltip-border-color); + border-width: 11px; + top: 50%; + margin-top: -11px; +} + +#powerTip.w:after, #powerTip.w:before { + right: 100%; +} +#powerTip.w:after { + border-right-color: var(--tooltip-border-color); + border-width: 10px; + top: 50%; + margin-top: -10px; +} +#powerTip.w:before { + border-right-color: var(--tooltip-border-color); + border-width: 11px; + top: 50%; + margin-top: -11px; +} + +@media print +{ + #top { display: none; } + #side-nav { display: none; } + #nav-path { display: none; } + body { overflow:visible; } + h1, h2, h3, h4, h5, h6 { page-break-after: avoid; } + .summary { display: none; } + .memitem { page-break-inside: avoid; } + #doc-content + { + margin-left:0 !important; + height:auto !important; + width:auto !important; + overflow:inherit; + display:inline; + } +} + +/* @group Markdown */ + +table.markdownTable { + border-collapse:collapse; + margin-top: 4px; + margin-bottom: 4px; +} + +table.markdownTable td, table.markdownTable th { + border: 1px solid var(--table-cell-border-color); + padding: 3px 7px 2px; +} + +table.markdownTable tr { +} + +th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; +} + +th.markdownTableHeadLeft, td.markdownTableBodyLeft { + text-align: left +} + +th.markdownTableHeadRight, td.markdownTableBodyRight { + text-align: right +} + +th.markdownTableHeadCenter, td.markdownTableBodyCenter { + text-align: center +} + +tt, code, kbd +{ + display: inline-block; +} +tt, code, kbd +{ + vertical-align: top; +} +/* @end */ + +u { + text-decoration: underline; +} + +details>summary { + list-style-type: none; +} + +details > summary::-webkit-details-marker { + display: none; +} + +details>summary::before { + content: "\25ba"; + padding-right:4px; + font-size: 80%; +} + +details[open]>summary::before { + content: "\25bc"; + padding-right:4px; + font-size: 80%; +} + +:root { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-background-color); +} + +::-webkit-scrollbar { + background-color: var(--scrollbar-background-color); + height: 12px; + width: 12px; +} +::-webkit-scrollbar-thumb { + border-radius: 6px; + box-shadow: inset 0 0 12px 12px var(--scrollbar-thumb-color); + border: solid 2px transparent; +} +::-webkit-scrollbar-corner { + background-color: var(--scrollbar-background-color); +} + From 48d5fc925f0cae0c8f0b4eaa7422d9d05f62b383 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 08:27:27 -0700 Subject: [PATCH 190/319] doxygen: better scrollbar styling --- dist/doxygen/ghostty.css | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/dist/doxygen/ghostty.css b/dist/doxygen/ghostty.css index 9670a70ca..11b5130ee 100644 --- a/dist/doxygen/ghostty.css +++ b/dist/doxygen/ghostty.css @@ -54,6 +54,43 @@ h6, background: rgba(53, 81, 243, 0.6); } +/* Modern scrollbar styling for WebKit browsers (Safari, Chrome) */ +::-webkit-scrollbar { + width: 14px; + height: 14px; + -webkit-appearance: none; +} + +::-webkit-scrollbar-track { + background: #1a1f2e; + border-radius: 8px; +} + +::-webkit-scrollbar-thumb { + background: #4a5260; + border-radius: 8px; + border: 3px solid #1a1f2e; + min-height: 40px; +} + +::-webkit-scrollbar-thumb:hover { + background: #5a6270; +} + +::-webkit-scrollbar-thumb:active { + background: #6a7280; +} + +::-webkit-scrollbar-corner { + background: #1a1f2e; +} + +/* Firefox scrollbar styling */ +* { + scrollbar-width: thin; + scrollbar-color: #404754 #1a1f2e; +} + /* Tree view selected item */ #nav-tree .selected { background-color: #3551f3 !important; From 9194d6c496c56a680fb6010ae52cfaf8bdc53343 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 08:36:19 -0700 Subject: [PATCH 191/319] doxygen: integrate examples into documentation --- Doxyfile | 3 +++ include/ghostty/vt.h | 25 +++++++++++++++++++++---- src/build/docker/lib-c-docs/Dockerfile | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Doxyfile b/Doxyfile index 0c8688987..58b6be48a 100644 --- a/Doxyfile +++ b/Doxyfile @@ -6,6 +6,9 @@ PROJECT_LOGO = images/gnome/64.png INPUT = include/ghostty/vt.h INPUT_ENCODING = UTF-8 RECURSIVE = NO +EXAMPLE_PATH = example +EXAMPLE_RECURSIVE = YES +EXAMPLE_PATTERNS = * FULL_PATH_NAMES = NO STRIP_FROM_INC_PATH = include SOURCE_BROWSER = YES diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index ebb41e300..bcbb01d00 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -1,7 +1,7 @@ /** * @file vt.h * - * libghostty-vt - Virtual terminal sequence parsing library + * libghostty-vt - Virtual terminal emulator library * * This library provides functionality for parsing and handling terminal * escape sequences as well as maintaining terminal state such as styles, @@ -12,14 +12,15 @@ */ /** - * @mainpage libghostty-vt - Virtual Terminal Sequence Parser + * @mainpage libghostty-vt - Virtual Terminal Emulator Library * * libghostty-vt is a C library which implements a modern terminal emulator, * extracted from the [Ghostty](https://ghostty.org) terminal emulator. * * libghostty-vt contains the logic for handling the core parts of a terminal - * emulator: parsing terminal escape sequences and maintaining terminal state. - * It can handle scrollback, line wrapping, reflow on resize, and more. + * emulator: parsing terminal escape sequences, maintaining terminal state, + * encoding input events, etc. It can handle scrollback, line wrapping, + * reflow on resize, and more. * * @warning This library is currently in development and the API is not yet stable. * Breaking changes are expected in future versions. Use with caution in production code. @@ -31,6 +32,22 @@ * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref allocator "Memory Management" - Memory management and custom allocators * + * @section examples_sec Examples + * + * Complete working examples: + * - @ref c-vt/src/main.c - OSC parser example + * - @ref c-vt-key-encode/src/main.c - Key encoding example + * + */ + +/** @example c-vt/src/main.c + * This example demonstrates how to use the OSC parser to parse an OSC sequence, + * extract command information, and retrieve command-specific data like window titles. + */ + +/** @example c-vt-key-encode/src/main.c + * This example demonstrates how to use the key encoder to convert key events + * into terminal escape sequences using the Kitty keyboard protocol. */ #ifndef GHOSTTY_VT_H diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile index 8b4f2f6df..a3cfdcc98 100644 --- a/src/build/docker/lib-c-docs/Dockerfile +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -14,6 +14,7 @@ WORKDIR /ghostty COPY include/ ./include/ COPY images/ ./images/ COPY dist/doxygen/ ./dist/doxygen/ +COPY example/ ./example/ COPY Doxyfile ./ COPY DoxygenLayout.xml ./ RUN mkdir -p zig-out/share/ghostty/doc/libghostty From ffbdfbd1e68083d5d1f5bdbbeafc7352552e738b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 08:45:24 -0700 Subject: [PATCH 192/319] prettier --- dist/doxygen/ghostty.css | 87 +- dist/doxygen/mobile-nav.js | 50 +- dist/doxygen/stylesheet.css | 3254 +++++++++++++++++++---------------- 3 files changed, 1801 insertions(+), 1590 deletions(-) diff --git a/dist/doxygen/ghostty.css b/dist/doxygen/ghostty.css index 11b5130ee..678414b70 100644 --- a/dist/doxygen/ghostty.css +++ b/dist/doxygen/ghostty.css @@ -159,7 +159,7 @@ span.lineno a { #page-nav-toggle { display: none !important; } - + #page-nav { position: relative !important; width: 250px !important; @@ -175,17 +175,17 @@ span.lineno a { body { font-size: 14px !important; } - + /* Make navigation tree collapsible on mobile */ #side-nav { display: none; } - + #doc-content { margin-left: 0 !important; margin-right: 0 !important; } - + /* Make right sidebar (page-nav) overlay on mobile */ #page-nav { position: fixed !important; @@ -200,11 +200,11 @@ span.lineno a { overflow-y: auto !important; -webkit-overflow-scrolling: touch !important; } - + #page-nav.mobile-open { right: 0 !important; } - + /* Hamburger menu button for page nav */ #page-nav-toggle { display: block !important; @@ -221,7 +221,7 @@ span.lineno a { padding: 8px !important; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important; } - + #page-nav-toggle span { display: block !important; width: 24px !important; @@ -231,7 +231,7 @@ span.lineno a { border-radius: 2px !important; transition: 0.3s !important; } - + /* Mobile overlay backdrop */ #page-nav-backdrop { display: none !important; @@ -243,29 +243,29 @@ span.lineno a { background: rgba(0, 0, 0, 0.5) !important; z-index: 9999 !important; } - + #page-nav-backdrop.active { display: block !important; } - + /* Improve header and navigation */ #top { height: auto !important; } - + #titlearea { padding: 10px !important; } - + #projectname { font-size: 18px !important; } - + #projectbrief, #projectnumber { font-size: 12px !important; } - + /* Make tabs stack better on mobile */ #navrow1, #navrow2, @@ -275,33 +275,33 @@ span.lineno a { white-space: nowrap !important; -webkit-overflow-scrolling: touch !important; } - + .tablist li { display: inline-block !important; } - + /* Content adjustments */ .contents { padding: 10px !important; width: 100% !important; box-sizing: border-box !important; } - + .header { padding: 5px !important; } - + /* Code blocks */ .fragment { font-size: 12px !important; overflow-x: auto !important; -webkit-overflow-scrolling: touch !important; } - + div.line { font-size: 12px !important; } - + /* Tables */ table { display: block !important; @@ -309,52 +309,65 @@ span.lineno a { -webkit-overflow-scrolling: touch !important; width: 100% !important; } - + .memberdecls table, .fieldtable { font-size: 12px !important; } - + .memtitle { font-size: 14px !important; padding: 8px !important; } - + .memname { font-size: 13px !important; word-break: break-word !important; } - + .memitem { margin: 5px 0 !important; } - + /* Search box */ #MSearchBox { width: 100% !important; right: 0 !important; } - + /* Reduce padding and margins */ - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { margin-top: 10px !important; margin-bottom: 8px !important; } - - h1 { font-size: 22px !important; } - h2 { font-size: 18px !important; } - h3 { font-size: 16px !important; } - h4 { font-size: 14px !important; } - + + h1 { + font-size: 22px !important; + } + h2 { + font-size: 18px !important; + } + h3 { + font-size: 16px !important; + } + h4 { + font-size: 14px !important; + } + /* Directory/file listings */ .directory .levels span { display: none !important; } - + .directory .arrow { margin-right: 5px !important; } - + /* Treeview adjustments */ #nav-tree { width: 100% !important; @@ -366,11 +379,11 @@ span.lineno a { .contents { padding: 15px !important; } - + #side-nav { width: 200px !important; } - + #doc-content { margin-left: 200px !important; } diff --git a/dist/doxygen/mobile-nav.js b/dist/doxygen/mobile-nav.js index 1262360b8..c6c4e2214 100644 --- a/dist/doxygen/mobile-nav.js +++ b/dist/doxygen/mobile-nav.js @@ -2,7 +2,7 @@ * Mobile navigation toggle for Doxygen documentation */ -(function() { +(function () { // Only run on mobile devices function isMobile() { return window.innerWidth <= 767; @@ -10,56 +10,56 @@ function initMobileNav() { if (!isMobile()) return; - - const pageNav = document.getElementById('page-nav'); + + const pageNav = document.getElementById("page-nav"); if (!pageNav) return; // Create toggle button - const toggleBtn = document.createElement('button'); - toggleBtn.id = 'page-nav-toggle'; - toggleBtn.setAttribute('aria-label', 'Toggle page navigation'); - toggleBtn.innerHTML = ''; + const toggleBtn = document.createElement("button"); + toggleBtn.id = "page-nav-toggle"; + toggleBtn.setAttribute("aria-label", "Toggle page navigation"); + toggleBtn.innerHTML = ""; document.body.appendChild(toggleBtn); // Create backdrop - const backdrop = document.createElement('div'); - backdrop.id = 'page-nav-backdrop'; + const backdrop = document.createElement("div"); + backdrop.id = "page-nav-backdrop"; document.body.appendChild(backdrop); // Toggle function function toggleNav() { - const isOpen = pageNav.classList.toggle('mobile-open'); - backdrop.classList.toggle('active', isOpen); - document.body.style.overflow = isOpen ? 'hidden' : ''; + const isOpen = pageNav.classList.toggle("mobile-open"); + backdrop.classList.toggle("active", isOpen); + document.body.style.overflow = isOpen ? "hidden" : ""; } // Event listeners - toggleBtn.addEventListener('click', toggleNav); - backdrop.addEventListener('click', toggleNav); + toggleBtn.addEventListener("click", toggleNav); + backdrop.addEventListener("click", toggleNav); // Close on escape key - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape' && pageNav.classList.contains('mobile-open')) { + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && pageNav.classList.contains("mobile-open")) { toggleNav(); } }); } // Initialize on load and resize - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initMobileNav); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initMobileNav); } else { initMobileNav(); } - window.addEventListener('resize', function() { - const pageNav = document.getElementById('page-nav'); - const backdrop = document.getElementById('page-nav-backdrop'); - + window.addEventListener("resize", function () { + const pageNav = document.getElementById("page-nav"); + const backdrop = document.getElementById("page-nav-backdrop"); + if (!isMobile() && pageNav) { - pageNav.classList.remove('mobile-open'); - if (backdrop) backdrop.classList.remove('active'); - document.body.style.overflow = ''; + pageNav.classList.remove("mobile-open"); + if (backdrop) backdrop.classList.remove("active"); + document.body.style.overflow = ""; } }); })(); diff --git a/dist/doxygen/stylesheet.css b/dist/doxygen/stylesheet.css index 5d2eecd6d..21f34435e 100644 --- a/dist/doxygen/stylesheet.css +++ b/dist/doxygen/stylesheet.css @@ -1,509 +1,545 @@ /* The standard CSS for doxygen 1.14.0*/ html { -/* page base colors */ ---page-background-color: white; ---page-foreground-color: black; ---page-link-color: #3D578C; ---page-visited-link-color: #3D578C; ---page-external-link-color: #334975; + /* page base colors */ + --page-background-color: white; + --page-foreground-color: black; + --page-link-color: #3d578c; + --page-visited-link-color: #3d578c; + --page-external-link-color: #334975; -/* index */ ---index-odd-item-bg-color: #F8F9FC; ---index-even-item-bg-color: white; ---index-header-color: black; ---index-separator-color: #A0A0A0; + /* index */ + --index-odd-item-bg-color: #f8f9fc; + --index-even-item-bg-color: white; + --index-header-color: black; + --index-separator-color: #a0a0a0; -/* header */ ---header-background-color: #F9FAFC; ---header-separator-color: #C4CFE5; ---group-header-separator-color: #D9E0EE; ---group-header-color: #354C7B; ---inherit-header-color: gray; + /* header */ + --header-background-color: #f9fafc; + --header-separator-color: #c4cfe5; + --group-header-separator-color: #d9e0ee; + --group-header-color: #354c7b; + --inherit-header-color: gray; ---footer-foreground-color: #2A3D61; ---footer-logo-width: 75px; ---citation-label-color: #334975; ---glow-color: cyan; + --footer-foreground-color: #2a3d61; + --footer-logo-width: 75px; + --citation-label-color: #334975; + --glow-color: cyan; ---title-background-color: white; ---title-separator-color: #C4CFE5; ---directory-separator-color: #9CAFD4; ---separator-color: #4A6AAA; + --title-background-color: white; + --title-separator-color: #c4cfe5; + --directory-separator-color: #9cafd4; + --separator-color: #4a6aaa; ---blockquote-background-color: #F7F8FB; ---blockquote-border-color: #9CAFD4; + --blockquote-background-color: #f7f8fb; + --blockquote-border-color: #9cafd4; ---scrollbar-thumb-color: #C4CFE5; ---scrollbar-background-color: #F9FAFC; + --scrollbar-thumb-color: #c4cfe5; + --scrollbar-background-color: #f9fafc; ---icon-background-color: #728DC1; ---icon-foreground-color: white; -/* + --icon-background-color: #728dc1; + --icon-foreground-color: white; + /* --icon-doc-image: url('doc.svg'); --icon-folder-open-image: url('folderopen.svg'); --icon-folder-closed-image: url('folderclosed.svg');*/ ---icon-folder-open-fill-color: #C4CFE5; ---icon-folder-fill-color: #D8DFEE; ---icon-folder-border-color: #4665A2; ---icon-doc-fill-color: #D8DFEE; ---icon-doc-border-color: #4665A2; + --icon-folder-open-fill-color: #c4cfe5; + --icon-folder-fill-color: #d8dfee; + --icon-folder-border-color: #4665a2; + --icon-doc-fill-color: #d8dfee; + --icon-doc-border-color: #4665a2; -/* brief member declaration list */ ---memdecl-background-color: #F9FAFC; ---memdecl-separator-color: #DEE4F0; ---memdecl-foreground-color: #555; ---memdecl-template-color: #4665A2; ---memdecl-border-color: #D5DDEC; + /* brief member declaration list */ + --memdecl-background-color: #f9fafc; + --memdecl-separator-color: #dee4f0; + --memdecl-foreground-color: #555; + --memdecl-template-color: #4665a2; + --memdecl-border-color: #d5ddec; -/* detailed member list */ ---memdef-border-color: #A8B8D9; ---memdef-title-background-color: #E2E8F2; ---memdef-proto-background-color: #EEF1F7; ---memdef-proto-text-color: #253555; ---memdef-doc-background-color: white; ---memdef-param-name-color: #602020; ---memdef-template-color: #4665A2; + /* detailed member list */ + --memdef-border-color: #a8b8d9; + --memdef-title-background-color: #e2e8f2; + --memdef-proto-background-color: #eef1f7; + --memdef-proto-text-color: #253555; + --memdef-doc-background-color: white; + --memdef-param-name-color: #602020; + --memdef-template-color: #4665a2; -/* tables */ ---table-cell-border-color: #2D4068; ---table-header-background-color: #374F7F; ---table-header-foreground-color: #FFFFFF; + /* tables */ + --table-cell-border-color: #2d4068; + --table-header-background-color: #374f7f; + --table-header-foreground-color: #ffffff; -/* labels */ ---label-background-color: #728DC1; ---label-left-top-border-color: #5373B4; ---label-right-bottom-border-color: #C4CFE5; ---label-foreground-color: white; + /* labels */ + --label-background-color: #728dc1; + --label-left-top-border-color: #5373b4; + --label-right-bottom-border-color: #c4cfe5; + --label-foreground-color: white; -/** navigation bar/tree/menu */ ---nav-background-color: #F9FAFC; ---nav-foreground-color: #364D7C; ---nav-border-color: #C4CFE5; ---nav-breadcrumb-separator-color: #C4CFE5; ---nav-breadcrumb-active-bg: #EEF1F7; ---nav-breadcrumb-color: #354C7B; ---nav-breadcrumb-border-color: #E1E7F2; ---nav-splitbar-bg-color: #DCE2EF; ---nav-splitbar-handle-color: #9CAFD4; ---nav-font-size-level1: 13px; ---nav-font-size-level2: 10px; ---nav-font-size-level3: 9px; ---nav-text-normal-color: #283A5D; ---nav-text-hover-color: white; ---nav-text-active-color: white; ---nav-menu-button-color: #364D7C; ---nav-menu-background-color: white; ---nav-menu-foreground-color: #555555; ---nav-menu-active-bg: #DCE2EF; ---nav-menu-active-color: #9CAFD4; ---nav-menu-toggle-color: rgba(255, 255, 255, 0.5); ---nav-arrow-color: #B6C4DF; ---nav-arrow-selected-color: #90A5CE; + /** navigation bar/tree/menu */ + --nav-background-color: #f9fafc; + --nav-foreground-color: #364d7c; + --nav-border-color: #c4cfe5; + --nav-breadcrumb-separator-color: #c4cfe5; + --nav-breadcrumb-active-bg: #eef1f7; + --nav-breadcrumb-color: #354c7b; + --nav-breadcrumb-border-color: #e1e7f2; + --nav-splitbar-bg-color: #dce2ef; + --nav-splitbar-handle-color: #9cafd4; + --nav-font-size-level1: 13px; + --nav-font-size-level2: 10px; + --nav-font-size-level3: 9px; + --nav-text-normal-color: #283a5d; + --nav-text-hover-color: white; + --nav-text-active-color: white; + --nav-menu-button-color: #364d7c; + --nav-menu-background-color: white; + --nav-menu-foreground-color: #555555; + --nav-menu-active-bg: #dce2ef; + --nav-menu-active-color: #9cafd4; + --nav-menu-toggle-color: rgba(255, 255, 255, 0.5); + --nav-arrow-color: #b6c4df; + --nav-arrow-selected-color: #90a5ce; -/* sync icon */ ---sync-icon-border-color: #C4CFE5; ---sync-icon-background-color: #F9FAFC; ---sync-icon-selected-background-color: #EEF1F7; ---sync-icon-color: #C4CFE5; ---sync-icon-selected-color: #6884BD; + /* sync icon */ + --sync-icon-border-color: #c4cfe5; + --sync-icon-background-color: #f9fafc; + --sync-icon-selected-background-color: #eef1f7; + --sync-icon-color: #c4cfe5; + --sync-icon-selected-color: #6884bd; -/* table of contents */ ---toc-background-color: #F4F6FA; ---toc-border-color: #D8DFEE; ---toc-header-color: #4665A2; ---toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + /* table of contents */ + --toc-background-color: #f4f6fa; + --toc-border-color: #d8dfee; + --toc-header-color: #4665a2; + --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); -/** search field */ ---search-background-color: white; ---search-foreground-color: #909090; ---search-active-color: black; ---search-filter-background-color: rgba(255,255,255,.7); ---search-filter-backdrop-filter: blur(4px); ---search-filter-foreground-color: black; ---search-filter-border-color: rgba(150,150,150,.4); ---search-filter-highlight-text-color: white; ---search-filter-highlight-bg-color: #3D578C; ---search-results-foreground-color: #425E97; ---search-results-background-color: rgba(255,255,255,.8); ---search-results-backdrop-filter: blur(4px); ---search-results-border-color: rgba(150,150,150,.4); ---search-box-border-color: #B6C4DF; ---search-close-icon-bg-color: #A0A0A0; ---search-close-icon-fg-color: white; + /** search field */ + --search-background-color: white; + --search-foreground-color: #909090; + --search-active-color: black; + --search-filter-background-color: rgba(255, 255, 255, 0.7); + --search-filter-backdrop-filter: blur(4px); + --search-filter-foreground-color: black; + --search-filter-border-color: rgba(150, 150, 150, 0.4); + --search-filter-highlight-text-color: white; + --search-filter-highlight-bg-color: #3d578c; + --search-results-foreground-color: #425e97; + --search-results-background-color: rgba(255, 255, 255, 0.8); + --search-results-backdrop-filter: blur(4px); + --search-results-border-color: rgba(150, 150, 150, 0.4); + --search-box-border-color: #b6c4df; + --search-close-icon-bg-color: #a0a0a0; + --search-close-icon-fg-color: white; -/** code fragments */ ---code-keyword-color: #008000; ---code-type-keyword-color: #604020; ---code-flow-keyword-color: #E08000; ---code-comment-color: #800000; ---code-preprocessor-color: #806020; ---code-string-literal-color: #002080; ---code-char-literal-color: #008080; ---code-xml-cdata-color: black; ---code-vhdl-digit-color: #FF00FF; ---code-vhdl-char-color: #000000; ---code-vhdl-keyword-color: #700070; ---code-vhdl-logic-color: #FF0000; ---code-link-color: #4665A2; ---code-external-link-color: #4665A2; ---fragment-foreground-color: black; ---fragment-background-color: #FBFCFD; ---fragment-border-color: #C4CFE5; ---fragment-lineno-border-color: #00FF00; ---fragment-lineno-background-color: #E8E8E8; ---fragment-lineno-foreground-color: black; ---fragment-lineno-link-fg-color: #4665A2; ---fragment-lineno-link-bg-color: #D8D8D8; ---fragment-lineno-link-hover-fg-color: #4665A2; ---fragment-lineno-link-hover-bg-color: #C8C8C8; ---fragment-copy-ok-color: #2EC82E; ---tooltip-foreground-color: black; ---tooltip-background-color: rgba(255,255,255,0.8); ---tooltip-arrow-background-color: white; ---tooltip-border-color: rgba(150,150,150,0.7); ---tooltip-backdrop-filter: blur(3px); ---tooltip-doc-color: grey; ---tooltip-declaration-color: #006318; ---tooltip-link-color: #4665A2; ---tooltip-shadow: 0 4px 8px 0 rgba(0,0,0,.25); ---fold-line-color: #808080; + /** code fragments */ + --code-keyword-color: #008000; + --code-type-keyword-color: #604020; + --code-flow-keyword-color: #e08000; + --code-comment-color: #800000; + --code-preprocessor-color: #806020; + --code-string-literal-color: #002080; + --code-char-literal-color: #008080; + --code-xml-cdata-color: black; + --code-vhdl-digit-color: #ff00ff; + --code-vhdl-char-color: #000000; + --code-vhdl-keyword-color: #700070; + --code-vhdl-logic-color: #ff0000; + --code-link-color: #4665a2; + --code-external-link-color: #4665a2; + --fragment-foreground-color: black; + --fragment-background-color: #fbfcfd; + --fragment-border-color: #c4cfe5; + --fragment-lineno-border-color: #00ff00; + --fragment-lineno-background-color: #e8e8e8; + --fragment-lineno-foreground-color: black; + --fragment-lineno-link-fg-color: #4665a2; + --fragment-lineno-link-bg-color: #d8d8d8; + --fragment-lineno-link-hover-fg-color: #4665a2; + --fragment-lineno-link-hover-bg-color: #c8c8c8; + --fragment-copy-ok-color: #2ec82e; + --tooltip-foreground-color: black; + --tooltip-background-color: rgba(255, 255, 255, 0.8); + --tooltip-arrow-background-color: white; + --tooltip-border-color: rgba(150, 150, 150, 0.7); + --tooltip-backdrop-filter: blur(3px); + --tooltip-doc-color: grey; + --tooltip-declaration-color: #006318; + --tooltip-link-color: #4665a2; + --tooltip-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.25); + --fold-line-color: #808080; -/** font-family */ ---font-family-normal: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; ---font-family-monospace: 'JetBrains Mono',Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace,fixed; ---font-family-nav: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; ---font-family-title: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; ---font-family-toc: Verdana,'DejaVu Sans',Geneva,sans-serif; ---font-family-search: Arial,Verdana,sans-serif; ---font-family-icon: Arial,Helvetica; ---font-family-tooltip: Roboto,sans-serif; + /** font-family */ + --font-family-normal: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-monospace: + "JetBrains Mono", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace, + fixed; + --font-family-nav: "Lucida Grande", Geneva, Helvetica, Arial, sans-serif; + --font-family-title: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-toc: Verdana, "DejaVu Sans", Geneva, sans-serif; + --font-family-search: Arial, Verdana, sans-serif; + --font-family-icon: Arial, Helvetica; + --font-family-tooltip: Roboto, sans-serif; -/** special sections */ ---warning-color-bg: #f8d1cc; ---warning-color-hl: #b61825; ---warning-color-text: #75070f; ---note-color-bg: #faf3d8; ---note-color-hl: #f3a600; ---note-color-text: #5f4204; ---todo-color-bg: #e4f3ff; ---todo-color-hl: #1879C4; ---todo-color-text: #274a5c; ---test-color-bg: #e8e8ff; ---test-color-hl: #3939C4; ---test-color-text: #1a1a5c; ---deprecated-color-bg: #ecf0f3; ---deprecated-color-hl: #5b6269; ---deprecated-color-text: #43454a; ---bug-color-bg: #e4dafd; ---bug-color-hl: #5b2bdd; ---bug-color-text: #2a0d72; ---invariant-color-bg: #d8f1e3; ---invariant-color-hl: #44b86f; ---invariant-color-text: #265532; + /** special sections */ + --warning-color-bg: #f8d1cc; + --warning-color-hl: #b61825; + --warning-color-text: #75070f; + --note-color-bg: #faf3d8; + --note-color-hl: #f3a600; + --note-color-text: #5f4204; + --todo-color-bg: #e4f3ff; + --todo-color-hl: #1879c4; + --todo-color-text: #274a5c; + --test-color-bg: #e8e8ff; + --test-color-hl: #3939c4; + --test-color-text: #1a1a5c; + --deprecated-color-bg: #ecf0f3; + --deprecated-color-hl: #5b6269; + --deprecated-color-text: #43454a; + --bug-color-bg: #e4dafd; + --bug-color-hl: #5b2bdd; + --bug-color-text: #2a0d72; + --invariant-color-bg: #d8f1e3; + --invariant-color-hl: #44b86f; + --invariant-color-text: #265532; } @media (prefers-color-scheme: dark) { html:not(.dark-mode) { color-scheme: dark; -/* page base colors */ ---page-background-color: black; ---page-foreground-color: #C9D1D9; ---page-link-color: #90A5CE; ---page-visited-link-color: #90A5CE; ---page-external-link-color: #A3B4D7; + /* page base colors */ + --page-background-color: black; + --page-foreground-color: #c9d1d9; + --page-link-color: #90a5ce; + --page-visited-link-color: #90a5ce; + --page-external-link-color: #a3b4d7; -/* index */ ---index-odd-item-bg-color: #0B101A; ---index-even-item-bg-color: black; ---index-header-color: #C4CFE5; ---index-separator-color: #334975; + /* index */ + --index-odd-item-bg-color: #0b101a; + --index-even-item-bg-color: black; + --index-header-color: #c4cfe5; + --index-separator-color: #334975; -/* header */ ---header-background-color: #070B11; ---header-separator-color: #141C2E; ---group-header-separator-color: #1D2A43; ---group-header-color: #90A5CE; ---inherit-header-color: #A0A0A0; + /* header */ + --header-background-color: #070b11; + --header-separator-color: #141c2e; + --group-header-separator-color: #1d2a43; + --group-header-color: #90a5ce; + --inherit-header-color: #a0a0a0; ---footer-foreground-color: #5B7AB7; ---footer-logo-width: 60px; ---citation-label-color: #90A5CE; ---glow-color: cyan; + --footer-foreground-color: #5b7ab7; + --footer-logo-width: 60px; + --citation-label-color: #90a5ce; + --glow-color: cyan; ---title-background-color: #090D16; ---title-separator-color: #212F4B; ---directory-separator-color: #283A5D; ---separator-color: #283A5D; + --title-background-color: #090d16; + --title-separator-color: #212f4b; + --directory-separator-color: #283a5d; + --separator-color: #283a5d; ---blockquote-background-color: #101826; ---blockquote-border-color: #283A5D; + --blockquote-background-color: #101826; + --blockquote-border-color: #283a5d; ---scrollbar-thumb-color: #2C3F65; ---scrollbar-background-color: #070B11; + --scrollbar-thumb-color: #2c3f65; + --scrollbar-background-color: #070b11; ---icon-background-color: #334975; ---icon-foreground-color: #C4CFE5; ---icon-folder-open-fill-color: #4665A2; ---icon-folder-fill-color: #5373B4; ---icon-folder-border-color: #C4CFE5; ---icon-doc-fill-color: #6884BD; ---icon-doc-border-color: #C4CFE5; + --icon-background-color: #334975; + --icon-foreground-color: #c4cfe5; + --icon-folder-open-fill-color: #4665a2; + --icon-folder-fill-color: #5373b4; + --icon-folder-border-color: #c4cfe5; + --icon-doc-fill-color: #6884bd; + --icon-doc-border-color: #c4cfe5; -/* brief member declaration list */ ---memdecl-background-color: #0B101A; ---memdecl-separator-color: #2C3F65; ---memdecl-foreground-color: #BBB; ---memdecl-template-color: #7C95C6; ---memdecl-border-color: #233250; + /* brief member declaration list */ + --memdecl-background-color: #0b101a; + --memdecl-separator-color: #2c3f65; + --memdecl-foreground-color: #bbb; + --memdecl-template-color: #7c95c6; + --memdecl-border-color: #233250; -/* detailed member list */ ---memdef-border-color: #233250; ---memdef-title-background-color: #1B2840; ---memdef-proto-background-color: #19243A; ---memdef-proto-text-color: #9DB0D4; ---memdef-doc-background-color: black; ---memdef-param-name-color: #D28757; ---memdef-template-color: #7C95C6; + /* detailed member list */ + --memdef-border-color: #233250; + --memdef-title-background-color: #1b2840; + --memdef-proto-background-color: #19243a; + --memdef-proto-text-color: #9db0d4; + --memdef-doc-background-color: black; + --memdef-param-name-color: #d28757; + --memdef-template-color: #7c95c6; -/* tables */ ---table-cell-border-color: #283A5D; ---table-header-background-color: #283A5D; ---table-header-foreground-color: #C4CFE5; + /* tables */ + --table-cell-border-color: #283a5d; + --table-header-background-color: #283a5d; + --table-header-foreground-color: #c4cfe5; -/* labels */ ---label-background-color: #354C7B; ---label-left-top-border-color: #4665A2; ---label-right-bottom-border-color: #283A5D; ---label-foreground-color: #CCCCCC; + /* labels */ + --label-background-color: #354c7b; + --label-left-top-border-color: #4665a2; + --label-right-bottom-border-color: #283a5d; + --label-foreground-color: #cccccc; -/** navigation bar/tree/menu */ ---nav-background-color: #101826; ---nav-foreground-color: #364D7C; ---nav-border-color: #212F4B; ---nav-breadcrumb-separator-color: #212F4B; ---nav-breadcrumb-active-bg: #1D2A43; ---nav-breadcrumb-color: #90A5CE; ---nav-breadcrumb-border-color: #2A3D61; ---nav-splitbar-bg-color: #283A5D; ---nav-splitbar-handle-color: #4665A2; ---nav-font-size-level1: 13px; ---nav-font-size-level2: 10px; ---nav-font-size-level3: 9px; ---nav-text-normal-color: #B6C4DF; ---nav-text-hover-color: #DCE2EF; ---nav-text-active-color: #DCE2EF; ---nav-menu-button-color: #B6C4DF; ---nav-menu-background-color: #05070C; ---nav-menu-foreground-color: #BBBBBB; ---nav-menu-active-bg: #1D2A43; ---nav-menu-active-color: #C9D3E7; ---nav-menu-toggle-color: rgba(255, 255, 255, 0.2); ---nav-arrow-color: #4665A2; ---nav-arrow-selected-color: #6884BD; + /** navigation bar/tree/menu */ + --nav-background-color: #101826; + --nav-foreground-color: #364d7c; + --nav-border-color: #212f4b; + --nav-breadcrumb-separator-color: #212f4b; + --nav-breadcrumb-active-bg: #1d2a43; + --nav-breadcrumb-color: #90a5ce; + --nav-breadcrumb-border-color: #2a3d61; + --nav-splitbar-bg-color: #283a5d; + --nav-splitbar-handle-color: #4665a2; + --nav-font-size-level1: 13px; + --nav-font-size-level2: 10px; + --nav-font-size-level3: 9px; + --nav-text-normal-color: #b6c4df; + --nav-text-hover-color: #dce2ef; + --nav-text-active-color: #dce2ef; + --nav-menu-button-color: #b6c4df; + --nav-menu-background-color: #05070c; + --nav-menu-foreground-color: #bbbbbb; + --nav-menu-active-bg: #1d2a43; + --nav-menu-active-color: #c9d3e7; + --nav-menu-toggle-color: rgba(255, 255, 255, 0.2); + --nav-arrow-color: #4665a2; + --nav-arrow-selected-color: #6884bd; -/* sync icon */ ---sync-icon-border-color: #212F4B; ---sync-icon-background-color: #101826; ---sync-icon-selected-background-color: #1D2A43; ---sync-icon-color: #4665A2; ---sync-icon-selected-color: #5373B4; + /* sync icon */ + --sync-icon-border-color: #212f4b; + --sync-icon-background-color: #101826; + --sync-icon-selected-background-color: #1d2a43; + --sync-icon-color: #4665a2; + --sync-icon-selected-color: #5373b4; -/* table of contents */ ---toc-background-color: #151E30; ---toc-border-color: #202E4A; ---toc-header-color: #A3B4D7; ---toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + /* table of contents */ + --toc-background-color: #151e30; + --toc-border-color: #202e4a; + --toc-header-color: #a3b4d7; + --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); -/** search field */ ---search-background-color: black; ---search-foreground-color: #C5C5C5; ---search-active-color: #F5F5F5; ---search-filter-background-color: #101826; ---search-filter-foreground-color: #90A5CE; ---search-filter-backdrop-filter: none; ---search-filter-border-color: #7C95C6; ---search-filter-highlight-text-color: #BCC9E2; ---search-filter-highlight-bg-color: #283A5D; ---search-results-background-color: black; ---search-results-foreground-color: #90A5CE; ---search-results-backdrop-filter: none; ---search-results-border-color: #334975; ---search-box-border-color: #334975; ---search-close-icon-bg-color: #909090; ---search-close-icon-fg-color: black; + /** search field */ + --search-background-color: black; + --search-foreground-color: #c5c5c5; + --search-active-color: #f5f5f5; + --search-filter-background-color: #101826; + --search-filter-foreground-color: #90a5ce; + --search-filter-backdrop-filter: none; + --search-filter-border-color: #7c95c6; + --search-filter-highlight-text-color: #bcc9e2; + --search-filter-highlight-bg-color: #283a5d; + --search-results-background-color: black; + --search-results-foreground-color: #90a5ce; + --search-results-backdrop-filter: none; + --search-results-border-color: #334975; + --search-box-border-color: #334975; + --search-close-icon-bg-color: #909090; + --search-close-icon-fg-color: black; -/** code fragments */ ---code-keyword-color: #CC99CD; ---code-type-keyword-color: #AB99CD; ---code-flow-keyword-color: #E08000; ---code-comment-color: #717790; ---code-preprocessor-color: #65CABE; ---code-string-literal-color: #7EC699; ---code-char-literal-color: #00E0F0; ---code-xml-cdata-color: #C9D1D9; ---code-vhdl-digit-color: #FF00FF; ---code-vhdl-char-color: #C0C0C0; ---code-vhdl-keyword-color: #CF53C9; ---code-vhdl-logic-color: #FF0000; ---code-link-color: #79C0FF; ---code-external-link-color: #79C0FF; ---fragment-foreground-color: #C9D1D9; ---fragment-background-color: #090D16; ---fragment-border-color: #30363D; ---fragment-lineno-border-color: #30363D; ---fragment-lineno-background-color: black; ---fragment-lineno-foreground-color: #6E7681; ---fragment-lineno-link-fg-color: #6E7681; ---fragment-lineno-link-bg-color: #303030; ---fragment-lineno-link-hover-fg-color: #8E96A1; ---fragment-lineno-link-hover-bg-color: #505050; ---fragment-copy-ok-color: #0EA80E; ---tooltip-foreground-color: #C9D1D9; ---tooltip-background-color: #202020; ---tooltip-arrow-background-color: #202020; ---tooltip-backdrop-filter: none; ---tooltip-border-color: #C9D1D9; ---tooltip-doc-color: #D9E1E9; ---tooltip-declaration-color: #20C348; ---tooltip-link-color: #79C0FF; ---tooltip-shadow: none; ---fold-line-color: #808080; + /** code fragments */ + --code-keyword-color: #cc99cd; + --code-type-keyword-color: #ab99cd; + --code-flow-keyword-color: #e08000; + --code-comment-color: #717790; + --code-preprocessor-color: #65cabe; + --code-string-literal-color: #7ec699; + --code-char-literal-color: #00e0f0; + --code-xml-cdata-color: #c9d1d9; + --code-vhdl-digit-color: #ff00ff; + --code-vhdl-char-color: #c0c0c0; + --code-vhdl-keyword-color: #cf53c9; + --code-vhdl-logic-color: #ff0000; + --code-link-color: #79c0ff; + --code-external-link-color: #79c0ff; + --fragment-foreground-color: #c9d1d9; + --fragment-background-color: #090d16; + --fragment-border-color: #30363d; + --fragment-lineno-border-color: #30363d; + --fragment-lineno-background-color: black; + --fragment-lineno-foreground-color: #6e7681; + --fragment-lineno-link-fg-color: #6e7681; + --fragment-lineno-link-bg-color: #303030; + --fragment-lineno-link-hover-fg-color: #8e96a1; + --fragment-lineno-link-hover-bg-color: #505050; + --fragment-copy-ok-color: #0ea80e; + --tooltip-foreground-color: #c9d1d9; + --tooltip-background-color: #202020; + --tooltip-arrow-background-color: #202020; + --tooltip-backdrop-filter: none; + --tooltip-border-color: #c9d1d9; + --tooltip-doc-color: #d9e1e9; + --tooltip-declaration-color: #20c348; + --tooltip-link-color: #79c0ff; + --tooltip-shadow: none; + --fold-line-color: #808080; -/** font-family */ ---font-family-normal: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; ---font-family-monospace: 'JetBrains Mono',Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace,fixed; ---font-family-nav: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; ---font-family-title: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; ---font-family-toc: Verdana,'DejaVu Sans',Geneva,sans-serif; ---font-family-search: Arial,Verdana,sans-serif; ---font-family-icon: Arial,Helvetica; ---font-family-tooltip: Roboto,sans-serif; + /** font-family */ + --font-family-normal: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-monospace: + "JetBrains Mono", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", + monospace, fixed; + --font-family-nav: "Lucida Grande", Geneva, Helvetica, Arial, sans-serif; + --font-family-title: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-toc: Verdana, "DejaVu Sans", Geneva, sans-serif; + --font-family-search: Arial, Verdana, sans-serif; + --font-family-icon: Arial, Helvetica; + --font-family-tooltip: Roboto, sans-serif; -/** special sections */ ---warning-color-bg: #2e1917; ---warning-color-hl: #ad2617; ---warning-color-text: #f5b1aa; ---note-color-bg: #3b2e04; ---note-color-hl: #f1b602; ---note-color-text: #ceb670; ---todo-color-bg: #163750; ---todo-color-hl: #1982D2; ---todo-color-text: #dcf0fa; ---test-color-bg: #121258; ---test-color-hl: #4242cf; ---test-color-text: #c0c0da; ---deprecated-color-bg: #2e323b; ---deprecated-color-hl: #738396; ---deprecated-color-text: #abb0bd; ---bug-color-bg: #2a2536; ---bug-color-hl: #7661b3; ---bug-color-text: #ae9ed6; ---invariant-color-bg: #303a35; ---invariant-color-hl: #76ce96; ---invariant-color-text: #cceed5; -}} + /** special sections */ + --warning-color-bg: #2e1917; + --warning-color-hl: #ad2617; + --warning-color-text: #f5b1aa; + --note-color-bg: #3b2e04; + --note-color-hl: #f1b602; + --note-color-text: #ceb670; + --todo-color-bg: #163750; + --todo-color-hl: #1982d2; + --todo-color-text: #dcf0fa; + --test-color-bg: #121258; + --test-color-hl: #4242cf; + --test-color-text: #c0c0da; + --deprecated-color-bg: #2e323b; + --deprecated-color-hl: #738396; + --deprecated-color-text: #abb0bd; + --bug-color-bg: #2a2536; + --bug-color-hl: #7661b3; + --bug-color-text: #ae9ed6; + --invariant-color-bg: #303a35; + --invariant-color-hl: #76ce96; + --invariant-color-text: #cceed5; + } +} body { - background-color: var(--page-background-color); - color: var(--page-foreground-color); + background-color: var(--page-background-color); + color: var(--page-foreground-color); } -body, table, div, p, dl { - font-weight: 400; - font-size: 14px; - font-family: var(--font-family-normal); - line-height: 22px; +body, +table, +div, +p, +dl { + font-weight: 400; + font-size: 14px; + font-family: var(--font-family-normal); + line-height: 22px; } body.resizing { - user-select: none; - -webkit-user-select: none; + user-select: none; + -webkit-user-select: none; } #doc-content { - scrollbar-width: thin; + scrollbar-width: thin; } /* @group Heading Levels */ .title { - font-family: var(--font-family-normal); - line-height: 28px; - font-size: 160%; - font-weight: 400; - margin: 10px 2px; + font-family: var(--font-family-normal); + line-height: 28px; + font-size: 160%; + font-weight: 400; + margin: 10px 2px; } h1.groupheader { - font-size: 150%; + font-size: 150%; } h2.groupheader { - box-shadow: 12px 0 var(--page-background-color), - -12px 0 var(--page-background-color), - 12px 1px var(--group-header-separator-color), - -12px 1px var(--group-header-separator-color); - color: var(--group-header-color); - font-size: 150%; - font-weight: normal; - margin-top: 1.75em; - padding-top: 8px; - padding-bottom: 4px; - width: 100%; + box-shadow: + 12px 0 var(--page-background-color), + -12px 0 var(--page-background-color), + 12px 1px var(--group-header-separator-color), + -12px 1px var(--group-header-separator-color); + color: var(--group-header-color); + font-size: 150%; + font-weight: normal; + margin-top: 1.75em; + padding-top: 8px; + padding-bottom: 4px; + width: 100%; } td h2.groupheader { - box-shadow: 13px 0 var(--page-background-color), - -13px 0 var(--page-background-color), - 13px 1px var(--group-header-separator-color), - -13px 1px var(--group-header-separator-color); + box-shadow: + 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); } h3.groupheader { - font-size: 100%; + font-size: 100%; } -h1, h2, h3, h4, h5, h6 { - -webkit-transition: text-shadow 0.5s linear; - -moz-transition: text-shadow 0.5s linear; - -ms-transition: text-shadow 0.5s linear; - -o-transition: text-shadow 0.5s linear; - transition: text-shadow 0.5s linear; - margin-right: 15px; +h1, +h2, +h3, +h4, +h5, +h6 { + -webkit-transition: text-shadow 0.5s linear; + -moz-transition: text-shadow 0.5s linear; + -ms-transition: text-shadow 0.5s linear; + -o-transition: text-shadow 0.5s linear; + transition: text-shadow 0.5s linear; + margin-right: 15px; } -h1.glow, h2.glow, h3.glow, h4.glow, h5.glow, h6.glow { - text-shadow: 0 0 15px var(--glow-color); +h1.glow, +h2.glow, +h3.glow, +h4.glow, +h5.glow, +h6.glow { + text-shadow: 0 0 15px var(--glow-color); } dt { - font-weight: bold; + font-weight: bold; } -p.startli, p.startdd { - margin-top: 2px; +p.startli, +p.startdd { + margin-top: 2px; } -th p.starttd, th p.intertd, th p.endtd { - font-size: 100%; - font-weight: 700; +th p.starttd, +th p.intertd, +th p.endtd { + font-size: 100%; + font-weight: 700; } p.starttd { - margin-top: 0px; + margin-top: 0px; } p.endli { - margin-bottom: 0px; + margin-bottom: 0px; } p.enddd { - margin-bottom: 4px; + margin-bottom: 4px; } p.endtd { - margin-bottom: 2px; + margin-bottom: 2px; } p.interli { @@ -518,705 +554,785 @@ p.intertd { /* @end */ caption { - font-weight: bold; + font-weight: bold; } span.legend { - font-size: 70%; - text-align: center; + font-size: 70%; + text-align: center; } h3.version { - font-size: 90%; - text-align: center; + font-size: 90%; + text-align: center; } div.navtab { - margin-right: 6px; - padding-right: 6px; - text-align: right; - line-height: 110%; - background-color: var(--nav-background-color); + margin-right: 6px; + padding-right: 6px; + text-align: right; + line-height: 110%; + background-color: var(--nav-background-color); } div.navtab table { - border-spacing: 0; + border-spacing: 0; } td.navtab { - padding-right: 6px; - padding-left: 6px; + padding-right: 6px; + padding-left: 6px; } td.navtabHL { - padding-right: 6px; - padding-left: 6px; - border-radius: 0 6px 6px 0; - background-color: var(--nav-menu-active-bg); + padding-right: 6px; + padding-left: 6px; + border-radius: 0 6px 6px 0; + background-color: var(--nav-menu-active-bg); } -div.qindex{ - text-align: center; - width: 100%; - line-height: 140%; - font-size: 130%; - color: var(--index-separator-color); +div.qindex { + text-align: center; + width: 100%; + line-height: 140%; + font-size: 130%; + color: var(--index-separator-color); } #main-menu a:focus { - outline: auto; - z-index: 10; - position: relative; + outline: auto; + z-index: 10; + position: relative; } -dt.alphachar{ - font-size: 180%; - font-weight: bold; +dt.alphachar { + font-size: 180%; + font-weight: bold; } -.alphachar a{ - color: var(--index-header-color); +.alphachar a { + color: var(--index-header-color); } -.alphachar a:hover, .alphachar a:visited{ - text-decoration: none; +.alphachar a:hover, +.alphachar a:visited { + text-decoration: none; } .classindex dl { - padding: 25px; - column-count:1 + padding: 25px; + column-count: 1; } .classindex dd { - display:inline-block; - margin-left: 50px; - width: 90%; - line-height: 1.15em; + display: inline-block; + margin-left: 50px; + width: 90%; + line-height: 1.15em; } .classindex dl.even { - background-color: var(--index-even-item-bg-color); + background-color: var(--index-even-item-bg-color); } .classindex dl.odd { - background-color: var(--index-odd-item-bg-color); + background-color: var(--index-odd-item-bg-color); } -@media(min-width: 1120px) { - .classindex dl { - column-count:2 - } +@media (min-width: 1120px) { + .classindex dl { + column-count: 2; + } } -@media(min-width: 1320px) { - .classindex dl { - column-count:3 - } +@media (min-width: 1320px) { + .classindex dl { + column-count: 3; + } } - /* @group Link Styling */ a { - color: var(--page-link-color); - font-weight: normal; - text-decoration: none; + color: var(--page-link-color); + font-weight: normal; + text-decoration: none; } .contents a:visited { - color: var(--page-visited-link-color); + color: var(--page-visited-link-color); } span.label a:hover { - text-decoration: none; - background: linear-gradient(to bottom, transparent 0,transparent calc(100% - 1px), currentColor 100%); + text-decoration: none; + background: linear-gradient( + to bottom, + transparent 0, + transparent calc(100% - 1px), + currentColor 100% + ); } a.el { - font-weight: bold; + font-weight: bold; } a.elRef { } -a.el, a.el:visited, a.code, a.code:visited, a.line, a.line:visited { - color: var(--page-link-color); +a.el, +a.el:visited, +a.code, +a.code:visited, +a.line, +a.line:visited { + color: var(--page-link-color); } -a.codeRef, a.codeRef:visited, a.lineRef, a.lineRef:visited { - color: var(--page-external-link-color); +a.codeRef, +a.codeRef:visited, +a.lineRef, +a.lineRef:visited { + color: var(--page-external-link-color); } -a.code.hl_class { /* style for links to class names in code snippets */ } -a.code.hl_struct { /* style for links to struct names in code snippets */ } -a.code.hl_union { /* style for links to union names in code snippets */ } -a.code.hl_interface { /* style for links to interface names in code snippets */ } -a.code.hl_protocol { /* style for links to protocol names in code snippets */ } -a.code.hl_category { /* style for links to category names in code snippets */ } -a.code.hl_exception { /* style for links to exception names in code snippets */ } -a.code.hl_service { /* style for links to service names in code snippets */ } -a.code.hl_singleton { /* style for links to singleton names in code snippets */ } -a.code.hl_concept { /* style for links to concept names in code snippets */ } -a.code.hl_namespace { /* style for links to namespace names in code snippets */ } -a.code.hl_package { /* style for links to package names in code snippets */ } -a.code.hl_define { /* style for links to macro names in code snippets */ } -a.code.hl_function { /* style for links to function names in code snippets */ } -a.code.hl_variable { /* style for links to variable names in code snippets */ } -a.code.hl_typedef { /* style for links to typedef names in code snippets */ } -a.code.hl_enumvalue { /* style for links to enum value names in code snippets */ } -a.code.hl_enumeration { /* style for links to enumeration names in code snippets */ } -a.code.hl_signal { /* style for links to Qt signal names in code snippets */ } -a.code.hl_slot { /* style for links to Qt slot names in code snippets */ } -a.code.hl_friend { /* style for links to friend names in code snippets */ } -a.code.hl_dcop { /* style for links to KDE3 DCOP names in code snippets */ } -a.code.hl_property { /* style for links to property names in code snippets */ } -a.code.hl_event { /* style for links to event names in code snippets */ } -a.code.hl_sequence { /* style for links to sequence names in code snippets */ } -a.code.hl_dictionary { /* style for links to dictionary names in code snippets */ } +a.code.hl_class { + /* style for links to class names in code snippets */ +} +a.code.hl_struct { + /* style for links to struct names in code snippets */ +} +a.code.hl_union { + /* style for links to union names in code snippets */ +} +a.code.hl_interface { + /* style for links to interface names in code snippets */ +} +a.code.hl_protocol { + /* style for links to protocol names in code snippets */ +} +a.code.hl_category { + /* style for links to category names in code snippets */ +} +a.code.hl_exception { + /* style for links to exception names in code snippets */ +} +a.code.hl_service { + /* style for links to service names in code snippets */ +} +a.code.hl_singleton { + /* style for links to singleton names in code snippets */ +} +a.code.hl_concept { + /* style for links to concept names in code snippets */ +} +a.code.hl_namespace { + /* style for links to namespace names in code snippets */ +} +a.code.hl_package { + /* style for links to package names in code snippets */ +} +a.code.hl_define { + /* style for links to macro names in code snippets */ +} +a.code.hl_function { + /* style for links to function names in code snippets */ +} +a.code.hl_variable { + /* style for links to variable names in code snippets */ +} +a.code.hl_typedef { + /* style for links to typedef names in code snippets */ +} +a.code.hl_enumvalue { + /* style for links to enum value names in code snippets */ +} +a.code.hl_enumeration { + /* style for links to enumeration names in code snippets */ +} +a.code.hl_signal { + /* style for links to Qt signal names in code snippets */ +} +a.code.hl_slot { + /* style for links to Qt slot names in code snippets */ +} +a.code.hl_friend { + /* style for links to friend names in code snippets */ +} +a.code.hl_dcop { + /* style for links to KDE3 DCOP names in code snippets */ +} +a.code.hl_property { + /* style for links to property names in code snippets */ +} +a.code.hl_event { + /* style for links to event names in code snippets */ +} +a.code.hl_sequence { + /* style for links to sequence names in code snippets */ +} +a.code.hl_dictionary { + /* style for links to dictionary names in code snippets */ +} /* @end */ dl.el { - margin-left: -1cm; + margin-left: -1cm; } ul.check { - list-style:none; - text-indent: -16px; - padding-left: 38px; + list-style: none; + text-indent: -16px; + padding-left: 38px; } li.unchecked:before { - content: "\2610\A0"; + content: "\2610\A0"; } li.checked:before { - content: "\2611\A0"; + content: "\2611\A0"; } ol { - text-indent: 0px; + text-indent: 0px; } ul { - text-indent: 0px; - overflow: visible; + text-indent: 0px; + overflow: visible; } ul.multicol { - -moz-column-gap: 1em; - -webkit-column-gap: 1em; - column-gap: 1em; - -moz-column-count: 3; - -webkit-column-count: 3; - column-count: 3; - list-style-type: none; + -moz-column-gap: 1em; + -webkit-column-gap: 1em; + column-gap: 1em; + -moz-column-count: 3; + -webkit-column-count: 3; + column-count: 3; + list-style-type: none; } #side-nav ul { - overflow: visible; /* reset ul rule for scroll bar in GENERATE_TREEVIEW window */ + overflow: visible; /* reset ul rule for scroll bar in GENERATE_TREEVIEW window */ } #main-nav ul { - overflow: visible; /* reset ul rule for the navigation bar drop down lists */ + overflow: visible; /* reset ul rule for the navigation bar drop down lists */ } .fragment { - text-align: left; - direction: ltr; - overflow-x: auto; - overflow-y: hidden; - position: relative; - min-height: 12px; - margin: 10px 0px; - padding: 10px 10px; - border: 1px solid var(--fragment-border-color); - border-radius: 4px; - background-color: var(--fragment-background-color); - color: var(--fragment-foreground-color); + text-align: left; + direction: ltr; + overflow-x: auto; + overflow-y: hidden; + position: relative; + min-height: 12px; + margin: 10px 0px; + padding: 10px 10px; + border: 1px solid var(--fragment-border-color); + border-radius: 4px; + background-color: var(--fragment-background-color); + color: var(--fragment-foreground-color); } pre.fragment { - word-wrap: break-word; - font-size: 10pt; - line-height: 125%; - font-family: var(--font-family-monospace); + word-wrap: break-word; + font-size: 10pt; + line-height: 125%; + font-family: var(--font-family-monospace); } span.tt { - white-space: pre; - font-family: var(--font-family-monospace); + white-space: pre; + font-family: var(--font-family-monospace); } .clipboard { - width: 24px; - height: 24px; - right: 5px; - top: 5px; - opacity: 0; - position: absolute; - display: inline; - overflow: hidden; - justify-content: center; - align-items: center; - cursor: pointer; + width: 24px; + height: 24px; + right: 5px; + top: 5px; + opacity: 0; + position: absolute; + display: inline; + overflow: hidden; + justify-content: center; + align-items: center; + cursor: pointer; } .clipboard.success { - border: 1px solid var(--fragment-foreground-color); - border-radius: 4px; + border: 1px solid var(--fragment-foreground-color); + border-radius: 4px; } -.fragment:hover .clipboard, .clipboard.success { - opacity: .4; +.fragment:hover .clipboard, +.clipboard.success { + opacity: 0.4; } -.clipboard:hover, .clipboard.success { - opacity: 1 !important; +.clipboard:hover, +.clipboard.success { + opacity: 1 !important; } -.clipboard:active:not([class~=success]) svg { - transform: scale(.91); +.clipboard:active:not([class~="success"]) svg { + transform: scale(0.91); } .clipboard.success svg { - fill: var(--fragment-copy-ok-color); + fill: var(--fragment-copy-ok-color); } .clipboard.success { - border-color: var(--fragment-copy-ok-color); + border-color: var(--fragment-copy-ok-color); } div.line { - font-family: var(--font-family-monospace); - font-size: 13px; - min-height: 13px; - line-height: 1.2; - text-wrap: wrap; - word-break: break-all; - white-space: -moz-pre-wrap; /* Moz */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - white-space: pre-wrap; /* CSS3 */ - word-wrap: break-word; /* IE 5.5+ */ - text-indent: -62px; - padding-left: 62px; - padding-bottom: 0px; - margin: 0px; - -webkit-transition-property: background-color, box-shadow; - -webkit-transition-duration: 0.5s; - -moz-transition-property: background-color, box-shadow; - -moz-transition-duration: 0.5s; - -ms-transition-property: background-color, box-shadow; - -ms-transition-duration: 0.5s; - -o-transition-property: background-color, box-shadow; - -o-transition-duration: 0.5s; - transition-property: background-color, box-shadow; - transition-duration: 0.5s; + font-family: var(--font-family-monospace); + font-size: 13px; + min-height: 13px; + line-height: 1.2; + text-wrap: wrap; + word-break: break-all; + white-space: -moz-pre-wrap; /* Moz */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ + text-indent: -62px; + padding-left: 62px; + padding-bottom: 0px; + margin: 0px; + -webkit-transition-property: background-color, box-shadow; + -webkit-transition-duration: 0.5s; + -moz-transition-property: background-color, box-shadow; + -moz-transition-duration: 0.5s; + -ms-transition-property: background-color, box-shadow; + -ms-transition-duration: 0.5s; + -o-transition-property: background-color, box-shadow; + -o-transition-duration: 0.5s; + transition-property: background-color, box-shadow; + transition-duration: 0.5s; } div.line:after { - content:"\000A"; - white-space: pre; + content: "\000A"; + white-space: pre; } div.line.glow { - background-color: var(--glow-color); - box-shadow: 0 0 10px var(--glow-color); + background-color: var(--glow-color); + box-shadow: 0 0 10px var(--glow-color); } span.fold { - display: inline-block; - width: 12px; - height: 12px; - margin-left: 4px; - margin-right: 1px; + display: inline-block; + width: 12px; + height: 12px; + margin-left: 4px; + margin-right: 1px; } span.foldnone { - display: inline-block; - position: relative; - cursor: pointer; - user-select: none; + display: inline-block; + position: relative; + cursor: pointer; + user-select: none; } -span.fold.plus, span.fold.minus { - width: 10px; - height: 10px; - background-color: var(--fragment-background-color); - position: relative; - border: 1px solid var(--fold-line-color); - margin-right: 1px; +span.fold.plus, +span.fold.minus { + width: 10px; + height: 10px; + background-color: var(--fragment-background-color); + position: relative; + border: 1px solid var(--fold-line-color); + margin-right: 1px; } -span.fold.plus::before, span.fold.minus::before { - content: ''; - position: absolute; - background-color: var(--fold-line-color); +span.fold.plus::before, +span.fold.minus::before { + content: ""; + position: absolute; + background-color: var(--fold-line-color); } span.fold.plus::before { - width: 2px; - height: 6px; - top: 2px; - left: 4px; + width: 2px; + height: 6px; + top: 2px; + left: 4px; } span.fold.plus::after { - content: ''; - position: absolute; - width: 6px; - height: 2px; - top: 4px; - left: 2px; - background-color: var(--fold-line-color); + content: ""; + position: absolute; + width: 6px; + height: 2px; + top: 4px; + left: 2px; + background-color: var(--fold-line-color); } span.fold.minus::before { - width: 6px; - height: 2px; - top: 4px; - left: 2px; + width: 6px; + height: 2px; + top: 4px; + left: 2px; } span.lineno { - padding-right: 4px; - margin-right: 9px; - text-align: right; - border-right: 2px solid var(--fragment-lineno-border-color); - color: var(--fragment-lineno-foreground-color); - background-color: var(--fragment-lineno-background-color); - white-space: pre; + padding-right: 4px; + margin-right: 9px; + text-align: right; + border-right: 2px solid var(--fragment-lineno-border-color); + color: var(--fragment-lineno-foreground-color); + background-color: var(--fragment-lineno-background-color); + white-space: pre; } -span.lineno a, span.lineno a:visited { - color: var(--fragment-lineno-link-fg-color); - background-color: var(--fragment-lineno-link-bg-color); +span.lineno a, +span.lineno a:visited { + color: var(--fragment-lineno-link-fg-color); + background-color: var(--fragment-lineno-link-bg-color); } span.lineno a:hover { - color: var(--fragment-lineno-link-hover-fg-color); - background-color: var(--fragment-lineno-link-hover-bg-color); + color: var(--fragment-lineno-link-hover-fg-color); + background-color: var(--fragment-lineno-link-hover-bg-color); } .lineno { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } div.classindex ul { - list-style: none; - padding-left: 0; + list-style: none; + padding-left: 0; } div.classindex span.ai { - display: inline-block; + display: inline-block; } div.groupHeader { - box-shadow: 13px 0 var(--page-background-color), - -13px 0 var(--page-background-color), - 13px 1px var(--group-header-separator-color), - -13px 1px var(--group-header-separator-color); - color: var(--group-header-color); - font-size: 110%; - font-weight: 500; - margin-left: 0px; - margin-top: 0em; - margin-bottom: 6px; - padding-top: 8px; - padding-bottom: 4px; + box-shadow: + 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); + color: var(--group-header-color); + font-size: 110%; + font-weight: 500; + margin-left: 0px; + margin-top: 0em; + margin-bottom: 6px; + padding-top: 8px; + padding-bottom: 4px; } div.groupText { - margin-left: 16px; - font-style: italic; + margin-left: 16px; + font-style: italic; } body { - color: var(--page-foreground-color); - margin: 0; + color: var(--page-foreground-color); + margin: 0; } div.contents { - margin-top: 10px; - margin-left: 12px; - margin-right: 12px; + margin-top: 10px; + margin-left: 12px; + margin-right: 12px; } p.formulaDsp { - text-align: center; + text-align: center; } img.dark-mode-visible { - display: none; + display: none; } img.light-mode-visible { - display: none; + display: none; } -img.formulaInl, img.inline { - vertical-align: middle; +img.formulaInl, +img.inline { + vertical-align: middle; } div.center { - text-align: center; - margin-top: 0px; - margin-bottom: 0px; - padding: 0px; + text-align: center; + margin-top: 0px; + margin-bottom: 0px; + padding: 0px; } div.center img { - border: 0px; + border: 0px; } address.footer { - text-align: right; - padding-right: 12px; + text-align: right; + padding-right: 12px; } img.footer { - border: 0px; - vertical-align: middle; - width: var(--footer-logo-width); + border: 0px; + vertical-align: middle; + width: var(--footer-logo-width); } .compoundTemplParams { - color: var(--memdecl-template-color); - font-size: 80%; - line-height: 120%; + color: var(--memdecl-template-color); + font-size: 80%; + line-height: 120%; } /* @group Code Colorization */ span.keyword { - color: var(--code-keyword-color); + color: var(--code-keyword-color); } span.keywordtype { - color: var(--code-type-keyword-color); + color: var(--code-type-keyword-color); } span.keywordflow { - color: var(--code-flow-keyword-color); + color: var(--code-flow-keyword-color); } span.comment { - color: var(--code-comment-color); + color: var(--code-comment-color); } span.preprocessor { - color: var(--code-preprocessor-color); + color: var(--code-preprocessor-color); } span.stringliteral { - color: var(--code-string-literal-color); + color: var(--code-string-literal-color); } span.charliteral { - color: var(--code-char-literal-color); + color: var(--code-char-literal-color); } span.xmlcdata { - color: var(--code-xml-cdata-color); + color: var(--code-xml-cdata-color); } -span.vhdldigit { - color: var(--code-vhdl-digit-color); +span.vhdldigit { + color: var(--code-vhdl-digit-color); } -span.vhdlchar { - color: var(--code-vhdl-char-color); +span.vhdlchar { + color: var(--code-vhdl-char-color); } -span.vhdlkeyword { - color: var(--code-vhdl-keyword-color); +span.vhdlkeyword { + color: var(--code-vhdl-keyword-color); } -span.vhdllogic { - color: var(--code-vhdl-logic-color); +span.vhdllogic { + color: var(--code-vhdl-logic-color); } blockquote { - background-color: var(--blockquote-background-color); - border-left: 2px solid var(--blockquote-border-color); - margin: 0 24px 0 4px; - padding: 0 12px 0 16px; + background-color: var(--blockquote-background-color); + border-left: 2px solid var(--blockquote-border-color); + margin: 0 24px 0 4px; + padding: 0 12px 0 16px; } /* @end */ td.tiny { - font-size: 75%; + font-size: 75%; } .dirtab { - padding: 4px; - border-collapse: collapse; - border: 1px solid var(--table-cell-border-color); + padding: 4px; + border-collapse: collapse; + border: 1px solid var(--table-cell-border-color); } th.dirtab { - background-color: var(--table-header-background-color); - color: var(--table-header-foreground-color); - font-weight: bold; + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-weight: bold; } hr { - border: none; - margin-top: 16px; - margin-bottom: 16px; - height: 1px; - box-shadow: 13px 0 var(--page-background-color), - -13px 0 var(--page-background-color), - 13px 1px var(--group-header-separator-color), - -13px 1px var(--group-header-separator-color); + border: none; + margin-top: 16px; + margin-bottom: 16px; + height: 1px; + box-shadow: + 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); } hr.footer { - height: 1px; + height: 1px; } /* @group Member Descriptions */ table.memberdecls { - border-spacing: 0px; - padding: 0px; + border-spacing: 0px; + padding: 0px; } -.memberdecls td, .fieldtable tr { - transition-property: background-color, box-shadow; - transition-duration: 0.5s; +.memberdecls td, +.fieldtable tr { + transition-property: background-color, box-shadow; + transition-duration: 0.5s; } -.memberdecls td.glow, .fieldtable tr.glow { - background-color: var(--glow-color); - box-shadow: 0 0 15px var(--glow-color); +.memberdecls td.glow, +.fieldtable tr.glow { + background-color: var(--glow-color); + box-shadow: 0 0 15px var(--glow-color); } -.memberdecls tr[class^='memitem'] { - font-family: var(--font-family-monospace); +.memberdecls tr[class^="memitem"] { + font-family: var(--font-family-monospace); } -.mdescLeft, .mdescRight, -.memItemLeft, .memItemRight { - padding-top: 2px; - padding-bottom: 2px; +.mdescLeft, +.mdescRight, +.memItemLeft, +.memItemRight { + padding-top: 2px; + padding-bottom: 2px; } .memTemplParams { - padding-left: 10px; - padding-top: 5px; + padding-left: 10px; + padding-top: 5px; } -.memItemLeft, .memItemRight, .memTemplParams { - background-color: var(--memdecl-background-color); +.memItemLeft, +.memItemRight, +.memTemplParams { + background-color: var(--memdecl-background-color); } -.mdescLeft, .mdescRight { - padding: 0px 8px 4px 8px; - color: var(--memdecl-foreground-color); +.mdescLeft, +.mdescRight { + padding: 0px 8px 4px 8px; + color: var(--memdecl-foreground-color); } -tr[class^='memdesc'] { - box-shadow: inset 0px 1px 3px 0px rgba(0,0,0,.075); +tr[class^="memdesc"] { + box-shadow: inset 0px 1px 3px 0px rgba(0, 0, 0, 0.075); } .mdescLeft { - border-left: 1px solid var(--memdecl-border-color); - border-bottom: 1px solid var(--memdecl-border-color); + border-left: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); } .mdescRight { - border-right: 1px solid var(--memdecl-border-color); - border-bottom: 1px solid var(--memdecl-border-color); + border-right: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); } .memTemplParams { - color: var(--memdecl-template-color); - white-space: nowrap; - font-size: 80%; - border-left: 1px solid var(--memdecl-border-color); - border-right: 1px solid var(--memdecl-border-color); + color: var(--memdecl-template-color); + white-space: nowrap; + font-size: 80%; + border-left: 1px solid var(--memdecl-border-color); + border-right: 1px solid var(--memdecl-border-color); } td.ititle { - border: 1px solid var(--memdecl-border-color); - border-top-left-radius: 4px; - border-top-right-radius: 4px; - padding-left: 10px; + border: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding-left: 10px; } tr:not(:first-child) > td.ititle { - border-top: 0; - border-radius: 0; + border-top: 0; + border-radius: 0; } .memItemLeft { - white-space: nowrap; - border-left: 1px solid var(--memdecl-border-color); - border-bottom: 1px solid var(--memdecl-border-color); - padding-left: 10px; - transition: none; + white-space: nowrap; + border-left: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); + padding-left: 10px; + transition: none; } .memItemRight { - width: 100%; - border-right: 1px solid var(--memdecl-border-color); - border-bottom: 1px solid var(--memdecl-border-color); - padding-right: 10px; - transition: none; + width: 100%; + border-right: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); + padding-right: 10px; + transition: none; } -tr.heading + tr[class^='memitem'] td.memItemLeft, -tr.groupHeader + tr[class^='memitem'] td.memItemLeft, -tr.inherit_header + tr[class^='memitem'] td.memItemLeft { - border-top: 1px solid var(--memdecl-border-color); - border-top-left-radius: 4px; +tr.heading + tr[class^="memitem"] td.memItemLeft, +tr.groupHeader + tr[class^="memitem"] td.memItemLeft, +tr.inherit_header + tr[class^="memitem"] td.memItemLeft { + border-top: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; } -tr.heading + tr[class^='memitem'] td.memItemRight, -tr.groupHeader + tr[class^='memitem'] td.memItemRight, -tr.inherit_header + tr[class^='memitem'] td.memItemRight { - border-top: 1px solid var(--memdecl-border-color); - border-top-right-radius: 4px; +tr.heading + tr[class^="memitem"] td.memItemRight, +tr.groupHeader + tr[class^="memitem"] td.memItemRight, +tr.inherit_header + tr[class^="memitem"] td.memItemRight { + border-top: 1px solid var(--memdecl-border-color); + border-top-right-radius: 4px; } -tr.heading + tr[class^='memitem'] td.memTemplParams, -tr.heading + tr td.ititle, -tr.groupHeader + tr[class^='memitem'] td.memTemplParams, -tr.groupHeader + tr td.ititle, -tr.inherit_header + tr[class^='memitem'] td.memTemplParams { - border-top: 1px solid var(--memdecl-border-color); - border-top-left-radius: 4px; - border-top-right-radius: 4px; +tr.heading + tr[class^="memitem"] td.memTemplParams, +tr.heading + tr td.ititle, +tr.groupHeader + tr[class^="memitem"] td.memTemplParams, +tr.groupHeader + tr td.ititle, +tr.inherit_header + tr[class^="memitem"] td.memTemplParams { + border-top: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; + border-top-right-radius: 4px; } table.memberdecls tr:last-child td.memItemLeft, table.memberdecls tr:last-child td.mdescLeft, -table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemLeft, -table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemLeft, -table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescLeft, -table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescLeft { - border-bottom-left-radius: 4px; +table.memberdecls tr[class^="memitem"]:has(+ tr.groupHeader) td.memItemLeft, +table.memberdecls tr[class^="memitem"]:has(+ tr.inherit_header) td.memItemLeft, +table.memberdecls tr[class^="memdesc"]:has(+ tr.groupHeader) td.mdescLeft, +table.memberdecls tr[class^="memdesc"]:has(+ tr.inherit_header) td.mdescLeft { + border-bottom-left-radius: 4px; } table.memberdecls tr:last-child td.memItemRight, table.memberdecls tr:last-child td.mdescRight, -table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemRight, -table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemRight, -table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescRight, -table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescRight { - border-bottom-right-radius: 4px; +table.memberdecls tr[class^="memitem"]:has(+ tr.groupHeader) td.memItemRight, +table.memberdecls tr[class^="memitem"]:has(+ tr.inherit_header) td.memItemRight, +table.memberdecls tr[class^="memdesc"]:has(+ tr.groupHeader) td.mdescRight, +table.memberdecls tr[class^="memdesc"]:has(+ tr.inherit_header) td.mdescRight { + border-bottom-right-radius: 4px; } -tr.template .memItemLeft, tr.template .memItemRight { - border-top: none; - padding-top: 0; +tr.template .memItemLeft, +tr.template .memItemRight { + border-top: none; + padding-top: 0; } - /* @end */ /* @group Member Details */ @@ -1224,1238 +1340,1320 @@ tr.template .memItemLeft, tr.template .memItemRight { /* Styles for detailed member documentation */ .memtitle { - padding: 8px; - border-top: 1px solid var(--memdef-border-color); - border-left: 1px solid var(--memdef-border-color); - border-right: 1px solid var(--memdef-border-color); - border-top-right-radius: 4px; - border-top-left-radius: 4px; - margin-bottom: -1px; - background-color: var(--memdef-proto-background-color); - line-height: 1.25; - font-family: var(--font-family-monospace); - font-weight: 500; - font-size: 16px; - float:left; - box-shadow: 0 10px 0 -1px var(--memdef-proto-background-color), - 0 2px 8px 0 rgba(0,0,0,.075); - position: relative; + padding: 8px; + border-top: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + border-top-right-radius: 4px; + border-top-left-radius: 4px; + margin-bottom: -1px; + background-color: var(--memdef-proto-background-color); + line-height: 1.25; + font-family: var(--font-family-monospace); + font-weight: 500; + font-size: 16px; + float: left; + box-shadow: + 0 10px 0 -1px var(--memdef-proto-background-color), + 0 2px 8px 0 rgba(0, 0, 0, 0.075); + position: relative; } .memtitle:after { - content: ''; - display: block; - background: var(--memdef-proto-background-color); - height: 10px; - bottom: -10px; - left: 0px; - right: -14px; - position: absolute; - border-top-right-radius: 6px; + content: ""; + display: block; + background: var(--memdef-proto-background-color); + height: 10px; + bottom: -10px; + left: 0px; + right: -14px; + position: absolute; + border-top-right-radius: 6px; } -.permalink -{ - font-family: var(--font-family-monospace); - font-weight: 500; - line-height: 1.25; - font-size: 16px; - display: inline-block; - vertical-align: middle; +.permalink { + font-family: var(--font-family-monospace); + font-weight: 500; + line-height: 1.25; + font-size: 16px; + display: inline-block; + vertical-align: middle; } .memtemplate { - font-size: 80%; - color: var(--memdef-template-color); - font-family: var(--font-family-monospace); - font-weight: normal; - margin-left: 9px; + font-size: 80%; + color: var(--memdef-template-color); + font-family: var(--font-family-monospace); + font-weight: normal; + margin-left: 9px; } .mempage { - width: 100%; + width: 100%; } .memitem { - padding: 0; - margin-bottom: 10px; - margin-right: 5px; - display: table !important; - width: 100%; - box-shadow: 0 2px 8px 0 rgba(0,0,0,.075); - border-radius: 4px; + padding: 0; + margin-bottom: 10px; + margin-right: 5px; + display: table !important; + width: 100%; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.075); + border-radius: 4px; } .memitem.glow { - box-shadow: 0 0 15px var(--glow-color); + box-shadow: 0 0 15px var(--glow-color); } .memname { - font-family: var(--font-family-monospace); - font-size: 13px; - font-weight: 400; - margin-left: 6px; + font-family: var(--font-family-monospace); + font-size: 13px; + font-weight: 400; + margin-left: 6px; } .memname td { - vertical-align: bottom; + vertical-align: bottom; } -.memproto, dl.reflist dt { - border-top: 1px solid var(--memdef-border-color); - border-left: 1px solid var(--memdef-border-color); - border-right: 1px solid var(--memdef-border-color); - padding: 6px 0px 6px 0px; - color: var(--memdef-proto-text-color); - font-weight: bold; - background-color: var(--memdef-proto-background-color); - border-top-right-radius: 4px; - border-bottom: 1px solid var(--memdef-border-color); +.memproto, +dl.reflist dt { + border-top: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + padding: 6px 0px 6px 0px; + color: var(--memdef-proto-text-color); + font-weight: bold; + background-color: var(--memdef-proto-background-color); + border-top-right-radius: 4px; + border-bottom: 1px solid var(--memdef-border-color); } .overload { - font-family: var(--font-family-monospace); - font-size: 65%; + font-family: var(--font-family-monospace); + font-size: 65%; } -.memdoc, dl.reflist dd { - border-bottom: 1px solid var(--memdef-border-color); - border-left: 1px solid var(--memdef-border-color); - border-right: 1px solid var(--memdef-border-color); - padding: 6px 10px 2px 10px; - border-top-width: 0; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; +.memdoc, +dl.reflist dd { + border-bottom: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + padding: 6px 10px 2px 10px; + border-top-width: 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; } dl.reflist dt { - padding: 5px; + padding: 5px; } dl.reflist dd { - margin: 0px 0px 10px 0px; - padding: 5px; + margin: 0px 0px 10px 0px; + padding: 5px; } .paramkey { - text-align: right; + text-align: right; } .paramtype { - white-space: nowrap; - padding: 0px; - padding-bottom: 1px; + white-space: nowrap; + padding: 0px; + padding-bottom: 1px; } .paramname { - white-space: nowrap; - padding: 0px; - padding-bottom: 1px; - margin-left: 2px; + white-space: nowrap; + padding: 0px; + padding-bottom: 1px; + margin-left: 2px; } .paramname em { - color: var(--memdef-param-name-color); - font-style: normal; - margin-right: 1px; + color: var(--memdef-param-name-color); + font-style: normal; + margin-right: 1px; } .paramname .paramdefval { - font-family: var(--font-family-monospace); + font-family: var(--font-family-monospace); } -.params, .retval, .exception, .tparams { - margin-left: 0px; - padding-left: 0px; +.params, +.retval, +.exception, +.tparams { + margin-left: 0px; + padding-left: 0px; } -.params .paramname, .retval .paramname, .tparams .paramname, .exception .paramname { - font-weight: bold; - vertical-align: top; +.params .paramname, +.retval .paramname, +.tparams .paramname, +.exception .paramname { + font-weight: bold; + vertical-align: top; } -.params .paramtype, .tparams .paramtype { - font-style: italic; - vertical-align: top; +.params .paramtype, +.tparams .paramtype { + font-style: italic; + vertical-align: top; } -.params .paramdir, .tparams .paramdir { - font-family: var(--font-family-monospace); - vertical-align: top; +.params .paramdir, +.tparams .paramdir { + font-family: var(--font-family-monospace); + vertical-align: top; } table.mlabels { - border-spacing: 0px; + border-spacing: 0px; } td.mlabels-left { - width: 100%; - padding: 0px; + width: 100%; + padding: 0px; } td.mlabels-right { - vertical-align: bottom; - padding: 0px; - white-space: nowrap; + vertical-align: bottom; + padding: 0px; + white-space: nowrap; } span.mlabels { - margin-left: 8px; + margin-left: 8px; } span.mlabel { - background-color: var(--label-background-color); - border-top:1px solid var(--label-left-top-border-color); - border-left:1px solid var(--label-left-top-border-color); - border-right:1px solid var(--label-right-bottom-border-color); - border-bottom:1px solid var(--label-right-bottom-border-color); - text-shadow: none; - color: var(--label-foreground-color); - margin-right: 4px; - padding: 2px 3px; - border-radius: 3px; - font-size: 7pt; - white-space: nowrap; - vertical-align: middle; + background-color: var(--label-background-color); + border-top: 1px solid var(--label-left-top-border-color); + border-left: 1px solid var(--label-left-top-border-color); + border-right: 1px solid var(--label-right-bottom-border-color); + border-bottom: 1px solid var(--label-right-bottom-border-color); + text-shadow: none; + color: var(--label-foreground-color); + margin-right: 4px; + padding: 2px 3px; + border-radius: 3px; + font-size: 7pt; + white-space: nowrap; + vertical-align: middle; } - - /* @end */ /* these are for tree view inside a (index) page */ div.directory { - margin: 10px 0px; - width: 100%; + margin: 10px 0px; + width: 100%; } .directory table { - border-collapse:collapse; + border-collapse: collapse; } .directory td { - margin: 0px; - padding: 0px; - vertical-align: top; + margin: 0px; + padding: 0px; + vertical-align: top; } .directory td.entry { - white-space: nowrap; - padding-right: 6px; - padding-top: 3px; + white-space: nowrap; + padding-right: 6px; + padding-top: 3px; } .directory td.entry a { - outline:none; + outline: none; } .directory td.entry a img { - border: none; + border: none; } .directory td.desc { - width: 100%; - padding-left: 6px; - padding-right: 6px; - padding-top: 3px; - border-left: 1px solid rgba(0,0,0,0.05); + width: 100%; + padding-left: 6px; + padding-right: 6px; + padding-top: 3px; + border-left: 1px solid rgba(0, 0, 0, 0.05); } .directory tr.odd { - padding-left: 6px; - background-color: var(--index-odd-item-bg-color); + padding-left: 6px; + background-color: var(--index-odd-item-bg-color); } .directory tr.even { - padding-left: 6px; - background-color: var(--index-even-item-bg-color); + padding-left: 6px; + background-color: var(--index-even-item-bg-color); } .directory img { - vertical-align: -30%; + vertical-align: -30%; } .directory .levels { - white-space: nowrap; - width: 100%; - text-align: right; - font-size: 9pt; + white-space: nowrap; + width: 100%; + text-align: right; + font-size: 9pt; } .directory .levels span { - cursor: pointer; - padding-left: 2px; - padding-right: 2px; - color: var(--page-link-color); + cursor: pointer; + padding-left: 2px; + padding-right: 2px; + color: var(--page-link-color); } .arrow { - color: var(--nav-background-color); - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: pointer; - font-size: 80%; - display: inline-block; - width: 16px; - height: 14px; - transition: opacity 0.3s ease; + color: var(--nav-background-color); + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + font-size: 80%; + display: inline-block; + width: 16px; + height: 14px; + transition: opacity 0.3s ease; } span.arrowhead { - position: relative; - padding: 0; - margin: 0 0 0 2px; - display: inline-block; - width: 5px; - height: 5px; - border-right: 2px solid var(--nav-arrow-color); - border-bottom: 2px solid var(--nav-arrow-color); - transform: rotate(-45deg); - transition: transform 0.3s ease; + position: relative; + padding: 0; + margin: 0 0 0 2px; + display: inline-block; + width: 5px; + height: 5px; + border-right: 2px solid var(--nav-arrow-color); + border-bottom: 2px solid var(--nav-arrow-color); + transform: rotate(-45deg); + transition: transform 0.3s ease; } span.arrowhead.opened { - transform: rotate(45deg); + transform: rotate(45deg); } .selected span.arrowhead { - border-right: 2px solid var(--nav-arrow-selected-color); - border-bottom: 2px solid var(--nav-arrow-selected-color); + border-right: 2px solid var(--nav-arrow-selected-color); + border-bottom: 2px solid var(--nav-arrow-selected-color); } .icon { - font-family: var(--font-family-icon); - line-height: normal; - font-weight: bold; - font-size: 12px; - height: 14px; - width: 16px; - display: inline-block; - background-color: var(--icon-background-color); - color: var(--icon-foreground-color); - text-align: center; - border-radius: 4px; - margin-left: 2px; - margin-right: 2px; + font-family: var(--font-family-icon); + line-height: normal; + font-weight: bold; + font-size: 12px; + height: 14px; + width: 16px; + display: inline-block; + background-color: var(--icon-background-color); + color: var(--icon-foreground-color); + text-align: center; + border-radius: 4px; + margin-left: 2px; + margin-right: 2px; } .icona { - width: 24px; - height: 22px; - display: inline-block; + width: 24px; + height: 22px; + display: inline-block; } .iconfolder { - width: 24px; - height: 18px; - margin-top: 6px; - vertical-align:top; - display: inline-block; - position: relative; + width: 24px; + height: 18px; + margin-top: 6px; + vertical-align: top; + display: inline-block; + position: relative; } .icondoc { - width: 24px; - height: 18px; - margin-top: 3px; - vertical-align:top; - display: inline-block; - position: relative; + width: 24px; + height: 18px; + margin-top: 3px; + vertical-align: top; + display: inline-block; + position: relative; } .folder-icon { - width: 16px; - height: 11px; - background-color: var(--icon-folder-fill-color); - border: 1px solid var(--icon-folder-border-color); - border-radius: 0 2px 2px 2px; - position: relative; - box-sizing: content-box; + width: 16px; + height: 11px; + background-color: var(--icon-folder-fill-color); + border: 1px solid var(--icon-folder-border-color); + border-radius: 0 2px 2px 2px; + position: relative; + box-sizing: content-box; } .folder-icon::after { - content: ''; - position: absolute; - top: 2px; - left: -1px; - width: 16px; - height: 7px; - background-color: var(--icon-folder-open-fill-color); - border: 1px solid var(--icon-folder-border-color); - border-radius: 7px 7px 2px 2px; - transform-origin: top left; - opacity: 0; - transition: all 0.3s linear; + content: ""; + position: absolute; + top: 2px; + left: -1px; + width: 16px; + height: 7px; + background-color: var(--icon-folder-open-fill-color); + border: 1px solid var(--icon-folder-border-color); + border-radius: 7px 7px 2px 2px; + transform-origin: top left; + opacity: 0; + transition: all 0.3s linear; } .folder-icon::before { - content: ''; - position: absolute; - top: -3px; - left: -1px; - width: 6px; - height: 2px; - background-color: var(--icon-folder-fill-color); - border-top: 1px solid var(--icon-folder-border-color); - border-left: 1px solid var(--icon-folder-border-color); - border-right: 1px solid var(--icon-folder-border-color); - border-radius: 2px 2px 0 0; + content: ""; + position: absolute; + top: -3px; + left: -1px; + width: 6px; + height: 2px; + background-color: var(--icon-folder-fill-color); + border-top: 1px solid var(--icon-folder-border-color); + border-left: 1px solid var(--icon-folder-border-color); + border-right: 1px solid var(--icon-folder-border-color); + border-radius: 2px 2px 0 0; } .folder-icon.open::after { - top: 3px; - opacity: 1; + top: 3px; + opacity: 1; } .doc-icon { - left: 6px; - width: 12px; - height: 16px; - background-color: var(--icon-doc-border-color); - clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); - position: relative; - display: inline-block; + left: 6px; + width: 12px; + height: 16px; + background-color: var(--icon-doc-border-color); + clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); + position: relative; + display: inline-block; } .doc-icon::before { - content: ""; - left: 1px; - top: 1px; - width: 10px; - height: 14px; - background-color: var(--icon-doc-fill-color); - clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); - position: absolute; - box-sizing: border-box; + content: ""; + left: 1px; + top: 1px; + width: 10px; + height: 14px; + background-color: var(--icon-doc-fill-color); + clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); + position: absolute; + box-sizing: border-box; } .doc-icon::after { - content: ""; - left: 7px; - top: 0px; - width: 3px; - height: 3px; - background-color: transparent; - position: absolute; - border: 1px solid var(--icon-doc-border-color); + content: ""; + left: 7px; + top: 0px; + width: 3px; + height: 3px; + background-color: transparent; + position: absolute; + border: 1px solid var(--icon-doc-border-color); } - - - /* @end */ div.dynheader { - margin-top: 8px; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + margin-top: 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } span.dynarrow { - position: relative; - display: inline-block; - width: 12px; - bottom: 1px; + position: relative; + display: inline-block; + width: 12px; + bottom: 1px; } address { - font-style: normal; - color: var(--footer-foreground-color); + font-style: normal; + color: var(--footer-foreground-color); } table.doxtable caption { - caption-side: top; + caption-side: top; } table.doxtable { - border-collapse:collapse; - margin-top: 4px; - margin-bottom: 4px; + border-collapse: collapse; + margin-top: 4px; + margin-bottom: 4px; } -table.doxtable td, table.doxtable th { - border: 1px solid var(--table-cell-border-color); - padding: 3px 7px 2px; +table.doxtable td, +table.doxtable th { + border: 1px solid var(--table-cell-border-color); + padding: 3px 7px 2px; } table.doxtable th { - background-color: var(--table-header-background-color); - color: var(--table-header-foreground-color); - font-size: 110%; - padding-bottom: 4px; - padding-top: 5px; + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; } table.fieldtable { - margin-bottom: 10px; - border: 1px solid var(--memdef-border-color); - border-spacing: 0px; - border-radius: 4px; - box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); + margin-bottom: 10px; + border: 1px solid var(--memdef-border-color); + border-spacing: 0px; + border-radius: 4px; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); } -.fieldtable td, .fieldtable th { - padding: 3px 7px 2px; +.fieldtable td, +.fieldtable th { + padding: 3px 7px 2px; } -.fieldtable td.fieldtype, .fieldtable td.fieldname, .fieldtable td.fieldinit { - white-space: nowrap; - border-right: 1px solid var(--memdef-border-color); - border-bottom: 1px solid var(--memdef-border-color); - vertical-align: top; +.fieldtable td.fieldtype, +.fieldtable td.fieldname, +.fieldtable td.fieldinit { + white-space: nowrap; + border-right: 1px solid var(--memdef-border-color); + border-bottom: 1px solid var(--memdef-border-color); + vertical-align: top; } .fieldtable td.fieldname { - padding-top: 3px; + padding-top: 3px; } .fieldtable td.fieldinit { - padding-top: 3px; - text-align: right; + padding-top: 3px; + text-align: right; } - .fieldtable td.fielddoc { - border-bottom: 1px solid var(--memdef-border-color); + border-bottom: 1px solid var(--memdef-border-color); } .fieldtable td.fielddoc p:first-child { - margin-top: 0px; + margin-top: 0px; } .fieldtable td.fielddoc p:last-child { - margin-bottom: 2px; + margin-bottom: 2px; } .fieldtable tr:last-child td { - border-bottom: none; + border-bottom: none; } .fieldtable th { - background-color: var(--memdef-title-background-color); - font-size: 90%; - color: var(--memdef-proto-text-color); - padding-bottom: 4px; - padding-top: 5px; - text-align:left; - font-weight: 400; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom: 1px solid var(--memdef-border-color); + background-color: var(--memdef-title-background-color); + font-size: 90%; + color: var(--memdef-proto-text-color); + padding-bottom: 4px; + padding-top: 5px; + text-align: left; + font-weight: 400; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 1px solid var(--memdef-border-color); } /* ----------- navigation breadcrumb styling ----------- */ #nav-path ul { - height: 30px; - line-height: 30px; - color: var(--nav-text-normal-color); - overflow: hidden; - margin: 0px; - padding-left: 4px; - background-image: none; - background: var(--page-background-color); - border-bottom: 1px solid var(--nav-breadcrumb-separator-color); - font-size: var(--nav-font-size-level1); - font-family: var(--font-family-nav); - position: relative; - z-index: 100; + height: 30px; + line-height: 30px; + color: var(--nav-text-normal-color); + overflow: hidden; + margin: 0px; + padding-left: 4px; + background-image: none; + background: var(--page-background-color); + border-bottom: 1px solid var(--nav-breadcrumb-separator-color); + font-size: var(--nav-font-size-level1); + font-family: var(--font-family-nav); + position: relative; + z-index: 100; } #main-nav { - border-bottom: 1px solid var(--nav-border-color); + border-bottom: 1px solid var(--nav-border-color); } .navpath li { - list-style-type:none; - float:left; - color: var(--nav-foreground-color); + list-style-type: none; + float: left; + color: var(--nav-foreground-color); } .navpath li.footer { - list-style-type:none; - float:right; - padding-left:10px; - padding-right:15px; - background-image:none; - background-repeat:no-repeat; - background-position:right; - font-size: 8pt; - color: var(--footer-foreground-color); + list-style-type: none; + float: right; + padding-left: 10px; + padding-right: 15px; + background-image: none; + background-repeat: no-repeat; + background-position: right; + font-size: 8pt; + color: var(--footer-foreground-color); } #nav-path li.navelem { - background-image: none; - display: flex; - align-items: center; - padding-left: 15px; + background-image: none; + display: flex; + align-items: center; + padding-left: 15px; } .navpath li.navelem a { - text-shadow: none; - display: inline-block; - color: var(--nav-breadcrumb-color); - position: relative; - top: 0px; - height: 30px; - margin-right: -20px; + text-shadow: none; + display: inline-block; + color: var(--nav-breadcrumb-color); + position: relative; + top: 0px; + height: 30px; + margin-right: -20px; } #nav-path li.navelem:after { - content: ''; - display: inline-block; - position: relative; - top: 0; - right: -15px; - width: 30px; - height: 30px; - transform: scaleX(0.5) scale(0.707) rotate(45deg); - z-index: 10; - background: var(--page-background-color); - box-shadow: 2px -2px 0 2px var(--nav-breadcrumb-separator-color); - border-radius: 0 5px 0 50px; + content: ""; + display: inline-block; + position: relative; + top: 0; + right: -15px; + width: 30px; + height: 30px; + transform: scaleX(0.5) scale(0.707) rotate(45deg); + z-index: 10; + background: var(--page-background-color); + box-shadow: 2px -2px 0 2px var(--nav-breadcrumb-separator-color); + border-radius: 0 5px 0 50px; } #nav-path li.navelem:first-child { - margin-left: -6px; + margin-left: -6px; } #nav-path li.navelem:hover, #nav-path li.navelem:hover:after { - background-color: var(--nav-breadcrumb-active-bg); + background-color: var(--nav-breadcrumb-active-bg); } /* ---------------------- */ -div.summary -{ - float: right; - font-size: 8pt; - padding-right: 5px; - width: 50%; - text-align: right; +div.summary { + float: right; + font-size: 8pt; + padding-right: 5px; + width: 50%; + text-align: right; } -div.summary a -{ - white-space: nowrap; +div.summary a { + white-space: nowrap; } -table.classindex -{ - margin: 10px; - white-space: nowrap; - margin-left: 3%; - margin-right: 3%; - width: 94%; - border: 0; - border-spacing: 0; - padding: 0; +table.classindex { + margin: 10px; + white-space: nowrap; + margin-left: 3%; + margin-right: 3%; + width: 94%; + border: 0; + border-spacing: 0; + padding: 0; } -div.ingroups -{ - font-size: 8pt; - width: 50%; - text-align: left; +div.ingroups { + font-size: 8pt; + width: 50%; + text-align: left; } -div.ingroups a -{ - white-space: nowrap; +div.ingroups a { + white-space: nowrap; } -div.header -{ - margin: 0px; - background-color: var(--header-background-color); - border-bottom: 1px solid var(--header-separator-color); +div.header { + margin: 0px; + background-color: var(--header-background-color); + border-bottom: 1px solid var(--header-separator-color); } -div.headertitle -{ - padding: 5px 5px 5px 10px; +div.headertitle { + padding: 5px 5px 5px 10px; } dl { - padding: 0 0 0 0; + padding: 0 0 0 0; } -dl.bug dt a, dl.deprecated dt a, dl.todo dt a, dl.test a { - font-weight: bold !important; +dl.bug dt a, +dl.deprecated dt a, +dl.todo dt a, +dl.test a { + font-weight: bold !important; } -dl.warning, dl.attention, dl.important, dl.note, dl.deprecated, dl.bug, -dl.invariant, dl.pre, dl.post, dl.todo, dl.test, dl.remark { - padding: 10px; - margin: 10px 0px; - overflow: hidden; - margin-left: 0; - border-radius: 4px; +dl.warning, +dl.attention, +dl.important, +dl.note, +dl.deprecated, +dl.bug, +dl.invariant, +dl.pre, +dl.post, +dl.todo, +dl.test, +dl.remark { + padding: 10px; + margin: 10px 0px; + overflow: hidden; + margin-left: 0; + border-radius: 4px; } dl.section dd { - margin-bottom: 2px; + margin-bottom: 2px; } -dl.warning, dl.attention, dl.important { - background: var(--warning-color-bg); - border-left: 8px solid var(--warning-color-hl); - color: var(--warning-color-text); +dl.warning, +dl.attention, +dl.important { + background: var(--warning-color-bg); + border-left: 8px solid var(--warning-color-hl); + color: var(--warning-color-text); } -dl.warning dt, dl.attention dt, dl.important dt { - color: var(--warning-color-hl); +dl.warning dt, +dl.attention dt, +dl.important dt { + color: var(--warning-color-hl); } -dl.note, dl.remark { - background: var(--note-color-bg); - border-left: 8px solid var(--note-color-hl); - color: var(--note-color-text); +dl.note, +dl.remark { + background: var(--note-color-bg); + border-left: 8px solid var(--note-color-hl); + color: var(--note-color-text); } -dl.note dt, dl.remark dt { - color: var(--note-color-hl); +dl.note dt, +dl.remark dt { + color: var(--note-color-hl); } dl.todo { - background: var(--todo-color-bg); - border-left: 8px solid var(--todo-color-hl); - color: var(--todo-color-text); + background: var(--todo-color-bg); + border-left: 8px solid var(--todo-color-hl); + color: var(--todo-color-text); } dl.todo dt { - color: var(--todo-color-hl); + color: var(--todo-color-hl); } dl.test { - background: var(--test-color-bg); - border-left: 8px solid var(--test-color-hl); - color: var(--test-color-text); + background: var(--test-color-bg); + border-left: 8px solid var(--test-color-hl); + color: var(--test-color-text); } dl.test dt { - color: var(--test-color-hl); + color: var(--test-color-hl); } dl.bug dt a { - color: var(--bug-color-hl) !important; + color: var(--bug-color-hl) !important; } dl.bug { - background: var(--bug-color-bg); - border-left: 8px solid var(--bug-color-hl); - color: var(--bug-color-text); + background: var(--bug-color-bg); + border-left: 8px solid var(--bug-color-hl); + color: var(--bug-color-text); } dl.bug dt a { - color: var(--bug-color-hl) !important; + color: var(--bug-color-hl) !important; } dl.deprecated { - background: var(--deprecated-color-bg); - border-left: 8px solid var(--deprecated-color-hl); - color: var(--deprecated-color-text); + background: var(--deprecated-color-bg); + border-left: 8px solid var(--deprecated-color-hl); + color: var(--deprecated-color-text); } dl.deprecated dt a { - color: var(--deprecated-color-hl) !important; + color: var(--deprecated-color-hl) !important; } -dl.note dd, dl.warning dd, dl.pre dd, dl.post dd, -dl.remark dd, dl.attention dd, dl.important dd, dl.invariant dd, -dl.bug dd, dl.deprecated dd, dl.todo dd, dl.test dd { - margin-inline-start: 0px; +dl.note dd, +dl.warning dd, +dl.pre dd, +dl.post dd, +dl.remark dd, +dl.attention dd, +dl.important dd, +dl.invariant dd, +dl.bug dd, +dl.deprecated dd, +dl.todo dd, +dl.test dd { + margin-inline-start: 0px; } -dl.invariant, dl.pre, dl.post { - background: var(--invariant-color-bg); - border-left: 8px solid var(--invariant-color-hl); - color: var(--invariant-color-text); +dl.invariant, +dl.pre, +dl.post { + background: var(--invariant-color-bg); + border-left: 8px solid var(--invariant-color-hl); + color: var(--invariant-color-text); } -dl.invariant dt, dl.pre dt, dl.post dt { - color: var(--invariant-color-hl); +dl.invariant dt, +dl.pre dt, +dl.post dt { + color: var(--invariant-color-hl); } - -#projectrow -{ - height: 56px; +#projectrow { + height: 56px; } -#projectlogo -{ - text-align: center; - vertical-align: bottom; - border-collapse: separate; +#projectlogo { + text-align: center; + vertical-align: bottom; + border-collapse: separate; } -#projectlogo img -{ - border: 0px none; +#projectlogo img { + border: 0px none; } -#projectalign -{ - vertical-align: middle; - padding-left: 0.5em; +#projectalign { + vertical-align: middle; + padding-left: 0.5em; } -#projectname -{ - font-size: 200%; - font-family: var(--font-family-title); - margin: 0; - padding: 0; +#projectname { + font-size: 200%; + font-family: var(--font-family-title); + margin: 0; + padding: 0; } -#side-nav #projectname -{ - font-size: 130%; +#side-nav #projectname { + font-size: 130%; } -#projectbrief -{ - font-size: 90%; - font-family: var(--font-family-title); - margin: 0px; - padding: 0px; +#projectbrief { + font-size: 90%; + font-family: var(--font-family-title); + margin: 0px; + padding: 0px; } -#projectnumber -{ - font-size: 50%; - font-family: var(--font-family-title); - margin: 0px; - padding: 0px; +#projectnumber { + font-size: 50%; + font-family: var(--font-family-title); + margin: 0px; + padding: 0px; } -#titlearea -{ - padding: 0 0 0 5px; - margin: 0px; - border-bottom: 1px solid var(--title-separator-color); - background-color: var(--title-background-color); +#titlearea { + padding: 0 0 0 5px; + margin: 0px; + border-bottom: 1px solid var(--title-separator-color); + background-color: var(--title-background-color); } -.image -{ - text-align: center; +.image { + text-align: center; } -.dotgraph -{ - text-align: center; +.dotgraph { + text-align: center; } -.mscgraph -{ - text-align: center; +.mscgraph { + text-align: center; } -.plantumlgraph -{ - text-align: center; +.plantumlgraph { + text-align: center; } -.diagraph -{ - text-align: center; +.diagraph { + text-align: center; } -.caption -{ - font-weight: bold; +.caption { + font-weight: bold; } dl.citelist { - margin-bottom:50px; + margin-bottom: 50px; } dl.citelist dt { - color:var(--citation-label-color); - float:left; - font-weight:bold; - margin-right:10px; - padding:5px; - text-align:right; - width:52px; + color: var(--citation-label-color); + float: left; + font-weight: bold; + margin-right: 10px; + padding: 5px; + text-align: right; + width: 52px; } dl.citelist dd { - margin:2px 0 2px 72px; - padding:5px 0; + margin: 2px 0 2px 72px; + padding: 5px 0; } div.toc { - padding: 14px 25px; - background-color: var(--toc-background-color); - border: 1px solid var(--toc-border-color); - border-radius: 7px 7px 7px 7px; - float: right; - height: auto; - margin: 0 8px 10px 10px; - width: 200px; + padding: 14px 25px; + background-color: var(--toc-background-color); + border: 1px solid var(--toc-border-color); + border-radius: 7px 7px 7px 7px; + float: right; + height: auto; + margin: 0 8px 10px 10px; + width: 200px; } div.toc li { - background: var(--toc-down-arrow-image) no-repeat scroll 0 5px transparent; - font: 10px/1.2 var(--font-family-toc); - margin-top: 5px; - padding-left: 10px; - padding-top: 2px; + background: var(--toc-down-arrow-image) no-repeat scroll 0 5px transparent; + font: 10px/1.2 var(--font-family-toc); + margin-top: 5px; + padding-left: 10px; + padding-top: 2px; } div.toc h3 { - font: bold 12px/1.2 var(--font-family-toc); - color: var(--toc-header-color); - border-bottom: 0 none; - margin: 0; + font: bold 12px/1.2 var(--font-family-toc); + color: var(--toc-header-color); + border-bottom: 0 none; + margin: 0; } div.toc ul { - list-style: none outside none; - border: medium none; - padding: 0px; + list-style: none outside none; + border: medium none; + padding: 0px; } -div.toc li[class^='level'] { - margin-left: 15px; +div.toc li[class^="level"] { + margin-left: 15px; } div.toc li.level1 { - margin-left: 0px; + margin-left: 0px; } div.toc li.empty { - background-image: none; - margin-top: 0px; + background-image: none; + margin-top: 0px; } span.emoji { - /* font family used at the site: https://unicode.org/emoji/charts/full-emoji-list.html + /* font family used at the site: https://unicode.org/emoji/charts/full-emoji-list.html * font-family: "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort; */ } span.obfuscator { - display: none; + display: none; } .inherit_header { - font-weight: 400; - cursor: pointer; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + font-weight: 400; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .inherit_header td { - padding: 6px 0 2px 0; + padding: 6px 0 2px 0; } .inherit { - display: none; + display: none; } tr.heading h2 { - margin-top: 12px; - margin-bottom: 12px; + margin-top: 12px; + margin-bottom: 12px; } /* tooltip related style info */ .ttc { - position: absolute; - display: none; + position: absolute; + display: none; } #powerTip { - cursor: default; - color: var(--tooltip-foreground-color); - background-color: var(--tooltip-background-color); - backdrop-filter: var(--tooltip-backdrop-filter); - -webkit-backdrop-filter: var(--tooltip-backdrop-filter); - border: 1px solid var(--tooltip-border-color); - border-radius: 4px; - box-shadow: var(--tooltip-shadow); - display: none; - font-size: smaller; - max-width: 80%; - padding: 1ex 1em 1em; - position: absolute; - z-index: 2147483647; + cursor: default; + color: var(--tooltip-foreground-color); + background-color: var(--tooltip-background-color); + backdrop-filter: var(--tooltip-backdrop-filter); + -webkit-backdrop-filter: var(--tooltip-backdrop-filter); + border: 1px solid var(--tooltip-border-color); + border-radius: 4px; + box-shadow: var(--tooltip-shadow); + display: none; + font-size: smaller; + max-width: 80%; + padding: 1ex 1em 1em; + position: absolute; + z-index: 2147483647; } #powerTip div.ttdoc { - color: var(--tooltip-doc-color); - font-style: italic; + color: var(--tooltip-doc-color); + font-style: italic; } #powerTip div.ttname a { - font-weight: bold; + font-weight: bold; } #powerTip a { - color: var(--tooltip-link-color); + color: var(--tooltip-link-color); } #powerTip div.ttname { - font-weight: bold; + font-weight: bold; } #powerTip div.ttdeci { - color: var(--tooltip-declaration-color); + color: var(--tooltip-declaration-color); } #powerTip div { - margin: 0px; - padding: 0px; - font-size: 12px; - font-family: var(--font-family-tooltip); - line-height: 16px; + margin: 0px; + padding: 0px; + font-size: 12px; + font-family: var(--font-family-tooltip); + line-height: 16px; } -#powerTip:before, #powerTip:after { - content: ""; - position: absolute; - margin: 0px; +#powerTip:before, +#powerTip:after { + content: ""; + position: absolute; + margin: 0px; } -#powerTip.n:after, #powerTip.n:before, -#powerTip.s:after, #powerTip.s:before, -#powerTip.w:after, #powerTip.w:before, -#powerTip.e:after, #powerTip.e:before, -#powerTip.ne:after, #powerTip.ne:before, -#powerTip.se:after, #powerTip.se:before, -#powerTip.nw:after, #powerTip.nw:before, -#powerTip.sw:after, #powerTip.sw:before { - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; +#powerTip.n:after, +#powerTip.n:before, +#powerTip.s:after, +#powerTip.s:before, +#powerTip.w:after, +#powerTip.w:before, +#powerTip.e:after, +#powerTip.e:before, +#powerTip.ne:after, +#powerTip.ne:before, +#powerTip.se:after, +#powerTip.se:before, +#powerTip.nw:after, +#powerTip.nw:before, +#powerTip.sw:after, +#powerTip.sw:before { + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; } -#powerTip.n:after, #powerTip.s:after, -#powerTip.w:after, #powerTip.e:after, -#powerTip.nw:after, #powerTip.ne:after, -#powerTip.sw:after, #powerTip.se:after { - border-color: rgba(255, 255, 255, 0); +#powerTip.n:after, +#powerTip.s:after, +#powerTip.w:after, +#powerTip.e:after, +#powerTip.nw:after, +#powerTip.ne:after, +#powerTip.sw:after, +#powerTip.se:after { + border-color: rgba(255, 255, 255, 0); } -#powerTip.n:before, #powerTip.s:before, -#powerTip.w:before, #powerTip.e:before, -#powerTip.nw:before, #powerTip.ne:before, -#powerTip.sw:before, #powerTip.se:before { - border-color: rgba(128, 128, 128, 0); +#powerTip.n:before, +#powerTip.s:before, +#powerTip.w:before, +#powerTip.e:before, +#powerTip.nw:before, +#powerTip.ne:before, +#powerTip.sw:before, +#powerTip.se:before { + border-color: rgba(128, 128, 128, 0); } -#powerTip.n:after, #powerTip.n:before, -#powerTip.ne:after, #powerTip.ne:before, -#powerTip.nw:after, #powerTip.nw:before { - top: 100%; +#powerTip.n:after, +#powerTip.n:before, +#powerTip.ne:after, +#powerTip.ne:before, +#powerTip.nw:after, +#powerTip.nw:before { + top: 100%; } -#powerTip.n:after, #powerTip.ne:after, #powerTip.nw:after { - border-top-color: var(--tooltip-arrow-background-color); - border-width: 10px; - margin: 0px -10px; +#powerTip.n:after, +#powerTip.ne:after, +#powerTip.nw:after { + border-top-color: var(--tooltip-arrow-background-color); + border-width: 10px; + margin: 0px -10px; } -#powerTip.n:before, #powerTip.ne:before, #powerTip.nw:before { - border-top-color: var(--tooltip-border-color); - border-width: 11px; - margin: 0px -11px; +#powerTip.n:before, +#powerTip.ne:before, +#powerTip.nw:before { + border-top-color: var(--tooltip-border-color); + border-width: 11px; + margin: 0px -11px; } -#powerTip.n:after, #powerTip.n:before { - left: 50%; +#powerTip.n:after, +#powerTip.n:before { + left: 50%; } -#powerTip.nw:after, #powerTip.nw:before { - right: 14px; +#powerTip.nw:after, +#powerTip.nw:before { + right: 14px; } -#powerTip.ne:after, #powerTip.ne:before { - left: 14px; +#powerTip.ne:after, +#powerTip.ne:before { + left: 14px; } -#powerTip.s:after, #powerTip.s:before, -#powerTip.se:after, #powerTip.se:before, -#powerTip.sw:after, #powerTip.sw:before { - bottom: 100%; +#powerTip.s:after, +#powerTip.s:before, +#powerTip.se:after, +#powerTip.se:before, +#powerTip.sw:after, +#powerTip.sw:before { + bottom: 100%; } -#powerTip.s:after, #powerTip.se:after, #powerTip.sw:after { - border-bottom-color: var(--tooltip-arrow-background-color); - border-width: 10px; - margin: 0px -10px; +#powerTip.s:after, +#powerTip.se:after, +#powerTip.sw:after { + border-bottom-color: var(--tooltip-arrow-background-color); + border-width: 10px; + margin: 0px -10px; } -#powerTip.s:before, #powerTip.se:before, #powerTip.sw:before { - border-bottom-color: var(--tooltip-border-color); - border-width: 11px; - margin: 0px -11px; +#powerTip.s:before, +#powerTip.se:before, +#powerTip.sw:before { + border-bottom-color: var(--tooltip-border-color); + border-width: 11px; + margin: 0px -11px; } -#powerTip.s:after, #powerTip.s:before { - left: 50%; +#powerTip.s:after, +#powerTip.s:before { + left: 50%; } -#powerTip.sw:after, #powerTip.sw:before { - right: 14px; +#powerTip.sw:after, +#powerTip.sw:before { + right: 14px; } -#powerTip.se:after, #powerTip.se:before { - left: 14px; +#powerTip.se:after, +#powerTip.se:before { + left: 14px; } -#powerTip.e:after, #powerTip.e:before { - left: 100%; +#powerTip.e:after, +#powerTip.e:before { + left: 100%; } #powerTip.e:after { - border-left-color: var(--tooltip-border-color); - border-width: 10px; - top: 50%; - margin-top: -10px; + border-left-color: var(--tooltip-border-color); + border-width: 10px; + top: 50%; + margin-top: -10px; } #powerTip.e:before { - border-left-color: var(--tooltip-border-color); - border-width: 11px; - top: 50%; - margin-top: -11px; + border-left-color: var(--tooltip-border-color); + border-width: 11px; + top: 50%; + margin-top: -11px; } -#powerTip.w:after, #powerTip.w:before { - right: 100%; +#powerTip.w:after, +#powerTip.w:before { + right: 100%; } #powerTip.w:after { - border-right-color: var(--tooltip-border-color); - border-width: 10px; - top: 50%; - margin-top: -10px; + border-right-color: var(--tooltip-border-color); + border-width: 10px; + top: 50%; + margin-top: -10px; } #powerTip.w:before { - border-right-color: var(--tooltip-border-color); - border-width: 11px; - top: 50%; - margin-top: -11px; + border-right-color: var(--tooltip-border-color); + border-width: 11px; + top: 50%; + margin-top: -11px; } -@media print -{ - #top { display: none; } - #side-nav { display: none; } - #nav-path { display: none; } - body { overflow:visible; } - h1, h2, h3, h4, h5, h6 { page-break-after: avoid; } - .summary { display: none; } - .memitem { page-break-inside: avoid; } - #doc-content - { - margin-left:0 !important; - height:auto !important; - width:auto !important; - overflow:inherit; - display:inline; - } +@media print { + #top { + display: none; + } + #side-nav { + display: none; + } + #nav-path { + display: none; + } + body { + overflow: visible; + } + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-after: avoid; + } + .summary { + display: none; + } + .memitem { + page-break-inside: avoid; + } + #doc-content { + margin-left: 0 !important; + height: auto !important; + width: auto !important; + overflow: inherit; + display: inline; + } } /* @group Markdown */ table.markdownTable { - border-collapse:collapse; - margin-top: 4px; - margin-bottom: 4px; + border-collapse: collapse; + margin-top: 4px; + margin-bottom: 4px; } -table.markdownTable td, table.markdownTable th { - border: 1px solid var(--table-cell-border-color); - padding: 3px 7px 2px; +table.markdownTable td, +table.markdownTable th { + border: 1px solid var(--table-cell-border-color); + padding: 3px 7px 2px; } table.markdownTable tr { } -th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { - background-color: var(--table-header-background-color); - color: var(--table-header-foreground-color); - font-size: 110%; - padding-bottom: 4px; - padding-top: 5px; +th.markdownTableHeadLeft, +th.markdownTableHeadRight, +th.markdownTableHeadCenter, +th.markdownTableHeadNone { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; } -th.markdownTableHeadLeft, td.markdownTableBodyLeft { - text-align: left +th.markdownTableHeadLeft, +td.markdownTableBodyLeft { + text-align: left; } -th.markdownTableHeadRight, td.markdownTableBodyRight { - text-align: right +th.markdownTableHeadRight, +td.markdownTableBodyRight { + text-align: right; } -th.markdownTableHeadCenter, td.markdownTableBodyCenter { - text-align: center +th.markdownTableHeadCenter, +td.markdownTableBodyCenter { + text-align: center; } -tt, code, kbd -{ - display: inline-block; +tt, +code, +kbd { + display: inline-block; } -tt, code, kbd -{ - vertical-align: top; +tt, +code, +kbd { + vertical-align: top; } /* @end */ u { - text-decoration: underline; + text-decoration: underline; } -details>summary { - list-style-type: none; +details > summary { + list-style-type: none; } details > summary::-webkit-details-marker { - display: none; + display: none; } -details>summary::before { - content: "\25ba"; - padding-right:4px; - font-size: 80%; +details > summary::before { + content: "\25ba"; + padding-right: 4px; + font-size: 80%; } -details[open]>summary::before { - content: "\25bc"; - padding-right:4px; - font-size: 80%; +details[open] > summary::before { + content: "\25bc"; + padding-right: 4px; + font-size: 80%; } :root { - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-background-color); + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color) + var(--scrollbar-background-color); } ::-webkit-scrollbar { - background-color: var(--scrollbar-background-color); - height: 12px; - width: 12px; + background-color: var(--scrollbar-background-color); + height: 12px; + width: 12px; } ::-webkit-scrollbar-thumb { - border-radius: 6px; - box-shadow: inset 0 0 12px 12px var(--scrollbar-thumb-color); - border: solid 2px transparent; + border-radius: 6px; + box-shadow: inset 0 0 12px 12px var(--scrollbar-thumb-color); + border: solid 2px transparent; } ::-webkit-scrollbar-corner { - background-color: var(--scrollbar-background-color); + background-color: var(--scrollbar-background-color); } - From d7b9ce76a68baffa6ab1f70a2a5a79f00eb6105f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 08:47:01 -0700 Subject: [PATCH 193/319] typos --- dist/doxygen/stylesheet.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/doxygen/stylesheet.css b/dist/doxygen/stylesheet.css index 21f34435e..31ebcc685 100644 --- a/dist/doxygen/stylesheet.css +++ b/dist/doxygen/stylesheet.css @@ -112,7 +112,7 @@ html { --toc-background-color: #f4f6fa; --toc-border-color: #d8dfee; --toc-header-color: #4665a2; - --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); /** search field */ --search-background-color: white; @@ -163,7 +163,7 @@ html { --tooltip-arrow-background-color: white; --tooltip-border-color: rgba(150, 150, 150, 0.7); --tooltip-backdrop-filter: blur(3px); - --tooltip-doc-color: grey; + --tooltip-doc-color: gray; --tooltip-declaration-color: #006318; --tooltip-link-color: #4665a2; --tooltip-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.25); @@ -322,7 +322,7 @@ html { --toc-background-color: #151e30; --toc-border-color: #202e4a; --toc-header-color: #a3b4d7; - --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); /** search field */ --search-background-color: black; From e4f0c366ff5b5006b6e837dc9350ae53e78e76e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 08:48:54 -0700 Subject: [PATCH 194/319] lib-vt docs: add etags to the pages --- src/build/docker/lib-c-docs/entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/build/docker/lib-c-docs/entrypoint.sh b/src/build/docker/lib-c-docs/entrypoint.sh index 67cc44b03..ac9ca1c06 100755 --- a/src/build/docker/lib-c-docs/entrypoint.sh +++ b/src/build/docker/lib-c-docs/entrypoint.sh @@ -6,6 +6,8 @@ server { location / { root /usr/share/nginx/html; index index.html; + etag on; + add_header Cache-Control "no-cache" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; } @@ -20,6 +22,8 @@ server { location / { root /usr/share/nginx/html; index index.html; + etag on; + add_header Cache-Control "no-cache" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; } } From e1e5bfc09682b233f5d0ea65d20497ba42b5275a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 09:17:14 -0700 Subject: [PATCH 195/319] apprt/gtk: only close with no windows active if close delay is off Fixes #9052 --- src/apprt/gtk/class/application.zig | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 07663fec9..a35cd5b3f 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -530,12 +530,16 @@ pub const Application = extern struct { } // If we have no windows attached to our app, also quit. - if (priv.requested_window and @as( - ?*glib.List, - self.as(gtk.Application).getWindows(), - ) == null) { - log.debug("must_quit due to no app windows", .{}); - break :q true; + // We only do this if we don't have the closed delay set, + // because with the closed delay set we'll exit eventually. + if (config.@"quit-after-last-window-closed-delay" == null) { + if (priv.requested_window and @as( + ?*glib.List, + self.as(gtk.Application).getWindows(), + ) == null) { + log.debug("must_quit due to no app windows", .{}); + break :q true; + } } // No quit conditions met From debdf6bf03ff25a4ccbf00c1c74bb4f723de3e00 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 6 Oct 2025 14:52:09 -0500 Subject: [PATCH 196/319] osc: parse additional OSC 133 options OSC 133;A can have: - special_key - click_events OSC 133;C can have: - cmdline - cmdline_url Notably, they are in use by `fish`. Not sure what other shells currently use these options. Note that the options are only parsed. Nothing further is done with them at this point. --- src/inspector/termio.zig | 5 + src/terminal/osc.zig | 228 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 218 insertions(+), 15 deletions(-) diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 03a3b0375..212f0ea4a 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -264,6 +264,11 @@ pub const VTEvent = struct { if (std.mem.eql(u8, field.name, tag_name)) { const s = if (field.type == void) try alloc.dupeZ(u8, tag_name) + else if (field.type == [:0]const u8 or field.type == []const u8) + try std.fmt.allocPrintSentinel(alloc, "{s}={s}", .{ + tag_name, + @field(value, field.name), + }, 0) else try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{ tag_name, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 1d41d95f2..4b0f9553c 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -44,19 +44,33 @@ pub const Command = union(Key) { /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a /// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed /// not all shells will send the prompt end code. - /// - /// "aid" is an optional "application identifier" that helps disambiguate - /// nested shell sessions. It can be anything but is usually a process ID. - /// - /// "kind" tells us which kind of semantic prompt sequence this is: - /// - primary: normal, left-aligned first-line prompt (initial, default) - /// - continuation: an editable continuation line - /// - secondary: a non-editable continuation line - /// - right: a right-aligned prompt that may need adjustment during reflow prompt_start: struct { + /// "aid" is an optional "application identifier" that helps disambiguate + /// nested shell sessions. It can be anything but is usually a process ID. aid: ?[:0]const u8 = null, + /// "kind" tells us which kind of semantic prompt sequence this is: + /// - primary: normal, left-aligned first-line prompt (initial, default) + /// - continuation: an editable continuation line + /// - secondary: a non-editable continuation line + /// - right: a right-aligned prompt that may need adjustment during reflow kind: enum { primary, continuation, secondary, right } = .primary, + /// If true, the shell will not redraw the prompt on resize so don't erase it. + /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers redraw: bool = true, + /// Use a special key instead of arrow keys to move the cursor on + /// mouse click. Useful if arrow keys have side-effets like triggering + /// auto-complete. The shell integration script should bind the special + /// key as needed. + /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + special_key: bool = false, + /// If true, the shell is capable of handling mouse click events. + /// Ghostty will then send a click event to the shell when the user + /// clicks somewhere in the prompt. The shell can then move the cursor + /// to that position or perform some other appropriate action. If false, + /// Ghostty may generate a number of fake key events to move the cursor + /// which is not very robust. + /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + click_events: bool = false, }, /// End of prompt and start of user input, terminated by a OSC "133;C" @@ -72,7 +86,16 @@ pub const Command = union(Key) { /// OSC "133;I" then this is the start of a continuation input line. /// If we see anything else, it is the start of the output area (or end /// of command). - end_of_input: void, + end_of_input: struct { + /// The command line that the user entered. + /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + cmdline: ?union(enum) { + /// The command line has been encoded with bash's 'printf "%q"'. + printf_q_encoded: [:0]const u8, + /// The command line has been encoded with URL percent encoding. + percent_encoded: [:0]const u8, + } = null, + }, /// End of current command. /// @@ -1286,7 +1309,7 @@ pub const Parser = struct { 'C' => { self.state = .semantic_option_start; - self.command = .{ .end_of_input = {} }; + self.command = .{ .end_of_input = .{} }; self.complete = true; }, @@ -1456,11 +1479,24 @@ pub const Parser = struct { .prompt_start => |*v| v.aid = value, else => {}, } + } else if (mem.eql(u8, self.temp_state.key, "cmdline")) { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + switch (self.command) { + .end_of_input => |*v| v.cmdline = .{ + .printf_q_encoded = value, + }, + else => {}, + } + } else if (mem.eql(u8, self.temp_state.key, "cmdline_url")) { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + switch (self.command) { + .end_of_input => |*v| v.cmdline = .{ + .percent_encoded = value, + }, + else => {}, + } } else if (mem.eql(u8, self.temp_state.key, "redraw")) { - // Kitty supports a "redraw" option for prompt_start. I can't find - // this documented anywhere but can see in the code that this is used - // by shell environments to tell the terminal that the shell will NOT - // redraw the prompt so we should attempt to resize it. + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers switch (self.command) { .prompt_start => |*v| { const valid = if (value.len == 1) valid: { @@ -1479,7 +1515,48 @@ pub const Parser = struct { }, else => {}, } + } else if (mem.eql(u8, self.temp_state.key, "special_key")) { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + switch (self.command) { + .prompt_start => |*v| { + const valid = if (value.len == 1) valid: { + switch (value[0]) { + '0' => v.special_key = false, + '1' => v.special_key = true, + else => break :valid false, + } + + break :valid true; + } else false; + + if (!valid) { + log.info("OSC 133 A invalid special_key value: {s}", .{value}); + } + }, + else => {}, + } + } else if (mem.eql(u8, self.temp_state.key, "click_events")) { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + switch (self.command) { + .prompt_start => |*v| { + const valid = if (value.len == 1) valid: { + switch (value[0]) { + '0' => v.click_events = false, + '1' => v.click_events = true, + else => break :valid false, + } + + break :valid true; + } else false; + + if (!valid) { + log.info("OSC 133 A invalid click_events value: {s}", .{value}); + } + }, + else => {}, + } } else if (mem.eql(u8, self.temp_state.key, "k")) { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers // The "k" marks the kind of prompt, or "primary" if we don't know. // This can be used to distinguish between the first (initial) prompt, // a continuation, etc. @@ -2846,6 +2923,97 @@ test "OSC 133: prompt_start with secondary" { try testing.expect(cmd.prompt_start.kind == .secondary); } +test "OSC 133: prompt_start with special_key" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;special_key=1"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == true); +} + +test "OSC 133: prompt_start with special_key invalid" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;special_key=bobr"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == false); +} + +test "OSC 133: prompt_start with special_key 0" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;special_key=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == false); +} + +test "OSC 133: prompt_start with special_key empty" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;special_key="; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == false); +} + +test "OSC 133: prompt_start with click_events true" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;click_events=1"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.click_events == true); +} + +test "OSC 133: prompt_start with click_events false" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;click_events=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.click_events == false); +} + +test "OSC 133: prompt_start with click_events empty" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;A;click_events="; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.click_events == false); +} + test "OSC 133: end_of_command no exit code" { const testing = std.testing; @@ -2895,6 +3063,36 @@ test "OSC 133: end_of_input" { try testing.expect(cmd == .end_of_input); } +test "OSC 133: end_of_input with cmdline" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=echo bobr kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expect(cmd.end_of_input.cmdline.? == .printf_q_encoded); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?.printf_q_encoded); +} + +test "OSC 133: end_of_input with cmdline_url" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expect(cmd.end_of_input.cmdline.? == .percent_encoded); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?.percent_encoded); +} + test "OSC: OSC 777 show desktop notification with title" { const testing = std.testing; From f72bbb50381176e85a338654992e35a47d8a0afa Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 6 Oct 2025 15:01:16 -0500 Subject: [PATCH 197/319] fix custom-shader writergate breakage Fixes: #9060 --- src/renderer/shadertoy.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index d31c36dee..b0a190a8b 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -80,9 +80,7 @@ pub fn loadFromFile( const file = try cwd.openFile(path, .{}); defer file.close(); - var buf: [4096]u8 = undefined; - var reader = file.reader(&buf); - break :src try reader.interface.readAlloc( + break :src try file.readToEndAlloc( alloc, 4 * 1024 * 1024, // 4MB ); From 725203d494c70d531eb75053a29072e2a3e119fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 20:35:45 -0700 Subject: [PATCH 198/319] lib-vt: split header to be more consumable --- AGENTS.md | 1 + Doxyfile | 5 +- include/ghostty/vt.h | 1147 +----------------------------- include/ghostty/vt/allocator.h | 196 +++++ include/ghostty/vt/key.h | 80 +++ include/ghostty/vt/key/encoder.h | 221 ++++++ include/ghostty/vt/key/event.h | 474 ++++++++++++ include/ghostty/vt/osc.h | 231 ++++++ include/ghostty/vt/result.h | 20 + src/build/GhosttyLibVt.zig | 7 +- 10 files changed, 1234 insertions(+), 1148 deletions(-) create mode 100644 include/ghostty/vt/allocator.h create mode 100644 include/ghostty/vt/key.h create mode 100644 include/ghostty/vt/key/encoder.h create mode 100644 include/ghostty/vt/key/event.h create mode 100644 include/ghostty/vt/osc.h create mode 100644 include/ghostty/vt/result.h diff --git a/AGENTS.md b/AGENTS.md index 14fff7b3d..afa0fd1f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,7 @@ A file for [guiding coding agents](https://agents.md/). - Test: `zig build test-lib-vt` - Test filter: `zig build test-lib-vt -Dtest-filter=` - When working on libghostty-vt, do not build the full app. +- For C only changes, don't run the Zig tests. Build all the examples. ## macOS App diff --git a/Doxyfile b/Doxyfile index 58b6be48a..63e73334d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -3,9 +3,10 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "libghostty" PROJECT_LOGO = images/gnome/64.png -INPUT = include/ghostty/vt.h +INPUT = include/ghostty INPUT_ENCODING = UTF-8 -RECURSIVE = NO +RECURSIVE = YES +FILE_PATTERNS = *.h EXAMPLE_PATH = example EXAMPLE_RECURSIVE = YES EXAMPLE_PATTERNS = * diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index bcbb01d00..489996530 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -57,1149 +57,10 @@ extern "C" { #endif -#include -#include -#include - -//------------------------------------------------------------------- -// Types - -/** - * Opaque handle to an OSC parser instance. - * - * This handle represents an OSC (Operating System Command) parser that can - * be used to parse the contents of OSC sequences. This isn't a full VT - * parser; it is only the OSC parser component. This is useful if you have - * a parser already and want to only extract and handle OSC sequences. - * - * @ingroup osc - */ -typedef struct GhosttyOscParser *GhosttyOscParser; - -/** - * Opaque handle to a single OSC command. - * - * This handle represents a parsed OSC (Operating System Command) command. - * The command can be queried for its type and associated data using - * `ghostty_osc_command_type` and `ghostty_osc_command_data`. - * - * @ingroup osc - */ -typedef struct GhosttyOscCommand *GhosttyOscCommand; - -/** - * Result codes for libghostty-vt operations. - */ -typedef enum { - /** Operation completed successfully */ - GHOSTTY_SUCCESS = 0, - /** Operation failed due to failed allocation */ - GHOSTTY_OUT_OF_MEMORY = -1, -} GhosttyResult; - -/** - * OSC command types. - * - * @ingroup osc - */ -typedef enum { - GHOSTTY_OSC_COMMAND_INVALID = 0, - GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, - GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, - GHOSTTY_OSC_COMMAND_PROMPT_START = 3, - GHOSTTY_OSC_COMMAND_PROMPT_END = 4, - GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5, - GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6, - GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7, - GHOSTTY_OSC_COMMAND_REPORT_PWD = 8, - GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9, - GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10, - GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11, - GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12, - GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13, - GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14, - GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15, - GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16, - GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17, - GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18, - GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19, - GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, -} GhosttyOscCommandType; - -/** - * OSC command data types. - * - * These values specify what type of data to extract from an OSC command - * using `ghostty_osc_command_data`. - * - * @ingroup osc - */ -typedef enum { - /** Invalid data type. Never results in any data extraction. */ - GHOSTTY_OSC_DATA_INVALID = 0, - - /** - * Window title string data. - * - * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE - * - * Output type: const char ** (pointer to null-terminated string) - * - * Lifetime: Valid until the next call to any ghostty_osc_* function with - * the same parser instance. Memory is owned by the parser. - */ - GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, -} GhosttyOscCommandData; - -//------------------------------------------------------------------- -// Allocator Interface - -/** @defgroup allocator Memory Management - * - * libghostty-vt does require memory allocation for various operations, - * but is resilient to allocation failures and will gracefully handle - * out-of-memory situations by returning error codes. - * - * The exact memory management semantics are documented in the relevant - * functions and data structures. - * - * libghostty-vt uses explicit memory allocation via an allocator - * interface provided by GhosttyAllocator. The interface is based on the - * [Zig](https://ziglang.org) allocator interface, since this has been - * shown to be a flexible and powerful interface in practice and enables - * a wide variety of allocation strategies. - * - * **For the common case, you can pass NULL as the allocator for any - * function that accepts one,** and libghostty will use a default allocator. - * The default allocator will be libc malloc/free if libc is linked. - * Otherwise, a custom allocator is used (currently Zig's SMP allocator) - * that doesn't require any external dependencies. - * - * ## Basic Usage - * - * For simple use cases, you can ignore this interface entirely by passing NULL - * as the allocator parameter to functions that accept one. This will use the - * default allocator (typically libc malloc/free, if libc is linked, but - * we provide our own default allocator if libc isn't linked). - * - * To use a custom allocator: - * 1. Implement the GhosttyAllocatorVtable function pointers - * 2. Create a GhosttyAllocator struct with your vtable and context - * 3. Pass the allocator to functions that accept one - * - * @{ - */ - -/** - * Function table for custom memory allocator operations. - * - * This vtable defines the interface for a custom memory allocator. All - * function pointers must be valid and non-NULL. - * - * @ingroup allocator - * - * If you're not going to use a custom allocator, you can ignore all of - * this. All functions that take an allocator pointer allow NULL to use a - * default allocator. - * - * The interface is based on the Zig allocator interface. I'll say up front - * that it is easy to look at this interface and think "wow, this is really - * overcomplicated". The reason for this complexity is well thought out by - * the Zig folks, and it enables a diverse set of allocation strategies - * as shown by the Zig ecosystem. As a consolation, please note that many - * of the arguments are only needed for advanced use cases and can be - * safely ignored in simple implementations. For example, if you look at - * the Zig implementation of the libc allocator in `lib/std/heap.zig` - * (search for CAllocator), you'll see it is very simple. - * - * We chose to align with the Zig allocator interface because: - * - * 1. It is a proven interface that serves a wide variety of use cases - * in the real world via the Zig ecosystem. It's shown to work. - * - * 2. Our core implementation itself is Zig, and this lets us very - * cheaply and easily convert between C and Zig allocators. - * - * NOTE(mitchellh): In the future, we can have default implementations of - * resize/remap and allow those to be null. - */ -typedef struct { - /** - * Return a pointer to `len` bytes with specified `alignment`, or return - * `NULL` indicating the allocation failed. - * - * @param ctx The allocator context - * @param len Number of bytes to allocate - * @param alignment Required alignment for the allocation. Guaranteed to - * be a power of two between 1 and 16 inclusive. - * @param ret_addr First return address of the allocation call stack (0 if not provided) - * @return Pointer to allocated memory, or NULL if allocation failed - */ - void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); - - /** - * Attempt to expand or shrink memory in place. - * - * `memory_len` must equal the length requested from the most recent - * successful call to `alloc`, `resize`, or `remap`. `alignment` must - * equal the same value that was passed as the `alignment` parameter to - * the original `alloc` call. - * - * `new_len` must be greater than zero. - * - * @param ctx The allocator context - * @param memory Pointer to the memory block to resize - * @param memory_len Current size of the memory block - * @param alignment Alignment (must match original allocation) - * @param new_len New requested size - * @param ret_addr First return address of the allocation call stack (0 if not provided) - * @return true if resize was successful in-place, false if relocation would be required - */ - bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); - - /** - * Attempt to expand or shrink memory, allowing relocation. - * - * `memory_len` must equal the length requested from the most recent - * successful call to `alloc`, `resize`, or `remap`. `alignment` must - * equal the same value that was passed as the `alignment` parameter to - * the original `alloc` call. - * - * A non-`NULL` return value indicates the resize was successful. The - * allocation may have same address, or may have been relocated. In either - * case, the allocation now has size of `new_len`. A `NULL` return value - * indicates that the resize would be equivalent to allocating new memory, - * copying the bytes from the old memory, and then freeing the old memory. - * In such case, it is more efficient for the caller to perform the copy. - * - * `new_len` must be greater than zero. - * - * @param ctx The allocator context - * @param memory Pointer to the memory block to remap - * @param memory_len Current size of the memory block - * @param alignment Alignment (must match original allocation) - * @param new_len New requested size - * @param ret_addr First return address of the allocation call stack (0 if not provided) - * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed - */ - void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); - - /** - * Free and invalidate a region of memory. - * - * `memory_len` must equal the length requested from the most recent - * successful call to `alloc`, `resize`, or `remap`. `alignment` must - * equal the same value that was passed as the `alignment` parameter to - * the original `alloc` call. - * - * @param ctx The allocator context - * @param memory Pointer to the memory block to free - * @param memory_len Size of the memory block - * @param alignment Alignment (must match original allocation) - * @param ret_addr First return address of the allocation call stack (0 if not provided) - */ - void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); -} GhosttyAllocatorVtable; - -/** - * Custom memory allocator. - * - * For functions that take an allocator pointer, a NULL pointer indicates - * that the default allocator should be used. The default allocator will - * be libc malloc/free if we're linking to libc. If libc isn't linked, - * a custom allocator is used (currently Zig's SMP allocator). - * - * @ingroup allocator - * - * Usage example: - * @code - * GhosttyAllocator allocator = { - * .vtable = &my_allocator_vtable, - * .ctx = my_allocator_state - * }; - * @endcode - */ -typedef struct { - /** - * Opaque context pointer passed to all vtable functions. - * This allows the allocator implementation to maintain state - * or reference external resources needed for memory management. - */ - void *ctx; - - /** - * Pointer to the allocator's vtable containing function pointers - * for memory operations (alloc, resize, remap, free). - */ - const GhosttyAllocatorVtable *vtable; -} GhosttyAllocator; - -/** @} */ // end of allocator group - -//------------------------------------------------------------------- -// Key Encoding - -/** @defgroup key Key Encoding - * - * Utilities for encoding key events into terminal escape sequences, - * supporting both legacy encoding as well as Kitty Keyboard Protocol. - * - * ## Basic Usage - * - * 1. Create an encoder instance with ghostty_key_encoder_new() - * 2. Configure encoder options with ghostty_key_encoder_setopt(). - * 3. For each key event: - * - Create a key event with ghostty_key_event_new() - * - Set event properties (action, key, modifiers, etc.) - * - Encode with ghostty_key_encoder_encode() - * - Free the event with ghostty_key_event_free() - * - Note: You can also reuse the same key event multiple times by - * changing its properties. - * 4. Free the encoder with ghostty_key_encoder_free() when done - * - * ## Example - * - * @code{.c} - * #include - * #include - * #include - * - * int main() { - * // Create encoder - * GhosttyKeyEncoder encoder; - * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); - * assert(result == GHOSTTY_SUCCESS); - * - * // Enable Kitty keyboard protocol with all features - * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, - * &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); - * - * // Create and configure key event for Ctrl+C press - * GhosttyKeyEvent event; - * result = ghostty_key_event_new(NULL, &event); - * assert(result == GHOSTTY_SUCCESS); - * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); - * ghostty_key_event_set_key(event, GHOSTTY_KEY_C); - * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); - * - * // Encode the key event - * char buf[128]; - * size_t written = 0; - * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); - * assert(result == GHOSTTY_SUCCESS); - * - * // Use the encoded sequence (e.g., write to terminal) - * fwrite(buf, 1, written, stdout); - * - * // Cleanup - * ghostty_key_event_free(event); - * ghostty_key_encoder_free(encoder); - * return 0; - * } - * @endcode - * - * For a complete working example, see example/c-vt-key-encode in the - * repository. - * - * @{ - */ - -/** - * Opaque handle to a key event. - * - * This handle represents a keyboard input event containing information about - * the physical key pressed, modifiers, and generated text. The event can be - * configured using the `ghostty_key_event_set_*` functions. - * - * @ingroup key - */ -typedef struct GhosttyKeyEvent *GhosttyKeyEvent; - -/** - * Keyboard input event types. - * - * @ingroup key - */ -typedef enum { - /** Key was released */ - GHOSTTY_KEY_ACTION_RELEASE = 0, - /** Key was pressed */ - GHOSTTY_KEY_ACTION_PRESS = 1, - /** Key is being repeated (held down) */ - GHOSTTY_KEY_ACTION_REPEAT = 2, -} GhosttyKeyAction; - -/** - * Keyboard modifier keys bitmask. - * - * A bitmask representing all keyboard modifiers. This tracks which modifier keys - * are pressed and, where supported by the platform, which side (left or right) - * of each modifier is active. - * - * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. - * - * Modifier side bits are only meaningful when the corresponding modifier bit is set. - * Not all platforms support distinguishing between left and right modifier - * keys and Ghostty is built to expect that some platforms may not provide this - * information. - * - * @ingroup key - */ -typedef uint16_t GhosttyMods; - -/** Shift key is pressed */ -#define GHOSTTY_MODS_SHIFT (1 << 0) -/** Control key is pressed */ -#define GHOSTTY_MODS_CTRL (1 << 1) -/** Alt/Option key is pressed */ -#define GHOSTTY_MODS_ALT (1 << 2) -/** Super/Command/Windows key is pressed */ -#define GHOSTTY_MODS_SUPER (1 << 3) -/** Caps Lock is active */ -#define GHOSTTY_MODS_CAPS_LOCK (1 << 4) -/** Num Lock is active */ -#define GHOSTTY_MODS_NUM_LOCK (1 << 5) - -/** - * Right shift is pressed (0 = left, 1 = right). - * Only meaningful when GHOSTTY_MODS_SHIFT is set. - */ -#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) -/** - * Right ctrl is pressed (0 = left, 1 = right). - * Only meaningful when GHOSTTY_MODS_CTRL is set. - */ -#define GHOSTTY_MODS_CTRL_SIDE (1 << 7) -/** - * Right alt is pressed (0 = left, 1 = right). - * Only meaningful when GHOSTTY_MODS_ALT is set. - */ -#define GHOSTTY_MODS_ALT_SIDE (1 << 8) -/** - * Right super is pressed (0 = left, 1 = right). - * Only meaningful when GHOSTTY_MODS_SUPER is set. - */ -#define GHOSTTY_MODS_SUPER_SIDE (1 << 9) - -/** - * Physical key codes. - * - * The set of key codes that Ghostty is aware of. These represent physical keys - * on the keyboard and are layout-independent. For example, the "a" key on a US - * keyboard is the same as the "ф" key on a Russian keyboard, but both will - * report the same key_a value. - * - * Layout-dependent strings are provided separately as UTF-8 text and are produced - * by the platform. These values are based on the W3C UI Events KeyboardEvent code - * standard. See: https://www.w3.org/TR/uievents-code - * - * @ingroup key - */ -typedef enum { - GHOSTTY_KEY_UNIDENTIFIED = 0, - - // Writing System Keys (W3C § 3.1.1) - GHOSTTY_KEY_BACKQUOTE, - GHOSTTY_KEY_BACKSLASH, - GHOSTTY_KEY_BRACKET_LEFT, - GHOSTTY_KEY_BRACKET_RIGHT, - GHOSTTY_KEY_COMMA, - GHOSTTY_KEY_DIGIT_0, - GHOSTTY_KEY_DIGIT_1, - GHOSTTY_KEY_DIGIT_2, - GHOSTTY_KEY_DIGIT_3, - GHOSTTY_KEY_DIGIT_4, - GHOSTTY_KEY_DIGIT_5, - GHOSTTY_KEY_DIGIT_6, - GHOSTTY_KEY_DIGIT_7, - GHOSTTY_KEY_DIGIT_8, - GHOSTTY_KEY_DIGIT_9, - GHOSTTY_KEY_EQUAL, - GHOSTTY_KEY_INTL_BACKSLASH, - GHOSTTY_KEY_INTL_RO, - GHOSTTY_KEY_INTL_YEN, - GHOSTTY_KEY_A, - GHOSTTY_KEY_B, - GHOSTTY_KEY_C, - GHOSTTY_KEY_D, - GHOSTTY_KEY_E, - GHOSTTY_KEY_F, - GHOSTTY_KEY_G, - GHOSTTY_KEY_H, - GHOSTTY_KEY_I, - GHOSTTY_KEY_J, - GHOSTTY_KEY_K, - GHOSTTY_KEY_L, - GHOSTTY_KEY_M, - GHOSTTY_KEY_N, - GHOSTTY_KEY_O, - GHOSTTY_KEY_P, - GHOSTTY_KEY_Q, - GHOSTTY_KEY_R, - GHOSTTY_KEY_S, - GHOSTTY_KEY_T, - GHOSTTY_KEY_U, - GHOSTTY_KEY_V, - GHOSTTY_KEY_W, - GHOSTTY_KEY_X, - GHOSTTY_KEY_Y, - GHOSTTY_KEY_Z, - GHOSTTY_KEY_MINUS, - GHOSTTY_KEY_PERIOD, - GHOSTTY_KEY_QUOTE, - GHOSTTY_KEY_SEMICOLON, - GHOSTTY_KEY_SLASH, - - // Functional Keys (W3C § 3.1.2) - GHOSTTY_KEY_ALT_LEFT, - GHOSTTY_KEY_ALT_RIGHT, - GHOSTTY_KEY_BACKSPACE, - GHOSTTY_KEY_CAPS_LOCK, - GHOSTTY_KEY_CONTEXT_MENU, - GHOSTTY_KEY_CONTROL_LEFT, - GHOSTTY_KEY_CONTROL_RIGHT, - GHOSTTY_KEY_ENTER, - GHOSTTY_KEY_META_LEFT, - GHOSTTY_KEY_META_RIGHT, - GHOSTTY_KEY_SHIFT_LEFT, - GHOSTTY_KEY_SHIFT_RIGHT, - GHOSTTY_KEY_SPACE, - GHOSTTY_KEY_TAB, - GHOSTTY_KEY_CONVERT, - GHOSTTY_KEY_KANA_MODE, - GHOSTTY_KEY_NON_CONVERT, - - // Control Pad Section (W3C § 3.2) - GHOSTTY_KEY_DELETE, - GHOSTTY_KEY_END, - GHOSTTY_KEY_HELP, - GHOSTTY_KEY_HOME, - GHOSTTY_KEY_INSERT, - GHOSTTY_KEY_PAGE_DOWN, - GHOSTTY_KEY_PAGE_UP, - - // Arrow Pad Section (W3C § 3.3) - GHOSTTY_KEY_ARROW_DOWN, - GHOSTTY_KEY_ARROW_LEFT, - GHOSTTY_KEY_ARROW_RIGHT, - GHOSTTY_KEY_ARROW_UP, - - // Numpad Section (W3C § 3.4) - GHOSTTY_KEY_NUM_LOCK, - GHOSTTY_KEY_NUMPAD_0, - GHOSTTY_KEY_NUMPAD_1, - GHOSTTY_KEY_NUMPAD_2, - GHOSTTY_KEY_NUMPAD_3, - GHOSTTY_KEY_NUMPAD_4, - GHOSTTY_KEY_NUMPAD_5, - GHOSTTY_KEY_NUMPAD_6, - GHOSTTY_KEY_NUMPAD_7, - GHOSTTY_KEY_NUMPAD_8, - GHOSTTY_KEY_NUMPAD_9, - GHOSTTY_KEY_NUMPAD_ADD, - GHOSTTY_KEY_NUMPAD_BACKSPACE, - GHOSTTY_KEY_NUMPAD_CLEAR, - GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, - GHOSTTY_KEY_NUMPAD_COMMA, - GHOSTTY_KEY_NUMPAD_DECIMAL, - GHOSTTY_KEY_NUMPAD_DIVIDE, - GHOSTTY_KEY_NUMPAD_ENTER, - GHOSTTY_KEY_NUMPAD_EQUAL, - GHOSTTY_KEY_NUMPAD_MEMORY_ADD, - GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, - GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, - GHOSTTY_KEY_NUMPAD_MEMORY_STORE, - GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, - GHOSTTY_KEY_NUMPAD_MULTIPLY, - GHOSTTY_KEY_NUMPAD_PAREN_LEFT, - GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, - GHOSTTY_KEY_NUMPAD_SUBTRACT, - GHOSTTY_KEY_NUMPAD_SEPARATOR, - GHOSTTY_KEY_NUMPAD_UP, - GHOSTTY_KEY_NUMPAD_DOWN, - GHOSTTY_KEY_NUMPAD_RIGHT, - GHOSTTY_KEY_NUMPAD_LEFT, - GHOSTTY_KEY_NUMPAD_BEGIN, - GHOSTTY_KEY_NUMPAD_HOME, - GHOSTTY_KEY_NUMPAD_END, - GHOSTTY_KEY_NUMPAD_INSERT, - GHOSTTY_KEY_NUMPAD_DELETE, - GHOSTTY_KEY_NUMPAD_PAGE_UP, - GHOSTTY_KEY_NUMPAD_PAGE_DOWN, - - // Function Section (W3C § 3.5) - GHOSTTY_KEY_ESCAPE, - GHOSTTY_KEY_F1, - GHOSTTY_KEY_F2, - GHOSTTY_KEY_F3, - GHOSTTY_KEY_F4, - GHOSTTY_KEY_F5, - GHOSTTY_KEY_F6, - GHOSTTY_KEY_F7, - GHOSTTY_KEY_F8, - GHOSTTY_KEY_F9, - GHOSTTY_KEY_F10, - GHOSTTY_KEY_F11, - GHOSTTY_KEY_F12, - GHOSTTY_KEY_F13, - GHOSTTY_KEY_F14, - GHOSTTY_KEY_F15, - GHOSTTY_KEY_F16, - GHOSTTY_KEY_F17, - GHOSTTY_KEY_F18, - GHOSTTY_KEY_F19, - GHOSTTY_KEY_F20, - GHOSTTY_KEY_F21, - GHOSTTY_KEY_F22, - GHOSTTY_KEY_F23, - GHOSTTY_KEY_F24, - GHOSTTY_KEY_F25, - GHOSTTY_KEY_FN, - GHOSTTY_KEY_FN_LOCK, - GHOSTTY_KEY_PRINT_SCREEN, - GHOSTTY_KEY_SCROLL_LOCK, - GHOSTTY_KEY_PAUSE, - - // Media Keys (W3C § 3.6) - GHOSTTY_KEY_BROWSER_BACK, - GHOSTTY_KEY_BROWSER_FAVORITES, - GHOSTTY_KEY_BROWSER_FORWARD, - GHOSTTY_KEY_BROWSER_HOME, - GHOSTTY_KEY_BROWSER_REFRESH, - GHOSTTY_KEY_BROWSER_SEARCH, - GHOSTTY_KEY_BROWSER_STOP, - GHOSTTY_KEY_EJECT, - GHOSTTY_KEY_LAUNCH_APP_1, - GHOSTTY_KEY_LAUNCH_APP_2, - GHOSTTY_KEY_LAUNCH_MAIL, - GHOSTTY_KEY_MEDIA_PLAY_PAUSE, - GHOSTTY_KEY_MEDIA_SELECT, - GHOSTTY_KEY_MEDIA_STOP, - GHOSTTY_KEY_MEDIA_TRACK_NEXT, - GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, - GHOSTTY_KEY_POWER, - GHOSTTY_KEY_SLEEP, - GHOSTTY_KEY_AUDIO_VOLUME_DOWN, - GHOSTTY_KEY_AUDIO_VOLUME_MUTE, - GHOSTTY_KEY_AUDIO_VOLUME_UP, - GHOSTTY_KEY_WAKE_UP, - - // Legacy, Non-standard, and Special Keys (W3C § 3.7) - GHOSTTY_KEY_COPY, - GHOSTTY_KEY_CUT, - GHOSTTY_KEY_PASTE, -} GhosttyKey; - -/** - * Kitty keyboard protocol flags. - * - * Bitflags representing the various modes of the Kitty keyboard protocol. - * These can be combined using bitwise OR operations. Valid values all - * start with `GHOSTTY_KITTY_KEY_`. - * - * @ingroup key - */ -typedef uint8_t GhosttyKittyKeyFlags; - -/** Kitty keyboard protocol disabled (all flags off) */ -#define GHOSTTY_KITTY_KEY_DISABLED 0 - -/** Disambiguate escape codes */ -#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) - -/** Report key press and release events */ -#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) - -/** Report alternate key codes */ -#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) - -/** Report all key events including those normally handled by the terminal */ -#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) - -/** Report associated text with key events */ -#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) - -/** All Kitty keyboard protocol flags enabled */ -#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) - -/** - * macOS option key behavior. - * - * Determines whether the "option" key on macOS is treated as "alt" or not. - * See the Ghostty `macos-option-as-alt` configuration option for more details. - * - * @ingroup key - */ -typedef enum { - /** Option key is not treated as alt */ - GHOSTTY_OPTION_AS_ALT_FALSE = 0, - /** Option key is treated as alt */ - GHOSTTY_OPTION_AS_ALT_TRUE = 1, - /** Only left option key is treated as alt */ - GHOSTTY_OPTION_AS_ALT_LEFT = 2, - /** Only right option key is treated as alt */ - GHOSTTY_OPTION_AS_ALT_RIGHT = 3, -} GhosttyOptionAsAlt; - -/** - * Create a new key event instance. - * - * Creates a new key event with default values. The event must be freed using - * ghostty_key_event_free() when no longer needed. - * - * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator - * @param event Pointer to store the created key event handle - * @return GHOSTTY_SUCCESS on success, or an error code on failure - * - * @ingroup key - */ -GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); - -/** - * Free a key event instance. - * - * Releases all resources associated with the key event. After this call, - * the event handle becomes invalid and must not be used. - * - * @param event The key event handle to free (may be NULL) - * - * @ingroup key - */ -void ghostty_key_event_free(GhosttyKeyEvent event); - -/** - * Set the key action (press, release, repeat). - * - * @param event The key event handle, must not be NULL - * @param action The action to set - * - * @ingroup key - */ -void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); - -/** - * Get the key action (press, release, repeat). - * - * @param event The key event handle, must not be NULL - * @return The key action - * - * @ingroup key - */ -GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); - -/** - * Set the physical key code. - * - * @param event The key event handle, must not be NULL - * @param key The physical key code to set - * - * @ingroup key - */ -void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); - -/** - * Get the physical key code. - * - * @param event The key event handle, must not be NULL - * @return The physical key code - * - * @ingroup key - */ -GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); - -/** - * Set the modifier keys bitmask. - * - * @param event The key event handle, must not be NULL - * @param mods The modifier keys bitmask to set - * - * @ingroup key - */ -void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); - -/** - * Get the modifier keys bitmask. - * - * @param event The key event handle, must not be NULL - * @return The modifier keys bitmask - * - * @ingroup key - */ -GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); - -/** - * Set the consumed modifiers bitmask. - * - * @param event The key event handle, must not be NULL - * @param consumed_mods The consumed modifiers bitmask to set - * - * @ingroup key - */ -void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); - -/** - * Get the consumed modifiers bitmask. - * - * @param event The key event handle, must not be NULL - * @return The consumed modifiers bitmask - * - * @ingroup key - */ -GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); - -/** - * Set whether the key event is part of a composition sequence. - * - * @param event The key event handle, must not be NULL - * @param composing Whether the key event is part of a composition sequence - * - * @ingroup key - */ -void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); - -/** - * Get whether the key event is part of a composition sequence. - * - * @param event The key event handle, must not be NULL - * @return Whether the key event is part of a composition sequence - * - * @ingroup key - */ -bool ghostty_key_event_get_composing(GhosttyKeyEvent event); - -/** - * Set the UTF-8 text generated by the key event. - * - * The key event does NOT take ownership of the text pointer. The caller - * must ensure the string remains valid for the lifetime needed by the event. - * - * @param event The key event handle, must not be NULL - * @param utf8 The UTF-8 text to set (or NULL for empty) - * @param len Length of the UTF-8 text in bytes - * - * @ingroup key - */ -void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); - -/** - * Get the UTF-8 text generated by the key event. - * - * The returned pointer is valid until the event is freed or the UTF-8 text is modified. - * - * @param event The key event handle, must not be NULL - * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) - * @return The UTF-8 text (or NULL for empty) - * - * @ingroup key - */ -const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); - -/** - * Set the unshifted Unicode codepoint. - * - * @param event The key event handle, must not be NULL - * @param codepoint The unshifted Unicode codepoint to set - * - * @ingroup key - */ -void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); - -/** - * Get the unshifted Unicode codepoint. - * - * @param event The key event handle, must not be NULL - * @return The unshifted Unicode codepoint - * - * @ingroup key - */ -uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); - -/** - * Opaque handle to a key encoder instance. - * - * This handle represents a key encoder that converts key events into terminal - * escape sequences. The encoder supports both legacy encoding and the Kitty - * Keyboard Protocol, depending on the options set. - * - * @ingroup key - */ -typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; - -/** - * Key encoder option identifiers. - * - * These values are used with ghostty_key_encoder_setopt() to configure - * the behavior of the key encoder. - * - * @ingroup key - */ -typedef enum { - /** Terminal DEC mode 1: cursor key application mode (value: bool) */ - GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, - - /** Terminal DEC mode 66: keypad key application mode (value: bool) */ - GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, - - /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ - GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, - - /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ - GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, - - /** xterm modifyOtherKeys mode 2 (value: bool) */ - GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, - - /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ - GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, - - /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ - GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, -} GhosttyKeyEncoderOption; - -/** - * Create a new key encoder instance. - * - * Creates a new key encoder with default options. The encoder can be configured - * using ghostty_key_encoder_setopt() and must be freed using - * ghostty_key_encoder_free() when no longer needed. - * - * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator - * @param encoder Pointer to store the created encoder handle - * @return GHOSTTY_SUCCESS on success, or an error code on failure - * - * @ingroup key - */ -GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); - -/** - * Free a key encoder instance. - * - * Releases all resources associated with the key encoder. After this call, - * the encoder handle becomes invalid and must not be used. - * - * @param encoder The encoder handle to free (may be NULL) - * - * @ingroup key - */ -void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); - -/** - * Set an option on the key encoder. - * - * Configures the behavior of the key encoder. Options control various aspects - * of encoding such as terminal modes (cursor key application mode, keypad mode), - * protocol selection (Kitty keyboard protocol flags), and platform-specific - * behaviors (macOS option-as-alt). - * - * A null pointer value does nothing. It does not reset the value to the - * default. The setopt call will do nothing. - * - * @param encoder The encoder handle, must not be NULL - * @param option The option to set - * @param value Pointer to the value to set (type depends on the option) - * - * @ingroup key - */ -void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); - -/** - * Encode a key event into a terminal escape sequence. - * - * Converts a key event into the appropriate terminal escape sequence based on - * the encoder's current options. The sequence is written to the provided buffer. - * - * Not all key events produce output. For example, unmodified modifier keys - * typically don't generate escape sequences. Check the out_len parameter to - * determine if any data was written. - * - * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY - * and out_len will contain the required buffer size. The caller can then - * allocate a larger buffer and call the function again. - * - * @param encoder The encoder handle, must not be NULL - * @param event The key event to encode, must not be NULL - * @param out_buf Buffer to write the encoded sequence to - * @param out_buf_size Size of the output buffer in bytes - * @param out_len Pointer to store the number of bytes written (may be NULL) - * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code - * - * ## Example: Calculate required buffer size - * - * @code{.c} - * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) - * size_t required = 0; - * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); - * assert(result == GHOSTTY_OUT_OF_MEMORY); - * - * // Allocate buffer of required size - * char *buf = malloc(required); - * - * // Encode with properly sized buffer - * size_t written = 0; - * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); - * assert(result == GHOSTTY_SUCCESS); - * - * // Use the encoded sequence... - * - * free(buf); - * @endcode - * - * ## Example: Direct encoding with static buffer - * - * @code{.c} - * // Most escape sequences are short, so a static buffer often suffices - * char buf[128]; - * size_t written = 0; - * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); - * - * if (result == GHOSTTY_SUCCESS) { - * // Write the encoded sequence to the terminal - * write(pty_fd, buf, written); - * } else if (result == GHOSTTY_OUT_OF_MEMORY) { - * // Buffer too small, written contains required size - * char *dynamic_buf = malloc(written); - * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); - * assert(result == GHOSTTY_SUCCESS); - * write(pty_fd, dynamic_buf, written); - * free(dynamic_buf); - * } - * @endcode - * - * @ingroup key - */ -GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); - -/** @} */ // end of key group - -//------------------------------------------------------------------- -// OSC Parser - -/** @defgroup osc OSC Parser - * - * OSC (Operating System Command) sequence parser and command handling. - * - * The parser operates in a streaming fashion, processing input byte-by-byte - * to handle OSC sequences that may arrive in fragments across multiple reads. - * This interface makes it easy to integrate into most environments and avoids - * over-allocating buffers. - * - * ## Basic Usage - * - * 1. Create a parser instance with ghostty_osc_new() - * 2. Feed bytes to the parser using ghostty_osc_next() - * 3. Finalize parsing with ghostty_osc_end() to get the command - * 4. Query command type and extract data using ghostty_osc_command_type() - * and ghostty_osc_command_data() - * 5. Free the parser with ghostty_osc_free() when done - * - * @{ - */ - -/** - * Create a new OSC parser instance. - * - * Creates a new OSC (Operating System Command) parser using the provided - * allocator. The parser must be freed using ghostty_vt_osc_free() when - * no longer needed. - * - * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator - * @param parser Pointer to store the created parser handle - * @return GHOSTTY_SUCCESS on success, or an error code on failure - */ -GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); - -/** - * Free an OSC parser instance. - * - * Releases all resources associated with the OSC parser. After this call, - * the parser handle becomes invalid and must not be used. - * - * @param parser The parser handle to free (may be NULL) - */ -void ghostty_osc_free(GhosttyOscParser parser); - -/** - * Reset an OSC parser instance to its initial state. - * - * Resets the parser state, clearing any partially parsed OSC sequences - * and returning the parser to its initial state. This is useful for - * reusing a parser instance or recovering from parse errors. - * - * @param parser The parser handle to reset, must not be null. - */ -void ghostty_osc_reset(GhosttyOscParser parser); - -/** - * Parse the next byte in an OSC sequence. - * - * Processes a single byte as part of an OSC sequence. The parser maintains - * internal state to track the progress through the sequence. Call this - * function for each byte in the sequence data. - * - * When finished pumping the parser with bytes, call ghostty_osc_end - * to get the final result. - * - * @param parser The parser handle, must not be null. - * @param byte The next byte to parse - */ -void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); - -/** - * Finalize OSC parsing and retrieve the parsed command. - * - * Call this function after feeding all bytes of an OSC sequence to the parser - * using ghostty_osc_next() with the exception of the terminating character - * (ESC or ST). This function finalizes the parsing process and returns the - * parsed OSC command. - * - * The return value is never NULL. Invalid commands will return a command - * with type GHOSTTY_OSC_COMMAND_INVALID. - * - * The terminator parameter specifies the byte that terminated the OSC sequence - * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is - * preserved in the parsed command so that responses can use the same terminator - * format for better compatibility with the calling program. For commands that - * do not require a response, this parameter is ignored and the resulting - * command will not retain the terminator information. - * - * The returned command handle is valid until the next call to any - * `ghostty_osc_*` function with the same parser instance with the exception - * of command introspection functions such as `ghostty_osc_command_type`. - * - * @param parser The parser handle, must not be null. - * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) - * @return Handle to the parsed OSC command - */ -GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); - -/** - * Get the type of an OSC command. - * - * Returns the type identifier for the given OSC command. This can be used - * to determine what kind of command was parsed and what data might be - * available from it. - * - * @param command The OSC command handle to query (may be NULL) - * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL - */ -GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); - -/** - * Extract data from an OSC command. - * - * Extracts typed data from the given OSC command based on the specified - * data type. The output pointer must be of the appropriate type for the - * requested data kind. Valid command types, output types, and memory - * safety information are documented in the `GhosttyOscCommandData` enum. - * - * @param command The OSC command handle to query (may be NULL) - * @param data The type of data to extract - * @param out Pointer to store the extracted data (type depends on data parameter) - * @return true if data extraction was successful, false otherwise - */ -bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); - -/** @} */ // end of osc group +#include +#include +#include +#include #ifdef __cplusplus } diff --git a/include/ghostty/vt/allocator.h b/include/ghostty/vt/allocator.h new file mode 100644 index 000000000..4cebe91bb --- /dev/null +++ b/include/ghostty/vt/allocator.h @@ -0,0 +1,196 @@ +/** + * @file allocator.h + * + * Memory management interface for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_ALLOCATOR_H +#define GHOSTTY_VT_ALLOCATOR_H + +#include +#include +#include + +/** @defgroup allocator Memory Management + * + * libghostty-vt does require memory allocation for various operations, + * but is resilient to allocation failures and will gracefully handle + * out-of-memory situations by returning error codes. + * + * The exact memory management semantics are documented in the relevant + * functions and data structures. + * + * libghostty-vt uses explicit memory allocation via an allocator + * interface provided by GhosttyAllocator. The interface is based on the + * [Zig](https://ziglang.org) allocator interface, since this has been + * shown to be a flexible and powerful interface in practice and enables + * a wide variety of allocation strategies. + * + * **For the common case, you can pass NULL as the allocator for any + * function that accepts one,** and libghostty will use a default allocator. + * The default allocator will be libc malloc/free if libc is linked. + * Otherwise, a custom allocator is used (currently Zig's SMP allocator) + * that doesn't require any external dependencies. + * + * ## Basic Usage + * + * For simple use cases, you can ignore this interface entirely by passing NULL + * as the allocator parameter to functions that accept one. This will use the + * default allocator (typically libc malloc/free, if libc is linked, but + * we provide our own default allocator if libc isn't linked). + * + * To use a custom allocator: + * 1. Implement the GhosttyAllocatorVtable function pointers + * 2. Create a GhosttyAllocator struct with your vtable and context + * 3. Pass the allocator to functions that accept one + * + * @{ + */ + +/** + * Function table for custom memory allocator operations. + * + * This vtable defines the interface for a custom memory allocator. All + * function pointers must be valid and non-NULL. + * + * @ingroup allocator + * + * If you're not going to use a custom allocator, you can ignore all of + * this. All functions that take an allocator pointer allow NULL to use a + * default allocator. + * + * The interface is based on the Zig allocator interface. I'll say up front + * that it is easy to look at this interface and think "wow, this is really + * overcomplicated". The reason for this complexity is well thought out by + * the Zig folks, and it enables a diverse set of allocation strategies + * as shown by the Zig ecosystem. As a consolation, please note that many + * of the arguments are only needed for advanced use cases and can be + * safely ignored in simple implementations. For example, if you look at + * the Zig implementation of the libc allocator in `lib/std/heap.zig` + * (search for CAllocator), you'll see it is very simple. + * + * We chose to align with the Zig allocator interface because: + * + * 1. It is a proven interface that serves a wide variety of use cases + * in the real world via the Zig ecosystem. It's shown to work. + * + * 2. Our core implementation itself is Zig, and this lets us very + * cheaply and easily convert between C and Zig allocators. + * + * NOTE(mitchellh): In the future, we can have default implementations of + * resize/remap and allow those to be null. + */ +typedef struct { + /** + * Return a pointer to `len` bytes with specified `alignment`, or return + * `NULL` indicating the allocation failed. + * + * @param ctx The allocator context + * @param len Number of bytes to allocate + * @param alignment Required alignment for the allocation. Guaranteed to + * be a power of two between 1 and 16 inclusive. + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to allocated memory, or NULL if allocation failed + */ + void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory in place. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to resize + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return true if resize was successful in-place, false if relocation would be required + */ + bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory, allowing relocation. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * A non-`NULL` return value indicates the resize was successful. The + * allocation may have same address, or may have been relocated. In either + * case, the allocation now has size of `new_len`. A `NULL` return value + * indicates that the resize would be equivalent to allocating new memory, + * copying the bytes from the old memory, and then freeing the old memory. + * In such case, it is more efficient for the caller to perform the copy. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to remap + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed + */ + void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Free and invalidate a region of memory. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to free + * @param memory_len Size of the memory block + * @param alignment Alignment (must match original allocation) + * @param ret_addr First return address of the allocation call stack (0 if not provided) + */ + void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); +} GhosttyAllocatorVtable; + +/** + * Custom memory allocator. + * + * For functions that take an allocator pointer, a NULL pointer indicates + * that the default allocator should be used. The default allocator will + * be libc malloc/free if we're linking to libc. If libc isn't linked, + * a custom allocator is used (currently Zig's SMP allocator). + * + * @ingroup allocator + * + * Usage example: + * @code + * GhosttyAllocator allocator = { + * .vtable = &my_allocator_vtable, + * .ctx = my_allocator_state + * }; + * @endcode + */ +typedef struct GhosttyAllocator { + /** + * Opaque context pointer passed to all vtable functions. + * This allows the allocator implementation to maintain state + * or reference external resources needed for memory management. + */ + void *ctx; + + /** + * Pointer to the allocator's vtable containing function pointers + * for memory operations (alloc, resize, remap, free). + */ + const GhosttyAllocatorVtable *vtable; +} GhosttyAllocator; + +/** @} */ + +#endif /* GHOSTTY_VT_ALLOCATOR_H */ diff --git a/include/ghostty/vt/key.h b/include/ghostty/vt/key.h new file mode 100644 index 000000000..772b5d43b --- /dev/null +++ b/include/ghostty/vt/key.h @@ -0,0 +1,80 @@ +/** + * @file key.h + * + * Key encoding module - encode key events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_H +#define GHOSTTY_VT_KEY_H + +/** @defgroup key Key Encoding + * + * Utilities for encoding key events into terminal escape sequences, + * supporting both legacy encoding as well as Kitty Keyboard Protocol. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_key_encoder_new() + * 2. Configure encoder options with ghostty_key_encoder_setopt(). + * 3. For each key event: + * - Create a key event with ghostty_key_event_new() + * - Set event properties (action, key, modifiers, etc.) + * - Encode with ghostty_key_encoder_encode() + * - Free the event with ghostty_key_event_free() + * - Note: You can also reuse the same key event multiple times by + * changing its properties. + * 4. Free the encoder with ghostty_key_encoder_free() when done + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create encoder + * GhosttyKeyEncoder encoder; + * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + * assert(result == GHOSTTY_SUCCESS); + * + * // Enable Kitty keyboard protocol with all features + * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, + * &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + * + * // Create and configure key event for Ctrl+C press + * GhosttyKeyEvent event; + * result = ghostty_key_event_new(NULL, &event); + * assert(result == GHOSTTY_SUCCESS); + * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); + * ghostty_key_event_set_key(event, GHOSTTY_KEY_C); + * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + * + * // Encode the key event + * char buf[128]; + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence (e.g., write to terminal) + * fwrite(buf, 1, written, stdout); + * + * // Cleanup + * ghostty_key_event_free(event); + * ghostty_key_encoder_free(encoder); + * return 0; + * } + * @endcode + * + * For a complete working example, see example/c-vt-key-encode in the + * repository. + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_KEY_H */ diff --git a/include/ghostty/vt/key/encoder.h b/include/ghostty/vt/key/encoder.h new file mode 100644 index 000000000..766a29427 --- /dev/null +++ b/include/ghostty/vt/key/encoder.h @@ -0,0 +1,221 @@ +/** + * @file encoder.h + * + * Key event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_ENCODER_H +#define GHOSTTY_VT_KEY_ENCODER_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key encoder instance. + * + * This handle represents a key encoder that converts key events into terminal + * escape sequences. + * + * @ingroup key + */ +typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; + +/** + * Kitty keyboard protocol flags. + * + * Bitflags representing the various modes of the Kitty keyboard protocol. + * These can be combined using bitwise OR operations. Valid values all + * start with `GHOSTTY_KITTY_KEY_`. + * + * @ingroup key + */ +typedef uint8_t GhosttyKittyKeyFlags; + +/** Kitty keyboard protocol disabled (all flags off) */ +#define GHOSTTY_KITTY_KEY_DISABLED 0 + +/** Disambiguate escape codes */ +#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) + +/** Report key press and release events */ +#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) + +/** Report alternate key codes */ +#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) + +/** Report all key events including those normally handled by the terminal */ +#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) + +/** Report associated text with key events */ +#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) + +/** All Kitty keyboard protocol flags enabled */ +#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) + +/** + * macOS option key behavior. + * + * Determines whether the "option" key on macOS is treated as "alt" or not. + * See the Ghostty `macos-option-as-alt` configuration option for more details. + * + * @ingroup key + */ +typedef enum { + /** Option key is not treated as alt */ + GHOSTTY_OPTION_AS_ALT_FALSE = 0, + /** Option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_TRUE = 1, + /** Only left option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_LEFT = 2, + /** Only right option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_RIGHT = 3, +} GhosttyOptionAsAlt; + +/** + * Key encoder option identifiers. + * + * These values are used with ghostty_key_encoder_setopt() to configure + * the behavior of the key encoder. + * + * @ingroup key + */ +typedef enum { + /** Terminal DEC mode 1: cursor key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, + + /** Terminal DEC mode 66: keypad key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, + + /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, + + /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, + + /** xterm modifyOtherKeys mode 2 (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, + + /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ + GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, + + /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ + GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, +} GhosttyKeyEncoderOption; + +/** + * Create a new key encoder instance. + * + * Creates a new key encoder with default options. The encoder can be configured + * using ghostty_key_encoder_setopt() and must be freed using + * ghostty_key_encoder_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); + +/** + * Free a key encoder instance. + * + * Releases all resources associated with the key encoder. After this call, + * the encoder handle becomes invalid and must not be used. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); + +/** + * Set an option on the key encoder. + * + * Configures the behavior of the key encoder. Options control various aspects + * of encoding such as terminal modes (cursor key application mode, keypad mode), + * protocol selection (Kitty keyboard protocol flags), and platform-specific + * behaviors (macOS option-as-alt). + * + * A null pointer value does nothing. It does not reset the value to the + * default. The setopt call will do nothing. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option) + * + * @ingroup key + */ +void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); + +/** + * Encode a key event into a terminal escape sequence. + * + * Converts a key event into the appropriate terminal escape sequence based on + * the encoder's current options. The sequence is written to the provided buffer. + * + * Not all key events produce output. For example, unmodified modifier keys + * typically don't generate escape sequences. Check the out_len parameter to + * determine if any data was written. + * + * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY + * and out_len will contain the required buffer size. The caller can then + * allocate a larger buffer and call the function again. + * + * @param encoder The encoder handle, must not be NULL + * @param event The key event to encode, must not be NULL + * @param out_buf Buffer to write the encoded sequence to + * @param out_buf_size Size of the output buffer in bytes + * @param out_len Pointer to store the number of bytes written (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code + * + * ## Example: Calculate required buffer size + * + * @code{.c} + * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) + * size_t required = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + * assert(result == GHOSTTY_OUT_OF_MEMORY); + * + * // Allocate buffer of required size + * char *buf = malloc(required); + * + * // Encode with properly sized buffer + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence... + * + * free(buf); + * @endcode + * + * ## Example: Direct encoding with static buffer + * + * @code{.c} + * // Most escape sequences are short, so a static buffer often suffices + * char buf[128]; + * size_t written = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * if (result == GHOSTTY_SUCCESS) { + * // Write the encoded sequence to the terminal + * write(pty_fd, buf, written); + * } else if (result == GHOSTTY_OUT_OF_MEMORY) { + * // Buffer too small, written contains required size + * char *dynamic_buf = malloc(written); + * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); + * assert(result == GHOSTTY_SUCCESS); + * write(pty_fd, dynamic_buf, written); + * free(dynamic_buf); + * } + * @endcode + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); + +#endif /* GHOSTTY_VT_KEY_ENCODER_H */ diff --git a/include/ghostty/vt/key/event.h b/include/ghostty/vt/key/event.h new file mode 100644 index 000000000..dbd2e9f84 --- /dev/null +++ b/include/ghostty/vt/key/event.h @@ -0,0 +1,474 @@ +/** + * @file event.h + * + * Key event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_KEY_EVENT_H +#define GHOSTTY_VT_KEY_EVENT_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key event. + * + * This handle represents a keyboard input event containing information about + * the physical key pressed, modifiers, and generated text. + * + * @ingroup key + */ +typedef struct GhosttyKeyEvent *GhosttyKeyEvent; + +/** + * Keyboard input event types. + * + * @ingroup key + */ +typedef enum { + /** Key was released */ + GHOSTTY_KEY_ACTION_RELEASE = 0, + /** Key was pressed */ + GHOSTTY_KEY_ACTION_PRESS = 1, + /** Key is being repeated (held down) */ + GHOSTTY_KEY_ACTION_REPEAT = 2, +} GhosttyKeyAction; + +/** + * Keyboard modifier keys bitmask. + * + * A bitmask representing all keyboard modifiers. This tracks which modifier keys + * are pressed and, where supported by the platform, which side (left or right) + * of each modifier is active. + * + * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. + * + * Modifier side bits are only meaningful when the corresponding modifier bit is set. + * Not all platforms support distinguishing between left and right modifier + * keys and Ghostty is built to expect that some platforms may not provide this + * information. + * + * @ingroup key + */ +typedef uint16_t GhosttyMods; + +/** Shift key is pressed */ +#define GHOSTTY_MODS_SHIFT (1 << 0) +/** Control key is pressed */ +#define GHOSTTY_MODS_CTRL (1 << 1) +/** Alt/Option key is pressed */ +#define GHOSTTY_MODS_ALT (1 << 2) +/** Super/Command/Windows key is pressed */ +#define GHOSTTY_MODS_SUPER (1 << 3) +/** Caps Lock is active */ +#define GHOSTTY_MODS_CAPS_LOCK (1 << 4) +/** Num Lock is active */ +#define GHOSTTY_MODS_NUM_LOCK (1 << 5) + +/** + * Right shift is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SHIFT is set. + */ +#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) +/** + * Right ctrl is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_CTRL is set. + */ +#define GHOSTTY_MODS_CTRL_SIDE (1 << 7) +/** + * Right alt is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_ALT is set. + */ +#define GHOSTTY_MODS_ALT_SIDE (1 << 8) +/** + * Right super is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SUPER is set. + */ +#define GHOSTTY_MODS_SUPER_SIDE (1 << 9) + +/** + * Physical key codes. + * + * The set of key codes that Ghostty is aware of. These represent physical keys + * on the keyboard and are layout-independent. For example, the "a" key on a US + * keyboard is the same as the "ф" key on a Russian keyboard, but both will + * report the same key_a value. + * + * Layout-dependent strings are provided separately as UTF-8 text and are produced + * by the platform. These values are based on the W3C UI Events KeyboardEvent code + * standard. See: https://www.w3.org/TR/uievents-code + * + * @ingroup key + */ +typedef enum { + GHOSTTY_KEY_UNIDENTIFIED = 0, + + // Writing System Keys (W3C § 3.1.1) + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, + + // Functional Keys (W3C § 3.1.2) + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, + + // Control Pad Section (W3C § 3.2) + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // Arrow Pad Section (W3C § 3.3) + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // Numpad Section (W3C § 3.4) + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // Function Section (W3C § 3.5) + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, + + // Media Keys (W3C § 3.6) + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, + + // Legacy, Non-standard, and Special Keys (W3C § 3.7) + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, +} GhosttyKey; + +/** + * Create a new key event instance. + * + * Creates a new key event with default values. The event must be freed using + * ghostty_key_event_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param event Pointer to store the created key event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); + +/** + * Free a key event instance. + * + * Releases all resources associated with the key event. After this call, + * the event handle becomes invalid and must not be used. + * + * @param event The key event handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_event_free(GhosttyKeyEvent event); + +/** + * Set the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @param action The action to set + * + * @ingroup key + */ +void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); + +/** + * Get the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @return The key action + * + * @ingroup key + */ +GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); + +/** + * Set the physical key code. + * + * @param event The key event handle, must not be NULL + * @param key The physical key code to set + * + * @ingroup key + */ +void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); + +/** + * Get the physical key code. + * + * @param event The key event handle, must not be NULL + * @return The physical key code + * + * @ingroup key + */ +GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); + +/** + * Set the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @param mods The modifier keys bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); + +/** + * Get the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @return The modifier keys bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); + +/** + * Set the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @param consumed_mods The consumed modifiers bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); + +/** + * Get the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @return The consumed modifiers bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); + +/** + * Set whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @param composing Whether the key event is part of a composition sequence + * + * @ingroup key + */ +void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); + +/** + * Get whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @return Whether the key event is part of a composition sequence + * + * @ingroup key + */ +bool ghostty_key_event_get_composing(GhosttyKeyEvent event); + +/** + * Set the UTF-8 text generated by the key event. + * + * The key event does NOT take ownership of the text pointer. The caller + * must ensure the string remains valid for the lifetime needed by the event. + * + * @param event The key event handle, must not be NULL + * @param utf8 The UTF-8 text to set (or NULL for empty) + * @param len Length of the UTF-8 text in bytes + * + * @ingroup key + */ +void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); + +/** + * Get the UTF-8 text generated by the key event. + * + * The returned pointer is valid until the event is freed or the UTF-8 text is modified. + * + * @param event The key event handle, must not be NULL + * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) + * @return The UTF-8 text (or NULL for empty) + * + * @ingroup key + */ +const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); + +/** + * Set the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @param codepoint The unshifted Unicode codepoint to set + * + * @ingroup key + */ +void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); + +/** + * Get the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @return The unshifted Unicode codepoint + * + * @ingroup key + */ +uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); + +#endif /* GHOSTTY_VT_KEY_EVENT_H */ diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h new file mode 100644 index 000000000..7e2c8f322 --- /dev/null +++ b/include/ghostty/vt/osc.h @@ -0,0 +1,231 @@ +/** + * @file osc.h + * + * OSC (Operating System Command) sequence parser and command handling. + */ + +#ifndef GHOSTTY_VT_OSC_H +#define GHOSTTY_VT_OSC_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to an OSC parser instance. + * + * This handle represents an OSC (Operating System Command) parser that can + * be used to parse the contents of OSC sequences. + * + * @ingroup osc + */ +typedef struct GhosttyOscParser *GhosttyOscParser; + +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data. + * + * @ingroup osc + */ +typedef struct GhosttyOscCommand *GhosttyOscCommand; + +/** @defgroup osc OSC Parser + * + * OSC (Operating System Command) sequence parser and command handling. + * + * The parser operates in a streaming fashion, processing input byte-by-byte + * to handle OSC sequences that may arrive in fragments across multiple reads. + * This interface makes it easy to integrate into most environments and avoids + * over-allocating buffers. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_osc_new() + * 2. Feed bytes to the parser using ghostty_osc_next() + * 3. Finalize parsing with ghostty_osc_end() to get the command + * 4. Query command type and extract data using ghostty_osc_command_type() + * and ghostty_osc_command_data() + * 5. Free the parser with ghostty_osc_free() when done + * + * @{ + */ + +/** + * OSC command types. + * + * @ingroup osc + */ +typedef enum { + GHOSTTY_OSC_COMMAND_INVALID = 0, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, + GHOSTTY_OSC_COMMAND_PROMPT_START = 3, + GHOSTTY_OSC_COMMAND_PROMPT_END = 4, + GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5, + GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 8, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, +} GhosttyOscCommandType; + +/** + * OSC command data types. + * + * These values specify what type of data to extract from an OSC command + * using `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_OSC_DATA_INVALID = 0, + + /** + * Window title string data. + * + * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE + * + * Output type: const char ** (pointer to null-terminated string) + * + * Lifetime: Valid until the next call to any ghostty_osc_* function with + * the same parser instance. Memory is owned by the parser. + */ + GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, +} GhosttyOscCommandData; + +/** + * Create a new OSC parser instance. + * + * Creates a new OSC (Operating System Command) parser using the provided + * allocator. The parser must be freed using ghostty_vt_osc_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup osc + */ +GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); + +/** + * Free an OSC parser instance. + * + * Releases all resources associated with the OSC parser. After this call, + * the parser handle becomes invalid and must not be used. + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup osc + */ +void ghostty_osc_free(GhosttyOscParser parser); + +/** + * Reset an OSC parser instance to its initial state. + * + * Resets the parser state, clearing any partially parsed OSC sequences + * and returning the parser to its initial state. This is useful for + * reusing a parser instance or recovering from parse errors. + * + * @param parser The parser handle to reset, must not be null. + * + * @ingroup osc + */ +void ghostty_osc_reset(GhosttyOscParser parser); + +/** + * Parse the next byte in an OSC sequence. + * + * Processes a single byte as part of an OSC sequence. The parser maintains + * internal state to track the progress through the sequence. Call this + * function for each byte in the sequence data. + * + * When finished pumping the parser with bytes, call ghostty_osc_end + * to get the final result. + * + * @param parser The parser handle, must not be null. + * @param byte The next byte to parse + * + * @ingroup osc + */ +void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); + +/** + * Finalize OSC parsing and retrieve the parsed command. + * + * Call this function after feeding all bytes of an OSC sequence to the parser + * using ghostty_osc_next() with the exception of the terminating character + * (ESC or ST). This function finalizes the parsing process and returns the + * parsed OSC command. + * + * The return value is never NULL. Invalid commands will return a command + * with type GHOSTTY_OSC_COMMAND_INVALID. + * + * The terminator parameter specifies the byte that terminated the OSC sequence + * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is + * preserved in the parsed command so that responses can use the same terminator + * format for better compatibility with the calling program. For commands that + * do not require a response, this parameter is ignored and the resulting + * command will not retain the terminator information. + * + * The returned command handle is valid until the next call to any + * `ghostty_osc_*` function with the same parser instance with the exception + * of command introspection functions such as `ghostty_osc_command_type`. + * + * @param parser The parser handle, must not be null. + * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) + * @return Handle to the parsed OSC command + * + * @ingroup osc + */ +GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); + +/** + * Get the type of an OSC command. + * + * Returns the type identifier for the given OSC command. This can be used + * to determine what kind of command was parsed and what data might be + * available from it. + * + * @param command The OSC command handle to query (may be NULL) + * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL + * + * @ingroup osc + */ +GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); + +/** + * Extract data from an OSC command. + * + * Extracts typed data from the given OSC command based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid command types, output types, and memory + * safety information are documented in the `GhosttyOscCommandData` enum. + * + * @param command The OSC command handle to query (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return true if data extraction was successful, false otherwise + * + * @ingroup osc + */ +bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); + +/** @} */ + +#endif /* GHOSTTY_VT_OSC_H */ diff --git a/include/ghostty/vt/result.h b/include/ghostty/vt/result.h new file mode 100644 index 000000000..cc382eade --- /dev/null +++ b/include/ghostty/vt/result.h @@ -0,0 +1,20 @@ +/** + * @file result.h + * + * Result codes for libghostty-vt operations. + */ + +#ifndef GHOSTTY_VT_RESULT_H +#define GHOSTTY_VT_RESULT_H + +/** + * Result codes for libghostty-vt operations. + */ +typedef enum { + /** Operation completed successfully */ + GHOSTTY_SUCCESS = 0, + /** Operation failed due to failed allocation */ + GHOSTTY_OUT_OF_MEMORY = -1, +} GhosttyResult; + +#endif /* GHOSTTY_VT_RESULT_H */ diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 1e57da7b1..590792ef3 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -30,9 +30,10 @@ pub fn initShared( .root_module = zig.vt_c, .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, }); - lib.installHeader( - b.path("include/ghostty/vt.h"), - "ghostty/vt.h", + lib.installHeadersDirectory( + b.path("include/ghostty"), + "ghostty", + .{ .include_extensions = &.{".h"} }, ); // Get our debug symbols From bf9f025aec78aedcb9431d503fd6c4b14f579fbb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Oct 2025 21:04:18 -0700 Subject: [PATCH 199/319] lib-vt: begin paste utilities exports starting with safe paste --- .github/workflows/test.yml | 2 +- example/c-vt-paste/README.md | 17 ++++++++ example/c-vt-paste/build.zig | 42 ++++++++++++++++++ example/c-vt-paste/build.zig.zon | 24 ++++++++++ example/c-vt-paste/src/main.c | 31 +++++++++++++ include/ghostty/vt.h | 8 ++++ include/ghostty/vt/paste.h | 75 ++++++++++++++++++++++++++++++++ src/lib_vt.zig | 1 + src/terminal/c/main.zig | 4 ++ src/terminal/c/paste.zig | 36 +++++++++++++++ 10 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 example/c-vt-paste/README.md create mode 100644 example/c-vt-paste/build.zig create mode 100644 example/c-vt-paste/build.zig.zon create mode 100644 example/c-vt-paste/src/main.c create mode 100644 include/ghostty/vt/paste.h create mode 100644 src/terminal/c/paste.zig diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59556f58e..f78855290 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,7 +94,7 @@ jobs: strategy: fail-fast: false matrix: - dir: [c-vt, zig-vt] + dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt] name: Example ${{ matrix.dir }} runs-on: namespace-profile-ghostty-sm needs: test diff --git a/example/c-vt-paste/README.md b/example/c-vt-paste/README.md new file mode 100644 index 000000000..0f911771f --- /dev/null +++ b/example/c-vt-paste/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` Paste Safety Check + +This contains a simple example of how to use the `ghostty-vt` paste +utilities to check if paste data is safe. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-paste/build.zig b/example/c-vt-paste/build.zig new file mode 100644 index 000000000..99b7ba771 --- /dev/null +++ b/example/c-vt-paste/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_paste", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-paste/build.zig.zon b/example/c-vt-paste/build.zig.zon new file mode 100644 index 000000000..fb78db9bc --- /dev/null +++ b/example/c-vt-paste/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_paste, + .version = "0.0.0", + .fingerprint = 0xa105002abbc8cf74, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-paste/src/main.c b/example/c-vt-paste/src/main.c new file mode 100644 index 000000000..153861ca9 --- /dev/null +++ b/example/c-vt-paste/src/main.c @@ -0,0 +1,31 @@ +#include +#include +#include + +int main() { + // Test safe paste data + const char *safe_data = "hello world"; + if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + printf("'%s' is safe to paste\n", safe_data); + } + + // Test unsafe paste data with newline + const char *unsafe_newline = "rm -rf /\n"; + if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) { + printf("'%s' is UNSAFE - contains newline\n", unsafe_newline); + } + + // Test unsafe paste data with bracketed paste end sequence + const char *unsafe_escape = "evil\x1b[201~code"; + if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) { + printf("Data with escape sequence is UNSAFE\n"); + } + + // Test empty data + const char *empty_data = ""; + if (ghostty_paste_is_safe(empty_data, 0)) { + printf("Empty data is safe\n"); + } + + return 0; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 489996530..cd357f0fa 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -30,6 +30,7 @@ * The API is organized into the following groups: * - @ref key "Key Encoding" - Encode key events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref paste "Paste Utilities" - Validate paste data safety * - @ref allocator "Memory Management" - Memory management and custom allocators * * @section examples_sec Examples @@ -37,6 +38,7 @@ * Complete working examples: * - @ref c-vt/src/main.c - OSC parser example * - @ref c-vt-key-encode/src/main.c - Key encoding example + * - @ref c-vt-paste/src/main.c - Paste safety check example * */ @@ -50,6 +52,11 @@ * into terminal escape sequences using the Kitty keyboard protocol. */ +/** @example c-vt-paste/src/main.c + * This example demonstrates how to use the paste utilities to check if + * paste data is safe before sending it to the terminal. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -61,6 +68,7 @@ extern "C" { #include #include #include +#include #ifdef __cplusplus } diff --git a/include/ghostty/vt/paste.h b/include/ghostty/vt/paste.h new file mode 100644 index 000000000..d90f303d4 --- /dev/null +++ b/include/ghostty/vt/paste.h @@ -0,0 +1,75 @@ +/** + * @file paste.h + * + * Paste utilities - validate and encode paste data for terminal input. + */ + +#ifndef GHOSTTY_VT_PASTE_H +#define GHOSTTY_VT_PASTE_H + +/** @defgroup paste Paste Utilities + * + * Utilities for validating paste data safety. + * + * ## Basic Usage + * + * Use ghostty_paste_is_safe() to check if paste data contains potentially + * dangerous sequences before sending it to the terminal. + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * const char* safe_data = "hello world"; + * const char* unsafe_data = "rm -rf /\n"; + * + * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + * printf("Safe to paste\n"); + * } + * + * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { + * printf("Unsafe! Contains newline\n"); + * } + * + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Check if paste data is safe to paste into the terminal. + * + * Data is considered unsafe if it contains: + * - Newlines (`\n`) which can inject commands + * - The bracketed paste end sequence (`\x1b[201~`) which can be used + * to exit bracketed paste mode and inject commands + * + * This check is conservative and considers data unsafe regardless of + * current terminal state. + * + * @param data The paste data to check (must not be NULL) + * @param len The length of the data in bytes + * @return true if the data is safe to paste, false otherwise + */ +bool ghostty_paste_is_safe(const char* data, size_t len); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_PASTE_H */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 73a030333..1df8330ea 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -122,6 +122,7 @@ comptime { @export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" }); @export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" }); @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); + @export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); } } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 500dbf56c..f68333d9b 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,6 +1,7 @@ pub const osc = @import("osc.zig"); pub const key_event = @import("key_event.zig"); pub const key_encode = @import("key_encode.zig"); +pub const paste = @import("paste.zig"); // The full C API, unexported. pub const osc_new = osc.new; @@ -33,10 +34,13 @@ pub const key_encoder_free = key_encode.free; pub const key_encoder_setopt = key_encode.setopt; pub const key_encoder_encode = key_encode.encode; +pub const paste_is_safe = paste.is_safe; + test { _ = osc; _ = key_event; _ = key_encode; + _ = paste; // We want to make sure we run the tests for the C allocator interface. _ = @import("../../lib/allocator.zig"); diff --git a/src/terminal/c/paste.zig b/src/terminal/c/paste.zig new file mode 100644 index 000000000..eb4117a70 --- /dev/null +++ b/src/terminal/c/paste.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const paste = @import("../../input/paste.zig"); + +pub fn is_safe(data: ?[*]const u8, len: usize) callconv(.c) bool { + const slice: []const u8 = if (data) |v| v[0..len] else &.{}; + return paste.isSafe(slice); +} + +test "is_safe with safe data" { + const testing = std.testing; + const safe = "hello world"; + try testing.expect(is_safe(safe.ptr, safe.len)); +} + +test "is_safe with newline" { + const testing = std.testing; + const unsafe = "hello\nworld"; + try testing.expect(!is_safe(unsafe.ptr, unsafe.len)); +} + +test "is_safe with bracketed paste end" { + const testing = std.testing; + const unsafe = "hello\x1b[201~world"; + try testing.expect(!is_safe(unsafe.ptr, unsafe.len)); +} + +test "is_safe with empty data" { + const testing = std.testing; + const empty = ""; + try testing.expect(is_safe(empty.ptr, 0)); +} + +test "is_safe with null empty data" { + const testing = std.testing; + try testing.expect(is_safe(null, 0)); +} From e70ae28fa34523bd3a14c634da08d21a0975ec1f Mon Sep 17 00:00:00 2001 From: Ravi Chandra Date: Tue, 7 Oct 2025 18:14:10 +1300 Subject: [PATCH 200/319] terminal: add semi-colon character to word boundary list --- src/terminal/Screen.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index a98407af7..228b87922 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2565,6 +2565,7 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { '`', '|', ':', + ';', ',', '(', ')', @@ -7819,6 +7820,7 @@ test "Screen: selectWord with character boundary" { " `abc` \n123", " |abc| \n123", " :abc: \n123", + " ;abc; \n123", " ,abc, \n123", " (abc( \n123", " )abc) \n123", From b56808f138876281f2fe09b83eb71b5c67258058 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:07:23 +0000 Subject: [PATCH 201/319] build(deps): bump softprops/action-gh-release from 2.3.4 to 2.4.0 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.4 to 2.4.0. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/62c96d0c4e8a889135c1f3a25910db8dbe0e85f7...aec2ec56f94eb8180ceec724245f64ef008b89f5) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index eca678244..d3ea88def 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -188,7 +188,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -359,7 +359,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -590,7 +590,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -775,7 +775,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From 09ba5a27a234bfe5c8cad1c51da7b5028f239f1b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 08:43:39 -0700 Subject: [PATCH 202/319] macOS: Unobtrusive update views --- macos/Sources/App/macOS/AppDelegate.swift | 30 +- .../Window Styles/TerminalWindow.swift | 140 ++++++- .../Sources/Features/Update/UpdateBadge.swift | 65 ++++ .../Sources/Features/Update/UpdatePill.swift | 51 +++ .../Features/Update/UpdatePopoverView.swift | 362 ++++++++++++++++++ .../Features/Update/UpdateViewModel.swift | 178 +++++++++ 6 files changed, 814 insertions(+), 12 deletions(-) create mode 100644 macos/Sources/Features/Update/UpdateBadge.swift create mode 100644 macos/Sources/Features/Update/UpdatePill.swift create mode 100644 macos/Sources/Features/Update/UpdatePopoverView.swift create mode 100644 macos/Sources/Features/Update/UpdateViewModel.swift diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 942aecdd4..a893c3877 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1,4 +1,5 @@ import AppKit +import SwiftUI import UserNotifications import OSLog import Sparkle @@ -1004,7 +1005,34 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - updaterController.checkForUpdates(sender) + // Demo mode: simulate update check instead of real Sparkle check + // TODO: Replace with real updaterController.checkForUpdates(sender) when SPUUserDriver is implemented + + guard let terminalWindow = NSApp.keyWindow as? TerminalWindow else { + // Fallback to real update check if no terminal window + updaterController.checkForUpdates(sender) + return + } + + let model = terminalWindow.updateUIModel + + // Simulate the full update check flow + model.state = .checking + model.progress = nil + model.details = nil + model.error = nil + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + // Simulate finding an update + model.state = .updateAvailable + model.details = .init( + version: "1.2.0", + build: "demo", + size: "42 MB", + date: Date(), + notesSummary: "This is a demo of the update UI. New features and bug fixes would be listed here." + ) + } } @IBAction func newWindow(_ sender: Any?) { diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 3ab6293dc..248577f4f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -14,6 +14,10 @@ class TerminalWindow: NSWindow { /// Reset split zoom button in titlebar private let resetZoomAccessory = NSTitlebarAccessoryViewController() + + /// Update notification UI in titlebar + private let updateAccessory = NSTitlebarAccessoryViewController() + private(set) var updateUIModel = UpdateViewModel() /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() @@ -85,6 +89,16 @@ class TerminalWindow: NSWindow { })) addTitlebarAccessoryViewController(resetZoomAccessory) resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false + + // Create update notification accessory + updateAccessory.layoutAttribute = .right + updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( + viewModel: viewModel, + model: updateUIModel, + actions: createUpdateActions() + )) + addTitlebarAccessoryViewController(updateAccessory) + updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false } // Setup the accessory view for tabs that shows our keyboard shortcuts, @@ -198,6 +212,9 @@ class TerminalWindow: NSWindow { if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) { removeTitlebarAccessoryViewController(at: idx) } + + // We don't need to do this with the update accessory. I don't know why but + // everything works fine. } private func tabBarDidDisappear() { @@ -436,6 +453,94 @@ class TerminalWindow: NSWindow { standardWindowButton(.miniaturizeButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true } + + // MARK: Update UI + + private func createUpdateActions() -> UpdateUIActions { + UpdateUIActions( + allowAutoChecks: { [weak self] in + print("Demo: Allow auto checks") + self?.updateUIModel.state = .idle + }, + denyAutoChecks: { [weak self] in + print("Demo: Deny auto checks") + self?.updateUIModel.state = .idle + }, + cancel: { [weak self] in + print("Demo: Cancel") + self?.updateUIModel.state = .idle + }, + install: { [weak self] in + guard let self else { return } + print("Demo: Install - simulating download and install flow") + + // Start downloading + self.updateUIModel.state = .downloading + self.updateUIModel.progress = 0.0 + + // Simulate download progress + for i in 1...10 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + self.updateUIModel.progress = Double(i) / 10.0 + + if i == 10 { + // Move to extraction + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.updateUIModel.state = .extracting + self.updateUIModel.progress = 0.0 + + // Simulate extraction progress + for j in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { + self.updateUIModel.progress = Double(j) / 5.0 + + if j == 5 { + // Move to ready to install + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.updateUIModel.state = .readyToInstall + self.updateUIModel.progress = nil + } + } + } + } + } + } + } + } + }, + remindLater: { [weak self] in + print("Demo: Remind later") + self?.updateUIModel.state = .idle + }, + skipThisVersion: { [weak self] in + print("Demo: Skip version") + self?.updateUIModel.state = .idle + }, + showReleaseNotes: { [weak self] in + print("Demo: Show release notes") + guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return } + NSWorkspace.shared.open(url) + }, + retry: { [weak self] in + guard let self else { return } + print("Demo: Retry - simulating update check") + self.updateUIModel.state = .checking + self.updateUIModel.progress = nil + self.updateUIModel.error = nil + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.updateUIModel.state = .updateAvailable + self.updateUIModel.details = .init( + version: "1.2.0", + build: "demo", + size: "42 MB", + date: Date(), + notesSummary: "This is a demo of the update UI." + ) + } + } + ) + } // MARK: Config @@ -467,21 +572,20 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false + + /// Calculates the top padding based on toolbar visibility and macOS version + fileprivate var accessoryTopPadding: CGFloat { + if #available(macOS 26.0, *) { + return hasToolbar ? 10 : 5 + } else { + return hasToolbar ? 9 : 4 + } + } } struct ResetZoomAccessoryView: View { @ObservedObject var viewModel: ViewModel let action: () -> Void - - // The padding from the top that the view appears. This was all just manually - // measured based on the OS. - var topPadding: CGFloat { - if #available(macOS 26.0, *) { - return viewModel.hasToolbar ? 10 : 5 - } else { - return viewModel.hasToolbar ? 9 : 4 - } - } var body: some View { if viewModel.isSurfaceZoomed { @@ -497,10 +601,24 @@ extension TerminalWindow { } // With a toolbar, the window title is taller, so we need more padding // to properly align. - .padding(.top, topPadding) + .padding(.top, viewModel.accessoryTopPadding) // We always need space at the end of the titlebar .padding(.trailing, 10) } } } + + /// A pill-shaped button that displays update status and provides access to update actions. + struct UpdateAccessoryView: View { + @ObservedObject var viewModel: ViewModel + @ObservedObject var model: UpdateViewModel + let actions: UpdateUIActions + + var body: some View { + UpdatePill(model: model, actions: actions) + .padding(.top, viewModel.accessoryTopPadding) + .padding(.trailing, 10) + } + } + } diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift new file mode 100644 index 000000000..a6ffe6cb6 --- /dev/null +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -0,0 +1,65 @@ +import SwiftUI + +/// A badge view that displays the current state of an update operation. +/// +/// Shows different visual indicators based on the update state: +/// - Progress ring for downloading/extracting with progress +/// - Animated rotating icon for checking/installing +/// - Static icon for other states +struct UpdateBadge: View { + /// The update view model that provides the current state and progress + @ObservedObject var model: UpdateViewModel + + /// Current rotation angle for animated icon states + @State private var rotationAngle: Double = 0 + + var body: some View { + switch model.state { + case .downloading, .extracting: + if let progress = model.progress { + ProgressRingView(progress: progress) + } else { + Image(systemName: "arrow.down.circle") + } + + case .checking, .installing: + Image(systemName: model.iconName) + .rotationEffect(.degrees(rotationAngle)) + .onAppear { + withAnimation(.linear(duration: 2.5).repeatForever(autoreverses: false)) { + rotationAngle = 360 + } + } + .onDisappear { + rotationAngle = 0 + } + + default: + Image(systemName: model.iconName) + } + } +} + +/// A circular progress indicator with a stroke-based ring design. +/// +/// Displays a partially filled circle that represents progress from 0.0 to 1.0. +fileprivate struct ProgressRingView: View { + /// The current progress value, ranging from 0.0 (empty) to 1.0 (complete) + let progress: Double + + /// The width of the progress ring stroke + let lineWidth: CGFloat = 2 + + var body: some View { + ZStack { + Circle() + .stroke(Color.primary.opacity(0.2), lineWidth: lineWidth) + + Circle() + .trim(from: 0, to: progress) + .stroke(Color.primary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeInOut(duration: 0.2), value: progress) + } + } +} diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift new file mode 100644 index 000000000..604be0fbc --- /dev/null +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -0,0 +1,51 @@ +import SwiftUI + +/// A pill-shaped button that displays update status and provides access to update actions. +struct UpdatePill: View { + /// The update view model that provides the current state and information + @ObservedObject var model: UpdateViewModel + + /// The actions that can be performed on updates + let actions: UpdateUIActions + + /// Whether the update popover is currently visible + @State private var showPopover = false + + var body: some View { + if model.state != .idle { + VStack { + pillButton + Spacer() + } + .popover(isPresented: $showPopover, arrowEdge: .bottom) { + UpdatePopoverView(model: model, actions: actions) + } + .transition(.opacity.combined(with: .scale(scale: 0.95))) + } + } + + /// The pill-shaped button view that displays the update badge and text + @ViewBuilder + private var pillButton: some View { + Button(action: { showPopover.toggle() }) { + HStack(spacing: 6) { + UpdateBadge(model: model) + .frame(width: 14, height: 14) + + Text(model.text) + .font(.system(size: 11, weight: .medium)) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(model.backgroundColor) + ) + .foregroundColor(model.foregroundColor) + .contentShape(Capsule()) + } + .buttonStyle(.plain) + .help(model.stateTooltip) + } +} diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift new file mode 100644 index 000000000..af870b4de --- /dev/null +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -0,0 +1,362 @@ +import SwiftUI + +/// A popover view that displays detailed update information and action buttons. +/// +/// The view adapts its content based on the current update state, showing appropriate +/// UI for checking, downloading, installing, or handling errors. +struct UpdatePopoverView: View { + /// The update view model that provides the current state and information + @ObservedObject var model: UpdateViewModel + + /// The actions that can be performed on updates + let actions: UpdateUIActions + + /// Environment value for dismissing the popover + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + switch model.state { + case .idle: + EmptyView() + + case .permissionRequest: + permissionRequestView + + case .checking: + checkingView + + case .updateAvailable: + updateAvailableView + + case .downloading: + downloadingView + + case .extracting: + extractingView + + case .readyToInstall: + readyToInstallView + + case .installing: + installingView + + case .notFound: + notFoundView + + case .error: + errorView + } + } + .frame(width: 300) + } + + /// View shown when requesting permission to enable automatic updates + private var permissionRequestView: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Enable automatic updates?") + .font(.system(size: 13, weight: .semibold)) + + Text("Ghostty can automatically check for and download updates in the background.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack(spacing: 8) { + Button("Not Now") { + actions.denyAutoChecks() + dismiss() + } + .keyboardShortcut(.cancelAction) + + Spacer() + + Button("Allow") { + actions.allowAutoChecks() + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + } + } + .padding(16) + } + + /// View shown while checking for updates + private var checkingView: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 10) { + ProgressView() + .controlSize(.small) + Text("Checking for updates…") + .font(.system(size: 13)) + } + + HStack { + Spacer() + Button("Cancel") { + actions.cancel() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + } + } + .padding(16) + } + + /// View shown when an update is available, displaying version and size information + private var updateAvailableView: some View { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + Text("Update Available") + .font(.system(size: 13, weight: .semibold)) + + if let details = model.details { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Text("Version:") + .foregroundColor(.secondary) + .frame(width: 50, alignment: .trailing) + Text(details.version) + } + .font(.system(size: 11)) + + if let size = details.size { + HStack(spacing: 6) { + Text("Size:") + .foregroundColor(.secondary) + .frame(width: 50, alignment: .trailing) + Text(size) + } + .font(.system(size: 11)) + } + } + } + } + + HStack(spacing: 8) { + Button("Skip") { + actions.skipThisVersion() + dismiss() + } + .controlSize(.small) + + Button("Later") { + actions.remindLater() + dismiss() + } + .controlSize(.small) + .keyboardShortcut(.cancelAction) + + Spacer() + + Button("Install") { + actions.install() + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .padding(16) + + if model.details?.notesSummary != nil { + Divider() + + Button(action: actions.showReleaseNotes) { + HStack { + Text("View Release Notes") + .font(.system(size: 11)) + Spacer() + Image(systemName: "arrow.up.right.square") + .font(.system(size: 11)) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(Color(nsColor: .controlBackgroundColor)) + } + } + } + + /// View shown while downloading an update, with progress indicator + private var downloadingView: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Downloading Update") + .font(.system(size: 13, weight: .semibold)) + + if let progress = model.progress { + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: progress) + Text(String(format: "%.0f%%", progress * 100)) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } else { + ProgressView() + .controlSize(.small) + } + } + + HStack { + Spacer() + Button("Cancel") { + actions.cancel() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + } + } + .padding(16) + } + + /// View shown while extracting/preparing the downloaded update + private var extractingView: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Preparing Update") + .font(.system(size: 13, weight: .semibold)) + + if let progress = model.progress { + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: progress) + Text(String(format: "%.0f%%", progress * 100)) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } else { + ProgressView() + .controlSize(.small) + } + } + .padding(16) + } + + /// View shown when an update is ready to be installed + private var readyToInstallView: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Ready to Install") + .font(.system(size: 13, weight: .semibold)) + + if let details = model.details { + Text("Version \(details.version) is ready to install.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + + HStack(spacing: 8) { + Button("Later") { + actions.remindLater() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + + Spacer() + + Button("Install and Relaunch") { + actions.install() + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .padding(16) + } + + /// View shown during the installation process + private var installingView: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 10) { + ProgressView() + .controlSize(.small) + Text("Installing…") + .font(.system(size: 13, weight: .semibold)) + } + + Text("The application will relaunch shortly.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + .padding(16) + } + + /// View shown when no updates are found (already on latest version) + private var notFoundView: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("No Updates Found") + .font(.system(size: 13, weight: .semibold)) + + Text("You're already running the latest version.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack { + Spacer() + Button("OK") { + actions.remindLater() + dismiss() + } + .keyboardShortcut(.defaultAction) + .controlSize(.small) + } + } + .padding(16) + } + + /// View shown when an error occurs during the update process + private var errorView: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + .font(.system(size: 13)) + Text(model.error?.title ?? "Update Failed") + .font(.system(size: 13, weight: .semibold)) + } + + if let message = model.error?.message { + Text(message) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + + HStack(spacing: 8) { + Button("OK") { + actions.remindLater() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + + Spacer() + + Button("Retry") { + actions.retry() + dismiss() + } + .keyboardShortcut(.defaultAction) + .controlSize(.small) + } + } + .padding(16) + } +} diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift new file mode 100644 index 000000000..fb477324c --- /dev/null +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -0,0 +1,178 @@ +import Foundation +import SwiftUI + +struct UpdateUIActions { + let allowAutoChecks: () -> Void + let denyAutoChecks: () -> Void + let cancel: () -> Void + let install: () -> Void + let remindLater: () -> Void + let skipThisVersion: () -> Void + let showReleaseNotes: () -> Void + let retry: () -> Void +} + +class UpdateViewModel: ObservableObject { + @Published var state: State = .idle + @Published var progress: Double? = nil + @Published var details: Details? = nil + @Published var error: ErrorInfo? = nil + + enum State: Equatable { + case idle + case permissionRequest + case checking + case updateAvailable + case downloading + case extracting + case readyToInstall + case installing + case notFound + case error + } + + struct ErrorInfo: Equatable { + let title: String + let message: String + } + + struct Details: Equatable { + let version: String + let build: String? + let size: String? + let date: Date? + let notesSummary: String? + } + + var stateTooltip: String { + switch state { + case .idle: + return "" + case .permissionRequest: + return "Update permission required" + case .checking: + return "Checking for updates…" + case .updateAvailable: + if let details { + return "Update available: \(details.version)" + } + return "Update available" + case .downloading: + if let progress { + return String(format: "Downloading %.0f%%…", progress * 100) + } + return "Downloading…" + case .extracting: + if let progress { + return String(format: "Preparing %.0f%%…", progress * 100) + } + return "Preparing…" + case .readyToInstall: + return "Ready to install" + case .installing: + return "Installing…" + case .notFound: + return "No updates found" + case .error: + return error?.title ?? "Update failed" + } + } + + var text: String { + switch state { + case .idle: + return "" + case .permissionRequest: + return "Update Permission" + case .checking: + return "Checking for Updates…" + case .updateAvailable: + if let details { + return "Update Available: \(details.version)" + } + return "Update Available" + case .downloading: + if let progress { + return String(format: "Downloading: %.0f%%", progress * 100) + } + return "Downloading…" + case .extracting: + if let progress { + return String(format: "Preparing: %.0f%%", progress * 100) + } + return "Preparing…" + case .readyToInstall: + return "Install Update" + case .installing: + return "Installing…" + case .notFound: + return "No Updates Available" + case .error: + return error?.title ?? "Update Failed" + } + } + + var iconName: String { + switch state { + case .idle: + return "" + case .permissionRequest: + return "questionmark.circle" + case .checking: + return "arrow.triangle.2.circlepath" + case .updateAvailable: + return "arrow.down.circle.fill" + case .downloading, .extracting: + return "" // Progress ring instead + case .readyToInstall: + return "checkmark.circle.fill" + case .installing: + return "gear" + case .notFound: + return "info.circle" + case .error: + return "exclamationmark.triangle.fill" + } + } + + var iconColor: Color { + switch state { + case .idle: + return .secondary + case .permissionRequest, .checking: + return .secondary + case .updateAvailable, .readyToInstall: + return .accentColor + case .downloading, .extracting, .installing: + return .secondary + case .notFound: + return .secondary + case .error: + return .orange + } + } + + var backgroundColor: Color { + switch state { + case .updateAvailable: + return .accentColor + case .readyToInstall: + return Color(nsColor: NSColor.systemGreen.blended(withFraction: 0.3, of: .black) ?? .systemGreen) + case .error: + return .orange.opacity(0.2) + default: + return Color(nsColor: .controlBackgroundColor) + } + } + + var foregroundColor: Color { + switch state { + case .updateAvailable, .readyToInstall: + return .white + case .error: + return .orange + default: + return .primary + } + } +} From fc347a6040b5e626f630c19afce69f947318e4f9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 12:40:42 -0700 Subject: [PATCH 203/319] macOS: Move update view model over to App scope --- macos/Sources/App/macOS/AppDelegate.swift | 23 +++--- .../Window Styles/TerminalWindow.swift | 72 +++++++++++-------- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index a893c3877..898191b1a 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -101,6 +101,9 @@ class AppDelegate: NSObject, /// Manages updates let updaterController: SPUStandardUpdaterController let updaterDelegate: UpdaterDelegate = UpdaterDelegate() + + /// Update view model for UI display + @Published private(set) var updateUIModel = UpdateViewModel() /// The elapsed time since the process was started var timeSinceLaunch: TimeInterval { @@ -1008,24 +1011,16 @@ class AppDelegate: NSObject, // Demo mode: simulate update check instead of real Sparkle check // TODO: Replace with real updaterController.checkForUpdates(sender) when SPUUserDriver is implemented - guard let terminalWindow = NSApp.keyWindow as? TerminalWindow else { - // Fallback to real update check if no terminal window - updaterController.checkForUpdates(sender) - return - } - - let model = terminalWindow.updateUIModel - // Simulate the full update check flow - model.state = .checking - model.progress = nil - model.details = nil - model.error = nil + updateUIModel.state = .checking + updateUIModel.progress = nil + updateUIModel.details = nil + updateUIModel.error = nil DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { // Simulate finding an update - model.state = .updateAvailable - model.details = .init( + self.updateUIModel.state = .updateAvailable + self.updateUIModel.details = .init( version: "1.2.0", build: "demo", size: "42 MB", diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 248577f4f..87f2be1ca 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -17,7 +17,6 @@ class TerminalWindow: NSWindow { /// Update notification UI in titlebar private let updateAccessory = NSTitlebarAccessoryViewController() - private(set) var updateUIModel = UpdateViewModel() /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() @@ -94,7 +93,7 @@ class TerminalWindow: NSWindow { updateAccessory.layoutAttribute = .right updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( viewModel: viewModel, - model: updateUIModel, + model: appDelegate.updateUIModel, actions: createUpdateActions() )) addTitlebarAccessoryViewController(updateAccessory) @@ -457,48 +456,60 @@ class TerminalWindow: NSWindow { // MARK: Update UI private func createUpdateActions() -> UpdateUIActions { - UpdateUIActions( - allowAutoChecks: { [weak self] in + guard let appDelegate = NSApp.delegate as? AppDelegate else { + return UpdateUIActions( + allowAutoChecks: {}, + denyAutoChecks: {}, + cancel: {}, + install: {}, + remindLater: {}, + skipThisVersion: {}, + showReleaseNotes: {}, + retry: {} + ) + } + + return UpdateUIActions( + allowAutoChecks: { print("Demo: Allow auto checks") - self?.updateUIModel.state = .idle + appDelegate.updateUIModel.state = .idle }, - denyAutoChecks: { [weak self] in + denyAutoChecks: { print("Demo: Deny auto checks") - self?.updateUIModel.state = .idle + appDelegate.updateUIModel.state = .idle }, - cancel: { [weak self] in + cancel: { print("Demo: Cancel") - self?.updateUIModel.state = .idle + appDelegate.updateUIModel.state = .idle }, - install: { [weak self] in - guard let self else { return } + install: { print("Demo: Install - simulating download and install flow") // Start downloading - self.updateUIModel.state = .downloading - self.updateUIModel.progress = 0.0 + appDelegate.updateUIModel.state = .downloading + appDelegate.updateUIModel.progress = 0.0 // Simulate download progress for i in 1...10 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { - self.updateUIModel.progress = Double(i) / 10.0 + appDelegate.updateUIModel.progress = Double(i) / 10.0 if i == 10 { // Move to extraction DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.updateUIModel.state = .extracting - self.updateUIModel.progress = 0.0 + appDelegate.updateUIModel.state = .extracting + appDelegate.updateUIModel.progress = 0.0 // Simulate extraction progress for j in 1...5 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { - self.updateUIModel.progress = Double(j) / 5.0 + appDelegate.updateUIModel.progress = Double(j) / 5.0 if j == 5 { // Move to ready to install DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.updateUIModel.state = .readyToInstall - self.updateUIModel.progress = nil + appDelegate.updateUIModel.state = .readyToInstall + appDelegate.updateUIModel.progress = nil } } } @@ -508,29 +519,28 @@ class TerminalWindow: NSWindow { } } }, - remindLater: { [weak self] in + remindLater: { print("Demo: Remind later") - self?.updateUIModel.state = .idle + appDelegate.updateUIModel.state = .idle }, - skipThisVersion: { [weak self] in + skipThisVersion: { print("Demo: Skip version") - self?.updateUIModel.state = .idle + appDelegate.updateUIModel.state = .idle }, - showReleaseNotes: { [weak self] in + showReleaseNotes: { print("Demo: Show release notes") guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return } NSWorkspace.shared.open(url) }, - retry: { [weak self] in - guard let self else { return } + retry: { print("Demo: Retry - simulating update check") - self.updateUIModel.state = .checking - self.updateUIModel.progress = nil - self.updateUIModel.error = nil + appDelegate.updateUIModel.state = .checking + appDelegate.updateUIModel.progress = nil + appDelegate.updateUIModel.error = nil DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.updateUIModel.state = .updateAvailable - self.updateUIModel.details = .init( + appDelegate.updateUIModel.state = .updateAvailable + appDelegate.updateUIModel.details = .init( version: "1.2.0", build: "demo", size: "42 MB", From 81e3ff90a35ed75839cea83d909790bf7d807c63 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 13:24:37 -0700 Subject: [PATCH 204/319] macOS: Show update information as an overlay --- macos/Sources/App/macOS/AppDelegate.swift | 84 +++++++++++++++ .../Features/Terminal/TerminalView.swift | 20 ++++ .../Window Styles/TerminalWindow.swift | 101 +----------------- .../Sources/Features/Update/UpdatePill.swift | 13 +-- 4 files changed, 110 insertions(+), 108 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 898191b1a..cfa871b32 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -104,6 +104,11 @@ class AppDelegate: NSObject, /// Update view model for UI display @Published private(set) var updateUIModel = UpdateViewModel() + + /// Update actions for UI interactions + private(set) lazy var updateActions: UpdateUIActions = { + createUpdateActions() + }() /// The elapsed time since the process was started var timeSinceLaunch: TimeInterval { @@ -1029,6 +1034,85 @@ class AppDelegate: NSObject, ) } } + + private func createUpdateActions() -> UpdateUIActions { + return UpdateUIActions( + allowAutoChecks: { + print("Demo: Allow auto checks") + self.updateUIModel.state = .idle + }, + denyAutoChecks: { + print("Demo: Deny auto checks") + self.updateUIModel.state = .idle + }, + cancel: { + print("Demo: Cancel") + self.updateUIModel.state = .idle + }, + install: { + print("Demo: Install - simulating download and install flow") + + self.updateUIModel.state = .downloading + self.updateUIModel.progress = 0.0 + + for i in 1...10 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + self.updateUIModel.progress = Double(i) / 10.0 + + if i == 10 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.updateUIModel.state = .extracting + self.updateUIModel.progress = 0.0 + + for j in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { + self.updateUIModel.progress = Double(j) / 5.0 + + if j == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.updateUIModel.state = .readyToInstall + self.updateUIModel.progress = nil + } + } + } + } + } + } + } + } + }, + remindLater: { + print("Demo: Remind later") + self.updateUIModel.state = .idle + }, + skipThisVersion: { + print("Demo: Skip version") + self.updateUIModel.state = .idle + }, + showReleaseNotes: { + print("Demo: Show release notes") + guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return } + NSWorkspace.shared.open(url) + }, + retry: { + print("Demo: Retry - simulating update check") + self.updateUIModel.state = .checking + self.updateUIModel.progress = nil + self.updateUIModel.error = nil + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.updateUIModel.state = .updateAvailable + self.updateUIModel.details = .init( + version: "1.2.0", + build: "demo", + size: "42 MB", + date: Date(), + notesSummary: "This is a demo of the update UI." + ) + } + } + ) + } @IBAction func newWindow(_ sender: Any?) { _ = TerminalController.newWindow(ghostty) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index b5be0ae42..54d2011c7 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -109,6 +109,26 @@ struct TerminalView: View { self.delegate?.performAction(action, on: surfaceView) } } + + // Show update information above all else. + UpdateOverlay() + } + } + } +} + +fileprivate struct UpdateOverlay: View { + var body: some View { + if let appDelegate = NSApp.delegate as? AppDelegate { + VStack { + Spacer() + + HStack { + Spacer() + UpdatePill(model: appDelegate.updateUIModel, actions: appDelegate.updateActions) + .padding(.bottom, 12) + .padding(.trailing, 12) + } } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 87f2be1ca..62439f676 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -94,7 +94,7 @@ class TerminalWindow: NSWindow { updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( viewModel: viewModel, model: appDelegate.updateUIModel, - actions: createUpdateActions() + actions: appDelegate.updateActions )) addTitlebarAccessoryViewController(updateAccessory) updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false @@ -453,105 +453,6 @@ class TerminalWindow: NSWindow { standardWindowButton(.zoomButton)?.isHidden = true } - // MARK: Update UI - - private func createUpdateActions() -> UpdateUIActions { - guard let appDelegate = NSApp.delegate as? AppDelegate else { - return UpdateUIActions( - allowAutoChecks: {}, - denyAutoChecks: {}, - cancel: {}, - install: {}, - remindLater: {}, - skipThisVersion: {}, - showReleaseNotes: {}, - retry: {} - ) - } - - return UpdateUIActions( - allowAutoChecks: { - print("Demo: Allow auto checks") - appDelegate.updateUIModel.state = .idle - }, - denyAutoChecks: { - print("Demo: Deny auto checks") - appDelegate.updateUIModel.state = .idle - }, - cancel: { - print("Demo: Cancel") - appDelegate.updateUIModel.state = .idle - }, - install: { - print("Demo: Install - simulating download and install flow") - - // Start downloading - appDelegate.updateUIModel.state = .downloading - appDelegate.updateUIModel.progress = 0.0 - - // Simulate download progress - for i in 1...10 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { - appDelegate.updateUIModel.progress = Double(i) / 10.0 - - if i == 10 { - // Move to extraction - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - appDelegate.updateUIModel.state = .extracting - appDelegate.updateUIModel.progress = 0.0 - - // Simulate extraction progress - for j in 1...5 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { - appDelegate.updateUIModel.progress = Double(j) / 5.0 - - if j == 5 { - // Move to ready to install - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - appDelegate.updateUIModel.state = .readyToInstall - appDelegate.updateUIModel.progress = nil - } - } - } - } - } - } - } - } - }, - remindLater: { - print("Demo: Remind later") - appDelegate.updateUIModel.state = .idle - }, - skipThisVersion: { - print("Demo: Skip version") - appDelegate.updateUIModel.state = .idle - }, - showReleaseNotes: { - print("Demo: Show release notes") - guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return } - NSWorkspace.shared.open(url) - }, - retry: { - print("Demo: Retry - simulating update check") - appDelegate.updateUIModel.state = .checking - appDelegate.updateUIModel.progress = nil - appDelegate.updateUIModel.error = nil - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - appDelegate.updateUIModel.state = .updateAvailable - appDelegate.updateUIModel.details = .init( - version: "1.2.0", - build: "demo", - size: "42 MB", - date: Date(), - notesSummary: "This is a demo of the update UI." - ) - } - } - ) - } - // MARK: Config struct DerivedConfig { diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index 604be0fbc..ea3c093dc 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -13,14 +13,11 @@ struct UpdatePill: View { var body: some View { if model.state != .idle { - VStack { - pillButton - Spacer() - } - .popover(isPresented: $showPopover, arrowEdge: .bottom) { - UpdatePopoverView(model: model, actions: actions) - } - .transition(.opacity.combined(with: .scale(scale: 0.95))) + pillButton + .popover(isPresented: $showPopover, arrowEdge: .bottom) { + UpdatePopoverView(model: model, actions: actions) + } + .transition(.opacity.combined(with: .scale(scale: 0.95))) } } From f975ac8019c2e5854dbc4b8f6fe1f1913b319a72 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 13:54:58 -0700 Subject: [PATCH 205/319] macOS: only show the update overlay if window doesn't support it --- .../QuickTerminalController.swift | 2 +- .../Terminal/BaseTerminalController.swift | 38 ++++++++++++++++++- .../Features/Terminal/TerminalView.swift | 7 +++- .../HiddenTitlebarTerminalWindow.swift | 3 ++ .../Window Styles/TerminalWindow.swift | 27 ++++++++----- .../TitlebarTabsTahoeTerminalWindow.swift | 4 ++ .../TitlebarTabsVenturaTerminalWindow.swift | 4 ++ 7 files changed, 73 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index fcc8c6505..37c9985c9 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -37,7 +37,7 @@ class QuickTerminalController: BaseTerminalController { /// Tracks if we're currently handling a manual resize to prevent recursion private var isHandlingResize: Bool = false - + init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, baseConfig base: Ghostty.SurfaceConfiguration? = nil, diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index f660ea3ad..b9f9c5a05 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -48,6 +48,9 @@ class BaseTerminalController: NSWindowController, /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false + + /// Set if the terminal view should show the update overlay. + @Published var updateOverlayIsVisible: Bool = false /// Whether the terminal surface should focus when the mouse is over it. var focusFollowsMouse: Bool { @@ -818,7 +821,18 @@ class BaseTerminalController: NSWindowController, } } - func fullscreenDidChange() {} + func fullscreenDidChange() { + guard let fullscreenStyle else { return } + + // When we enter fullscreen, we want to show the update overlay so that it + // is easily visible. For native fullscreen this is visible by showing the + // menubar but we don't want to rely on that. + if fullscreenStyle.isFullscreen { + updateOverlayIsVisible = true + } else { + updateOverlayIsVisible = defaultUpdateOverlayVisibility() + } + } // MARK: Clipboard Confirmation @@ -900,6 +914,28 @@ class BaseTerminalController: NSWindowController, fullscreenStyle = NativeFullscreen(window) fullscreenStyle?.delegate = self } + + // Set our update overlay state + updateOverlayIsVisible = defaultUpdateOverlayVisibility() + } + + func defaultUpdateOverlayVisibility() -> Bool { + guard let window else { return true } + + // No titlebar we always show the update overlay because it can't support + // updates in the titlebar + guard window.styleMask.contains(.titled) else { + return true + } + + // If it's a non terminal window we can't trust it has an update accessory, + // so we always want to show the overlay. + guard let window = window as? TerminalWindow else { + return true + } + + // Show the overlay if the window isn't. + return !window.supportsUpdateAccessory } // MARK: NSWindowDelegate diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 54d2011c7..832cd7966 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject { /// The command palette state. var commandPaletteIsShowing: Bool { get set } + + /// The update overlay should be visible. + var updateOverlayIsVisible: Bool { get } } /// The main terminal view. This terminal view supports splits. @@ -111,7 +114,9 @@ struct TerminalView: View { } // Show update information above all else. - UpdateOverlay() + if viewModel.updateOverlayIsVisible { + UpdateOverlay() + } } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index dc7dd7633..dd8b258f3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -1,6 +1,9 @@ import AppKit class HiddenTitlebarTerminalWindow: TerminalWindow { + // No titlebar, we don't support accessories. + override var supportsUpdateAccessory: Bool { false } + override func awakeFromNib() { super.awakeFromNib() diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 62439f676..6e657f33e 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -20,12 +20,19 @@ class TerminalWindow: NSWindow { /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() + + /// Whether this window supports the update accessory. If this is false, then views within this + /// window should determine how to show update notifications. + var supportsUpdateAccessory: Bool { + // Native window supports it. + true + } /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { windowController as? TerminalController } - + // MARK: NSWindow Overrides override var toolbar: NSToolbar? { @@ -90,14 +97,16 @@ class TerminalWindow: NSWindow { resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false // Create update notification accessory - updateAccessory.layoutAttribute = .right - updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( - viewModel: viewModel, - model: appDelegate.updateUIModel, - actions: appDelegate.updateActions - )) - addTitlebarAccessoryViewController(updateAccessory) - updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false + if supportsUpdateAccessory { + updateAccessory.layoutAttribute = .right + updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( + viewModel: viewModel, + model: appDelegate.updateUIModel, + actions: appDelegate.updateActions + )) + addTitlebarAccessoryViewController(updateAccessory) + updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false + } } // Setup the accessory view for tabs that shows our keyboard shortcuts, diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 260fac4cc..855d29f52 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -8,6 +8,10 @@ import SwiftUI class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate { /// The view model for SwiftUI views private var viewModel = ViewModel() + + /// Titlebar tabs can't support the update accessory because of the way we layout + /// the native tabs back into the menu bar. + override var supportsUpdateAccessory: Bool { false } deinit { tabBarObserver = nil diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 8589877d8..0c087faeb 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -2,6 +2,10 @@ import Cocoa /// Titlebar tabs for macOS 13 to 15. class TitlebarTabsVenturaTerminalWindow: TerminalWindow { + /// Titlebar tabs can't support the update accessory because of the way we layout + /// the native tabs back into the menu bar. + override var supportsUpdateAccessory: Bool { false } + /// This is used to determine if certain elements should be drawn light or dark and should /// be updated whenever the window background color or surrounding elements changes. fileprivate var isLightTheme: Bool = false From 5bebd10b7f73d5a6225c4d254be929cd104f9433 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 00:06:42 +0000 Subject: [PATCH 206/319] build(deps): bump hustcer/milestone-action from 2.9 to 2.11 Bumps [hustcer/milestone-action](https://github.com/hustcer/milestone-action) from 2.9 to 2.11. - [Release notes](https://github.com/hustcer/milestone-action/releases) - [Changelog](https://github.com/hustcer/milestone-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/hustcer/milestone-action/compare/b57a7e52e9913b6b0cdefb10add762af0398659d...bff2091b54a91cf1491564659c554742b285442f) --- updated-dependencies: - dependency-name: hustcer/milestone-action dependency-version: '2.11' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/milestone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index d12418d9c..25d8edaa0 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -15,7 +15,7 @@ jobs: name: Milestone Update steps: - name: Set Milestone for PR - uses: hustcer/milestone-action@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9 + uses: hustcer/milestone-action@bff2091b54a91cf1491564659c554742b285442f # v2.11 if: github.event.pull_request.merged == true with: action: bind-pr # `bind-pr` is the default action @@ -24,7 +24,7 @@ jobs: # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue - uses: hustcer/milestone-action@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9 + uses: hustcer/milestone-action@bff2091b54a91cf1491564659c554742b285442f # v2.11 if: github.event.issue.state == 'closed' with: action: bind-issue From e8ebc6f40535951af3737c571a0023a683e3bfba Mon Sep 17 00:00:00 2001 From: Mike Akers Date: Wed, 8 Oct 2025 21:05:04 -0400 Subject: [PATCH 207/319] docs: Update build requirements for macOS Adds the Metal Toolchain as a required Xcode component for building Ghostty. Also updates the notes about Xcode 26 now that it and Tahoe are out of Beta. --- HACKING.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/HACKING.md b/HACKING.md index 905a244e8..0a4bbef20 100644 --- a/HACKING.md +++ b/HACKING.md @@ -50,24 +50,22 @@ macOS users don't require any additional dependencies. ## Xcode Version and SDKs Building the Ghostty macOS app requires that Xcode, the macOS SDK, -and the iOS SDK are all installed. +the iOS SDK, and Metal Toolchain are all installed. A common issue is that the incorrect version of Xcode is either installed or selected. Use the `xcode-select` command to ensure that the correct version of Xcode is selected: ```shell-session -sudo xcode-select --switch /Applications/Xcode-beta.app +sudo xcode-select --switch /Applications/Xcode.app ``` > [!IMPORTANT] > -> Main branch development of Ghostty is preparing for the next major -> macOS release, Tahoe (macOS 26). Therefore, the main branch requires -> **Xcode 26 and the macOS 26 SDK**. +> Main branch development of Ghostty requires **Xcode 26 and the macOS 26 SDK**. > > You do not need to be running on macOS 26 to build Ghostty, you can -> still use Xcode 26 beta on macOS 15 stable. +> still use Xcode 26 on macOS 15 stable. ## AI and Agents From dfb32022d46576135412d5892713bdec6078519d Mon Sep 17 00:00:00 2001 From: Zhizhen He Date: Thu, 9 Oct 2025 10:52:52 +0800 Subject: [PATCH 208/319] ci: fix typo --- .github/workflows/publish-tag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index 710d04647..c433e7484 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -28,7 +28,7 @@ jobs: echo "Version is valid: ${{ github.event.inputs.version }}" - - name: Exract the Version + - name: Extract the Version id: extract_version run: | VERSION=${{ github.event.inputs.version }} From 59829f53598b48c73137fbfb1d0c7e81375569ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 15:52:42 -0700 Subject: [PATCH 209/319] Sparkle user driver, drives updates to the view model. --- macos/Sources/App/macOS/AppDelegate.swift | 160 +++++----- .../Features/Terminal/TerminalView.swift | 2 +- .../Window Styles/TerminalWindow.swift | 6 +- .../Sources/Features/Update/UpdateBadge.swift | 32 +- .../Features/Update/UpdateDriver.swift | 103 +++++++ .../Sources/Features/Update/UpdatePill.swift | 9 +- .../Features/Update/UpdatePopoverView.swift | 276 +++++++++--------- .../Features/Update/UpdateViewModel.swift | 165 +++++------ 8 files changed, 403 insertions(+), 350 deletions(-) create mode 100644 macos/Sources/Features/Update/UpdateDriver.swift diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index cfa871b32..4fd6dfb3f 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -103,12 +103,7 @@ class AppDelegate: NSObject, let updaterDelegate: UpdaterDelegate = UpdaterDelegate() /// Update view model for UI display - @Published private(set) var updateUIModel = UpdateViewModel() - - /// Update actions for UI interactions - private(set) lazy var updateActions: UpdateUIActions = { - createUpdateActions() - }() + @Published private(set) var updateViewModel = UpdateViewModel() /// The elapsed time since the process was started var timeSinceLaunch: TimeInterval { @@ -1013,106 +1008,83 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - // Demo mode: simulate update check instead of real Sparkle check - // TODO: Replace with real updaterController.checkForUpdates(sender) when SPUUserDriver is implemented + // Demo mode: simulate update check with new UpdateState + updateViewModel.state = .checking(.init(cancel: { [weak self] in + self?.updateViewModel.state = .idle + })) - // Simulate the full update check flow - updateUIModel.state = .checking - updateUIModel.progress = nil - updateUIModel.details = nil - updateUIModel.error = nil - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - // Simulate finding an update - self.updateUIModel.state = .updateAvailable - self.updateUIModel.details = .init( - version: "1.2.0", - build: "demo", - size: "42 MB", - date: Date(), - notesSummary: "This is a demo of the update UI. New features and bug fixes would be listed here." - ) + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + guard let self else { return } + + self.updateViewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { [weak self] choice in + if choice == .install { + self?.simulateDownload() + } else { + self?.updateViewModel.state = .idle + } + } + )) } } - private func createUpdateActions() -> UpdateUIActions { - return UpdateUIActions( - allowAutoChecks: { - print("Demo: Allow auto checks") - self.updateUIModel.state = .idle + private func simulateDownload() { + let download = UpdateState.Downloading( + cancel: { [weak self] in + self?.updateViewModel.state = .idle }, - denyAutoChecks: { - print("Demo: Deny auto checks") - self.updateUIModel.state = .idle - }, - cancel: { - print("Demo: Cancel") - self.updateUIModel.state = .idle - }, - install: { - print("Demo: Install - simulating download and install flow") + expectedLength: nil, + progress: 0, + ) + updateViewModel.state = .downloading(download) + + for i in 1...10 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { [weak self] in + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + self?.updateViewModel.state = .downloading(updatedDownload) - self.updateUIModel.state = .downloading - self.updateUIModel.progress = 0.0 - - for i in 1...10 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { - self.updateUIModel.progress = Double(i) / 10.0 - - if i == 10 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.updateUIModel.state = .extracting - self.updateUIModel.progress = 0.0 - - for j in 1...5 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { - self.updateUIModel.progress = Double(j) / 5.0 - - if j == 5 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.updateUIModel.state = .readyToInstall - self.updateUIModel.progress = nil - } - } - } - } - } - } + if i == 10 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.simulateExtract() } } - }, - remindLater: { - print("Demo: Remind later") - self.updateUIModel.state = .idle - }, - skipThisVersion: { - print("Demo: Skip version") - self.updateUIModel.state = .idle - }, - showReleaseNotes: { - print("Demo: Show release notes") - guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return } - NSWorkspace.shared.open(url) - }, - retry: { - print("Demo: Retry - simulating update check") - self.updateUIModel.state = .checking - self.updateUIModel.progress = nil - self.updateUIModel.error = nil + } + } + } + + private func simulateExtract() { + updateViewModel.state = .extracting(.init(progress: 0.0)) + + for j in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { [weak self] in + self?.updateViewModel.state = .extracting(.init(progress: Double(j) / 5.0)) - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.updateUIModel.state = .updateAvailable - self.updateUIModel.details = .init( - version: "1.2.0", - build: "demo", - size: "42 MB", - date: Date(), - notesSummary: "This is a demo of the update UI." - ) + if j == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.updateViewModel.state = .readyToInstall(.init( + reply: { [weak self] choice in + if choice == .install { + self?.updateViewModel.state = .installing + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.updateViewModel.state = .idle + } + } else { + self?.updateViewModel.state = .idle + } + } + )) + } } } - ) + } } + + @IBAction func newWindow(_ sender: Any?) { _ = TerminalController.newWindow(ghostty) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 832cd7966..51c4f6ddd 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -130,7 +130,7 @@ fileprivate struct UpdateOverlay: View { HStack { Spacer() - UpdatePill(model: appDelegate.updateUIModel, actions: appDelegate.updateActions) + UpdatePill(model: appDelegate.updateViewModel) .padding(.bottom, 12) .padding(.trailing, 12) } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 6e657f33e..4737bacaf 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -101,8 +101,7 @@ class TerminalWindow: NSWindow { updateAccessory.layoutAttribute = .right updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( viewModel: viewModel, - model: appDelegate.updateUIModel, - actions: appDelegate.updateActions + model: appDelegate.updateViewModel )) addTitlebarAccessoryViewController(updateAccessory) updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false @@ -532,10 +531,9 @@ extension TerminalWindow { struct UpdateAccessoryView: View { @ObservedObject var viewModel: ViewModel @ObservedObject var model: UpdateViewModel - let actions: UpdateUIActions var body: some View { - UpdatePill(model: model, actions: actions) + UpdatePill(model: model) .padding(.top, viewModel.accessoryTopPadding) .padding(.trailing, 10) } diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index a6ffe6cb6..fd1eb3498 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -15,27 +15,35 @@ struct UpdateBadge: View { var body: some View { switch model.state { - case .downloading, .extracting: - if let progress = model.progress { + case .downloading(let download): + if let expectedLength = download.expectedLength, expectedLength > 0 { + let progress = Double(download.progress) / Double(expectedLength) ProgressRingView(progress: progress) } else { Image(systemName: "arrow.down.circle") } + case .extracting(let extracting): + ProgressRingView(progress: extracting.progress) + case .checking, .installing: - Image(systemName: model.iconName) - .rotationEffect(.degrees(rotationAngle)) - .onAppear { - withAnimation(.linear(duration: 2.5).repeatForever(autoreverses: false)) { - rotationAngle = 360 + if let iconName = model.iconName { + Image(systemName: iconName) + .rotationEffect(.degrees(rotationAngle)) + .onAppear { + withAnimation(.linear(duration: 2.5).repeatForever(autoreverses: false)) { + rotationAngle = 360 + } } - } - .onDisappear { - rotationAngle = 0 - } + .onDisappear { + rotationAngle = 0 + } + } default: - Image(systemName: model.iconName) + if let iconName = model.iconName { + Image(systemName: iconName) + } } } } diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift new file mode 100644 index 000000000..00f74e9ed --- /dev/null +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -0,0 +1,103 @@ +import Sparkle + +/// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation. +class UpdateDriver: NSObject, SPUUserDriver { + let viewModel: UpdateViewModel + let retryHandler: () -> Void + + init(viewModel: UpdateViewModel, retryHandler: @escaping () -> Void) { + self.viewModel = viewModel + self.retryHandler = retryHandler + super.init() + } + + func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { + viewModel.state = .permissionRequest(.init(request: request, reply: reply)) + } + + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + viewModel.state = .checking(.init(cancel: cancellation)) + } + + func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { + viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply)) + } + + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + // We don't do anything with the release notes here because Ghostty + // doesn't use the release notes feature of Sparkle currently. + } + + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) { + // We don't do anything with release notes. See `showUpdateReleaseNotes` + } + + func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { + viewModel.state = .notFound + // TODO: Do we need to acknowledge? + } + + func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { + viewModel.state = .error(.init(error: error, retry: retryHandler)) + } + + func showDownloadInitiated(cancellation: @escaping () -> Void) { + viewModel.state = .downloading(.init( + cancel: cancellation, + expectedLength: nil, + progress: 0)) + } + + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + guard case let .downloading(downloading) = viewModel.state else { + return + } + + viewModel.state = .downloading(.init( + cancel: downloading.cancel, + expectedLength: expectedContentLength, + progress: 0)) + } + + func showDownloadDidReceiveData(ofLength length: UInt64) { + guard case let .downloading(downloading) = viewModel.state else { + return + } + + viewModel.state = .downloading(.init( + cancel: downloading.cancel, + expectedLength: downloading.expectedLength, + progress: downloading.progress + length)) + } + + func showDownloadDidStartExtractingUpdate() { + viewModel.state = .extracting(.init(progress: 0)) + } + + func showExtractionReceivedProgress(_ progress: Double) { + viewModel.state = .extracting(.init(progress: progress)) + } + + func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { + viewModel.state = .readyToInstall(.init(reply: reply)) + } + + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { + viewModel.state = .installing + } + + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + // We don't do anything here. + viewModel.state = .idle + } + + func showUpdateInFocus() { + // We don't currently implement this because our update state is + // shown in a terminal window. We may want to implement this at some + // point to handle the case that no windows are open, though. + } + + func dismissUpdateInstallation() { + viewModel.state = .idle + } +} diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index ea3c093dc..dda9ad607 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -5,17 +5,14 @@ struct UpdatePill: View { /// The update view model that provides the current state and information @ObservedObject var model: UpdateViewModel - /// The actions that can be performed on updates - let actions: UpdateUIActions - /// Whether the update popover is currently visible @State private var showPopover = false var body: some View { - if model.state != .idle { + if !model.state.isIdle { pillButton .popover(isPresented: $showPopover, arrowEdge: .bottom) { - UpdatePopoverView(model: model, actions: actions) + UpdatePopoverView(model: model) } .transition(.opacity.combined(with: .scale(scale: 0.95))) } @@ -43,6 +40,6 @@ struct UpdatePill: View { .contentShape(Capsule()) } .buttonStyle(.plain) - .help(model.stateTooltip) + .help(model.text) } } diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index af870b4de..39c4ac5c9 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Sparkle /// A popover view that displays detailed update information and action buttons. /// @@ -8,9 +9,6 @@ struct UpdatePopoverView: View { /// The update view model that provides the current state and information @ObservedObject var model: UpdateViewModel - /// The actions that can be performed on updates - let actions: UpdateUIActions - /// Environment value for dismissing the popover @Environment(\.dismiss) private var dismiss @@ -18,41 +16,47 @@ struct UpdatePopoverView: View { VStack(alignment: .leading, spacing: 0) { switch model.state { case .idle: + // Shouldn't happen in a well-formed view stack. Higher levels + // should not call the popover for idles. EmptyView() - case .permissionRequest: - permissionRequestView + case .permissionRequest(let request): + PermissionRequestView(request: request, dismiss: dismiss) - case .checking: - checkingView + case .checking(let checking): + CheckingView(checking: checking, dismiss: dismiss) - case .updateAvailable: - updateAvailableView + case .updateAvailable(let update): + UpdateAvailableView(update: update, dismiss: dismiss) - case .downloading: - downloadingView + case .downloading(let download): + DownloadingView(download: download, dismiss: dismiss) - case .extracting: - extractingView + case .extracting(let extracting): + ExtractingView(extracting: extracting) - case .readyToInstall: - readyToInstallView + case .readyToInstall(let ready): + ReadyToInstallView(ready: ready, dismiss: dismiss) case .installing: - installingView + InstallingView() case .notFound: - notFoundView + NotFoundView(dismiss: dismiss) - case .error: - errorView + case .error(let error): + UpdateErrorView(error: error, dismiss: dismiss) } } .frame(width: 300) } +} + +fileprivate struct PermissionRequestView: View { + let request: UpdateState.PermissionRequest + let dismiss: DismissAction - /// View shown when requesting permission to enable automatic updates - private var permissionRequestView: some View { + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Enable automatic updates?") @@ -66,7 +70,9 @@ struct UpdatePopoverView: View { HStack(spacing: 8) { Button("Not Now") { - actions.denyAutoChecks() + request.reply(SUUpdatePermissionResponse( + automaticUpdateChecks: false, + sendSystemProfile: false)) dismiss() } .keyboardShortcut(.cancelAction) @@ -74,7 +80,9 @@ struct UpdatePopoverView: View { Spacer() Button("Allow") { - actions.allowAutoChecks() + request.reply(SUUpdatePermissionResponse( + automaticUpdateChecks: true, + sendSystemProfile: false)) dismiss() } .keyboardShortcut(.defaultAction) @@ -83,9 +91,13 @@ struct UpdatePopoverView: View { } .padding(16) } +} + +fileprivate struct CheckingView: View { + let checking: UpdateState.Checking + let dismiss: DismissAction - /// View shown while checking for updates - private var checkingView: some View { + var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 10) { ProgressView() @@ -97,7 +109,7 @@ struct UpdatePopoverView: View { HStack { Spacer() Button("Cancel") { - actions.cancel() + checking.cancel() dismiss() } .keyboardShortcut(.cancelAction) @@ -106,47 +118,39 @@ struct UpdatePopoverView: View { } .padding(16) } +} + +fileprivate struct UpdateAvailableView: View { + let update: UpdateState.UpdateAvailable + let dismiss: DismissAction - /// View shown when an update is available, displaying version and size information - private var updateAvailableView: some View { + var body: some View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 8) { Text("Update Available") .font(.system(size: 13, weight: .semibold)) - if let details = model.details { - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 6) { - Text("Version:") - .foregroundColor(.secondary) - .frame(width: 50, alignment: .trailing) - Text(details.version) - } - .font(.system(size: 11)) - - if let size = details.size { - HStack(spacing: 6) { - Text("Size:") - .foregroundColor(.secondary) - .frame(width: 50, alignment: .trailing) - Text(size) - } - .font(.system(size: 11)) - } + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Text("Version:") + .foregroundColor(.secondary) + .frame(width: 50, alignment: .trailing) + Text(update.appcastItem.displayVersionString) } + .font(.system(size: 11)) } } HStack(spacing: 8) { Button("Skip") { - actions.skipThisVersion() + update.reply(.skip) dismiss() } .controlSize(.small) Button("Later") { - actions.remindLater() + update.reply(.dismiss) dismiss() } .controlSize(.small) @@ -155,7 +159,7 @@ struct UpdatePopoverView: View { Spacer() Button("Install") { - actions.install() + update.reply(.install) dismiss() } .keyboardShortcut(.defaultAction) @@ -164,36 +168,22 @@ struct UpdatePopoverView: View { } } .padding(16) - - if model.details?.notesSummary != nil { - Divider() - - Button(action: actions.showReleaseNotes) { - HStack { - Text("View Release Notes") - .font(.system(size: 11)) - Spacer() - Image(systemName: "arrow.up.right.square") - .font(.system(size: 11)) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color(nsColor: .controlBackgroundColor)) - } } } +} + +fileprivate struct DownloadingView: View { + let download: UpdateState.Downloading + let dismiss: DismissAction - /// View shown while downloading an update, with progress indicator - private var downloadingView: some View { + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Downloading Update") .font(.system(size: 13, weight: .semibold)) - if let progress = model.progress { + if let expectedLength = download.expectedLength, expectedLength > 0 { + let progress = Double(download.progress) / Double(expectedLength) VStack(alignment: .leading, spacing: 6) { ProgressView(value: progress) Text(String(format: "%.0f%%", progress * 100)) @@ -209,7 +199,7 @@ struct UpdatePopoverView: View { HStack { Spacer() Button("Cancel") { - actions.cancel() + download.cancel() dismiss() } .keyboardShortcut(.cancelAction) @@ -218,45 +208,45 @@ struct UpdatePopoverView: View { } .padding(16) } +} + +fileprivate struct ExtractingView: View { + let extracting: UpdateState.Extracting - /// View shown while extracting/preparing the downloaded update - private var extractingView: some View { + var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Preparing Update") .font(.system(size: 13, weight: .semibold)) - if let progress = model.progress { - VStack(alignment: .leading, spacing: 6) { - ProgressView(value: progress) - Text(String(format: "%.0f%%", progress * 100)) - .font(.system(size: 11)) - .foregroundColor(.secondary) - } - } else { - ProgressView() - .controlSize(.small) + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: extracting.progress, total: 1.0) + Text(String(format: "%.0f%%", extracting.progress * 100)) + .font(.system(size: 11)) + .foregroundColor(.secondary) } } .padding(16) } +} + +fileprivate struct ReadyToInstallView: View { + let ready: UpdateState.ReadyToInstall + let dismiss: DismissAction - /// View shown when an update is ready to be installed - private var readyToInstallView: some View { + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Ready to Install") .font(.system(size: 13, weight: .semibold)) - if let details = model.details { - Text("Version \(details.version) is ready to install.") - .font(.system(size: 11)) - .foregroundColor(.secondary) - } + Text("The update is ready to install.") + .font(.system(size: 11)) + .foregroundColor(.secondary) } HStack(spacing: 8) { Button("Later") { - actions.remindLater() + ready.reply(.dismiss) dismiss() } .keyboardShortcut(.cancelAction) @@ -265,7 +255,7 @@ struct UpdatePopoverView: View { Spacer() Button("Install and Relaunch") { - actions.install() + ready.reply(.install) dismiss() } .keyboardShortcut(.defaultAction) @@ -275,9 +265,10 @@ struct UpdatePopoverView: View { } .padding(16) } - - /// View shown during the installation process - private var installingView: some View { +} + +fileprivate struct InstallingView: View { + var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 10) { ProgressView() @@ -292,9 +283,12 @@ struct UpdatePopoverView: View { } .padding(16) } +} + +fileprivate struct NotFoundView: View { + let dismiss: DismissAction - /// View shown when no updates are found (already on latest version) - private var notFoundView: some View { + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("No Updates Found") @@ -309,48 +303,48 @@ struct UpdatePopoverView: View { HStack { Spacer() Button("OK") { - actions.remindLater() - dismiss() - } - .keyboardShortcut(.defaultAction) - .controlSize(.small) - } - } - .padding(16) - } - - /// View shown when an error occurs during the update process - private var errorView: some View { - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.orange) - .font(.system(size: 13)) - Text(model.error?.title ?? "Update Failed") - .font(.system(size: 13, weight: .semibold)) - } - - if let message = model.error?.message { - Text(message) - .font(.system(size: 11)) - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - } - - HStack(spacing: 8) { - Button("OK") { - actions.remindLater() - dismiss() - } - .keyboardShortcut(.cancelAction) - .controlSize(.small) - - Spacer() - - Button("Retry") { - actions.retry() + dismiss() + } + .keyboardShortcut(.defaultAction) + .controlSize(.small) + } + } + .padding(16) + } +} + +fileprivate struct UpdateErrorView: View { + let error: UpdateState.Error + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + .font(.system(size: 13)) + Text("Update Failed") + .font(.system(size: 13, weight: .semibold)) + } + + Text(error.error.localizedDescription) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack(spacing: 8) { + Button("OK") { + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + + Spacer() + + Button("Retry") { + error.retry() dismiss() } .keyboardShortcut(.defaultAction) diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index fb477324c..57e438bcd 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -1,83 +1,13 @@ import Foundation import SwiftUI - -struct UpdateUIActions { - let allowAutoChecks: () -> Void - let denyAutoChecks: () -> Void - let cancel: () -> Void - let install: () -> Void - let remindLater: () -> Void - let skipThisVersion: () -> Void - let showReleaseNotes: () -> Void - let retry: () -> Void -} +import Sparkle class UpdateViewModel: ObservableObject { - @Published var state: State = .idle - @Published var progress: Double? = nil - @Published var details: Details? = nil - @Published var error: ErrorInfo? = nil - - enum State: Equatable { - case idle - case permissionRequest - case checking - case updateAvailable - case downloading - case extracting - case readyToInstall - case installing - case notFound - case error - } - - struct ErrorInfo: Equatable { - let title: String - let message: String - } - - struct Details: Equatable { - let version: String - let build: String? - let size: String? - let date: Date? - let notesSummary: String? - } - - var stateTooltip: String { - switch state { - case .idle: - return "" - case .permissionRequest: - return "Update permission required" - case .checking: - return "Checking for updates…" - case .updateAvailable: - if let details { - return "Update available: \(details.version)" - } - return "Update available" - case .downloading: - if let progress { - return String(format: "Downloading %.0f%%…", progress * 100) - } - return "Downloading…" - case .extracting: - if let progress { - return String(format: "Preparing %.0f%%…", progress * 100) - } - return "Preparing…" - case .readyToInstall: - return "Ready to install" - case .installing: - return "Installing…" - case .notFound: - return "No updates found" - case .error: - return error?.title ?? "Update failed" - } - } + @Published var state: UpdateState = .idle + /// The text to display for the current update state. + /// Returns an empty string for idle state, progress percentages for downloading/extracting, + /// or descriptive text for other states. var text: String { switch state { case .idle: @@ -86,36 +16,33 @@ class UpdateViewModel: ObservableObject { return "Update Permission" case .checking: return "Checking for Updates…" - case .updateAvailable: - if let details { - return "Update Available: \(details.version)" - } - return "Update Available" - case .downloading: - if let progress { + case .updateAvailable(let update): + return "Update Available: \(update.appcastItem.displayVersionString)" + case .downloading(let download): + if let expectedLength = download.expectedLength, expectedLength > 0 { + let progress = Double(download.progress) / Double(expectedLength) return String(format: "Downloading: %.0f%%", progress * 100) } return "Downloading…" - case .extracting: - if let progress { - return String(format: "Preparing: %.0f%%", progress * 100) - } - return "Preparing…" + case .extracting(let extracting): + return String(format: "Preparing: %.0f%%", extracting.progress * 100) case .readyToInstall: return "Install Update" case .installing: return "Installing…" case .notFound: return "No Updates Available" - case .error: - return error?.title ?? "Update Failed" + case .error(let err): + return err.error.localizedDescription } } - var iconName: String { + /// The SF Symbol icon name for the current update state. + /// Returns nil for idle, downloading, and extracting states. + var iconName: String? { switch state { case .idle: - return "" + return nil case .permissionRequest: return "questionmark.circle" case .checking: @@ -123,7 +50,7 @@ class UpdateViewModel: ObservableObject { case .updateAvailable: return "arrow.down.circle.fill" case .downloading, .extracting: - return "" // Progress ring instead + return nil case .readyToInstall: return "checkmark.circle.fill" case .installing: @@ -135,6 +62,7 @@ class UpdateViewModel: ObservableObject { } } + /// The color to apply to the icon for the current update state. var iconColor: Color { switch state { case .idle: @@ -152,6 +80,7 @@ class UpdateViewModel: ObservableObject { } } + /// The background color for the update pill. var backgroundColor: Color { switch state { case .updateAvailable: @@ -165,6 +94,7 @@ class UpdateViewModel: ObservableObject { } } + /// The foreground (text) color for the update pill. var foregroundColor: Color { switch state { case .updateAvailable, .readyToInstall: @@ -176,3 +106,54 @@ class UpdateViewModel: ObservableObject { } } } + +enum UpdateState { + case idle + case permissionRequest(PermissionRequest) + case checking(Checking) + case updateAvailable(UpdateAvailable) + case notFound + case error(Error) + case downloading(Downloading) + case extracting(Extracting) + case readyToInstall(ReadyToInstall) + case installing + + var isIdle: Bool { + if case .idle = self { return true } + return false + } + + struct PermissionRequest { + let request: SPUUpdatePermissionRequest + let reply: @Sendable (SUUpdatePermissionResponse) -> Void + } + + struct Checking { + let cancel: () -> Void + } + + struct UpdateAvailable { + let appcastItem: SUAppcastItem + let reply: @Sendable (SPUUserUpdateChoice) -> Void + } + + struct Error { + let error: any Swift.Error + let retry: () -> Void + } + + struct Downloading { + let cancel: () -> Void + let expectedLength: UInt64? + let progress: UInt64 + } + + struct Extracting { + let progress: Double + } + + struct ReadyToInstall { + let reply: @Sendable (SPUUserUpdateChoice) -> Void + } +} From a55de09944865044d2f44a37a177bf0d5f882cdb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:09:06 -0700 Subject: [PATCH 210/319] macos: update simulator to test various scenarios in UI --- macos/Sources/App/macOS/AppDelegate.swift | 76 +---- .../Features/Update/UpdateSimulator.swift | 272 ++++++++++++++++++ 2 files changed, 273 insertions(+), 75 deletions(-) create mode 100644 macos/Sources/Features/Update/UpdateSimulator.swift diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 4fd6dfb3f..a9f72b58b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1008,82 +1008,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - // Demo mode: simulate update check with new UpdateState - updateViewModel.state = .checking(.init(cancel: { [weak self] in - self?.updateViewModel.state = .idle - })) - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in - guard let self else { return } - - self.updateViewModel.state = .updateAvailable(.init( - appcastItem: SUAppcastItem.empty(), - reply: { [weak self] choice in - if choice == .install { - self?.simulateDownload() - } else { - self?.updateViewModel.state = .idle - } - } - )) - } + UpdateSimulator.notFound.simulate(with: updateViewModel) } - - private func simulateDownload() { - let download = UpdateState.Downloading( - cancel: { [weak self] in - self?.updateViewModel.state = .idle - }, - expectedLength: nil, - progress: 0, - ) - updateViewModel.state = .downloading(download) - - for i in 1...10 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { [weak self] in - let updatedDownload = UpdateState.Downloading( - cancel: download.cancel, - expectedLength: 1000, - progress: UInt64(i * 100) - ) - self?.updateViewModel.state = .downloading(updatedDownload) - - if i == 10 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.simulateExtract() - } - } - } - } - } - - private func simulateExtract() { - updateViewModel.state = .extracting(.init(progress: 0.0)) - - for j in 1...5 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { [weak self] in - self?.updateViewModel.state = .extracting(.init(progress: Double(j) / 5.0)) - - if j == 5 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.updateViewModel.state = .readyToInstall(.init( - reply: { [weak self] choice in - if choice == .install { - self?.updateViewModel.state = .installing - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in - self?.updateViewModel.state = .idle - } - } else { - self?.updateViewModel.state = .idle - } - } - )) - } - } - } - } - } - @IBAction func newWindow(_ sender: Any?) { diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift new file mode 100644 index 000000000..0cf2d221b --- /dev/null +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -0,0 +1,272 @@ +import Foundation +import Sparkle + +/// Simulates various update scenarios for testing the update UI. +/// +/// The expected usage is by overriding the `checkForUpdates` function in AppDelegate and +/// calling one of these instead. This will allow us to test the update flows without having to use +/// real updates. +enum UpdateSimulator { + /// Complete successful update flow: checking → available → download → extract → ready → install → idle + case happyPath + + /// No updates available: checking (2s) → "No Updates Available" (3s) → idle + case notFound + + /// Error during check: checking (2s) → error with retry callback + case error + + /// Slower download for testing progress UI: checking → available → download (20 steps, ~10s) → extract → install + case slowDownload + + /// Initial permission request flow: shows permission dialog → proceeds with happy path if accepted + case permissionRequest + + /// User cancels during download: checking → available → download (5 steps) → cancels → idle + case cancelDuringDownload + + /// User cancels while checking: checking (1s) → cancels → idle + case cancelDuringChecking + + func simulate(with viewModel: UpdateViewModel) { + switch self { + case .happyPath: + simulateHappyPath(viewModel) + case .notFound: + simulateNotFound(viewModel) + case .error: + simulateError(viewModel) + case .slowDownload: + simulateSlowDownload(viewModel) + case .permissionRequest: + simulatePermissionRequest(viewModel) + case .cancelDuringDownload: + simulateCancelDuringDownload(viewModel) + case .cancelDuringChecking: + simulateCancelDuringChecking(viewModel) + } + } + + private func simulateHappyPath(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateDownload(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateNotFound(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .notFound + + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + viewModel.state = .idle + } + } + } + + private func simulateError(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .error(.init( + error: NSError(domain: "UpdateError", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Failed to check for updates" + ]), + retry: { + simulateHappyPath(viewModel) + } + )) + } + } + + private func simulateSlowDownload(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateSlowDownloadProgress(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...20 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 2000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 20 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + simulateExtract(viewModel) + } + } + } + } + } + + private func simulatePermissionRequest(_ viewModel: UpdateViewModel) { + let request = SPUUpdatePermissionRequest(systemProfile: []) + viewModel.state = .permissionRequest(.init( + request: request, + reply: { response in + if response.automaticUpdateChecks { + simulateHappyPath(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + + private func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateDownloadThenCancel(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.state = .idle + } + } + } + } + } + + private func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + viewModel.state = .idle + } + } + + private func simulateDownload(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...10 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 10 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + simulateExtract(viewModel) + } + } + } + } + } + + private func simulateExtract(_ viewModel: UpdateViewModel) { + viewModel.state = .extracting(.init(progress: 0.0)) + + for j in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { + viewModel.state = .extracting(.init(progress: Double(j) / 5.0)) + + if j == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.state = .readyToInstall(.init( + reply: { choice in + if choice == .install { + viewModel.state = .installing + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .idle + } + } else { + viewModel.state = .idle + } + } + )) + } + } + } + } + } +} From 95a9e6340134cafc084fe560b8f2c94e8e7baac6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:13:34 -0700 Subject: [PATCH 211/319] macos: not found state dismisses on click, after 5s --- .../Sources/Features/Update/UpdatePill.swift | 18 +++++++++- .../Features/Update/UpdateViewModel.swift | 33 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index dda9ad607..1dc29e250 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -15,13 +15,29 @@ struct UpdatePill: View { UpdatePopoverView(model: model) } .transition(.opacity.combined(with: .scale(scale: 0.95))) + .onChange(of: model.state) { newState in + if case .notFound = newState { + Task { + try? await Task.sleep(for: .seconds(5)) + if case .notFound = model.state { + model.state = .idle + } + } + } + } } } /// The pill-shaped button view that displays the update badge and text @ViewBuilder private var pillButton: some View { - Button(action: { showPopover.toggle() }) { + Button(action: { + if case .notFound = model.state { + model.state = .idle + } else { + showPopover.toggle() + } + }) { HStack(spacing: 6) { UpdateBadge(model: model) .frame(width: 14, height: 14) diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 57e438bcd..05f7eef9a 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -87,6 +87,8 @@ class UpdateViewModel: ObservableObject { return .accentColor case .readyToInstall: return Color(nsColor: NSColor.systemGreen.blended(withFraction: 0.3, of: .black) ?? .systemGreen) + case .notFound: + return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.5, of: .black) ?? .systemBlue) case .error: return .orange.opacity(0.2) default: @@ -99,6 +101,8 @@ class UpdateViewModel: ObservableObject { switch state { case .updateAvailable, .readyToInstall: return .white + case .notFound: + return .white case .error: return .orange default: @@ -107,7 +111,7 @@ class UpdateViewModel: ObservableObject { } } -enum UpdateState { +enum UpdateState: Equatable { case idle case permissionRequest(PermissionRequest) case checking(Checking) @@ -124,6 +128,33 @@ enum UpdateState { return false } + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { + switch (lhs, rhs) { + case (.idle, .idle): + return true + case (.permissionRequest, .permissionRequest): + return true + case (.checking, .checking): + return true + case (.updateAvailable(let lUpdate), .updateAvailable(let rUpdate)): + return lUpdate.appcastItem.displayVersionString == rUpdate.appcastItem.displayVersionString + case (.notFound, .notFound): + return true + case (.error(let lErr), .error(let rErr)): + return lErr.error.localizedDescription == rErr.error.localizedDescription + case (.downloading(let lDown), .downloading(let rDown)): + return lDown.progress == rDown.progress && lDown.expectedLength == rDown.expectedLength + case (.extracting(let lExt), .extracting(let rExt)): + return lExt.progress == rExt.progress + case (.readyToInstall, .readyToInstall): + return true + case (.installing, .installing): + return true + default: + return false + } + } + struct PermissionRequest { let request: SPUUpdatePermissionRequest let reply: @Sendable (SUUpdatePermissionResponse) -> Void From 9e17255ca9f097ebcccb0025ce32c798360f31a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:16:07 -0700 Subject: [PATCH 212/319] macos: "OK" should dismiss error --- macos/Sources/App/macOS/AppDelegate.swift | 2 +- macos/Sources/Features/Update/UpdateDriver.swift | 4 +++- macos/Sources/Features/Update/UpdatePopoverView.swift | 1 + macos/Sources/Features/Update/UpdateSimulator.swift | 3 +++ macos/Sources/Features/Update/UpdateViewModel.swift | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index a9f72b58b..9e1425062 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1008,7 +1008,7 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - UpdateSimulator.notFound.simulate(with: updateViewModel) + UpdateSimulator.error.simulate(with: updateViewModel) } diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 00f74e9ed..6627559e8 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -38,7 +38,9 @@ class UpdateDriver: NSObject, SPUUserDriver { } func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { - viewModel.state = .error(.init(error: error, retry: retryHandler)) + viewModel.state = .error(.init(error: error, retry: retryHandler, dismiss: { [weak viewModel] in + viewModel?.state = .idle + })) } func showDownloadInitiated(cancellation: @escaping () -> Void) { diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 39c4ac5c9..cbe517f74 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -336,6 +336,7 @@ fileprivate struct UpdateErrorView: View { HStack(spacing: 8) { Button("OK") { + error.dismiss() dismiss() } .keyboardShortcut(.cancelAction) diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift index 0cf2d221b..96fab4835 100644 --- a/macos/Sources/Features/Update/UpdateSimulator.swift +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -92,6 +92,9 @@ enum UpdateSimulator { ]), retry: { simulateHappyPath(viewModel) + }, + dismiss: { + viewModel.state = .idle } )) } diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 05f7eef9a..f0b779d60 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -172,6 +172,7 @@ enum UpdateState: Equatable { struct Error { let error: any Swift.Error let retry: () -> Void + let dismiss: () -> Void } struct Downloading { From b4ab1cc1edd273577449c9ced8f713c4293134b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:21:26 -0700 Subject: [PATCH 213/319] macos: clean up the permission request --- macos/Sources/App/macOS/AppDelegate.swift | 2 +- macos/Sources/Features/Update/UpdatePopoverView.swift | 2 +- macos/Sources/Features/Update/UpdateViewModel.swift | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 9e1425062..62c5c0316 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1008,7 +1008,7 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - UpdateSimulator.error.simulate(with: updateViewModel) + UpdateSimulator.permissionRequest.simulate(with: updateViewModel) } diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index cbe517f74..7f1886d60 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -62,7 +62,7 @@ fileprivate struct PermissionRequestView: View { Text("Enable automatic updates?") .font(.system(size: 13, weight: .semibold)) - Text("Ghostty can automatically check for and download updates in the background.") + Text("Ghostty can automatically check for updates in the background.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index f0b779d60..674888bb5 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -13,7 +13,7 @@ class UpdateViewModel: ObservableObject { case .idle: return "" case .permissionRequest: - return "Update Permission" + return "Enable Automatic Updates?" case .checking: return "Checking for Updates…" case .updateAvailable(let update): @@ -67,7 +67,9 @@ class UpdateViewModel: ObservableObject { switch state { case .idle: return .secondary - case .permissionRequest, .checking: + case .permissionRequest: + return .white + case .checking: return .secondary case .updateAvailable, .readyToInstall: return .accentColor @@ -83,6 +85,8 @@ class UpdateViewModel: ObservableObject { /// The background color for the update pill. var backgroundColor: Color { switch state { + case .permissionRequest: + return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.3, of: .black) ?? .systemBlue) case .updateAvailable: return .accentColor case .readyToInstall: @@ -99,6 +103,8 @@ class UpdateViewModel: ObservableObject { /// The foreground (text) color for the update pill. var foregroundColor: Color { switch state { + case .permissionRequest: + return .white case .updateAvailable, .readyToInstall: return .white case .notFound: From bce49a08438b39bbc699348d8308e724f6334f75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:29:14 -0700 Subject: [PATCH 214/319] macos: hook up our new update controller --- macos/Sources/App/macOS/AppDelegate.swift | 31 ++++------- .../Features/Update/UpdateController.swift | 55 +++++++++++++++++++ .../Features/Update/UpdateDriver.swift | 20 +++++-- 3 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 macos/Sources/Features/Update/UpdateController.swift diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 62c5c0316..216373e7e 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -99,11 +99,10 @@ class AppDelegate: NSObject, ) /// Manages updates - let updaterController: SPUStandardUpdaterController - let updaterDelegate: UpdaterDelegate = UpdaterDelegate() - - /// Update view model for UI display - @Published private(set) var updateViewModel = UpdateViewModel() + let updateController = UpdateController() + var updateViewModel: UpdateViewModel { + updateController.viewModel + } /// The elapsed time since the process was started var timeSinceLaunch: TimeInterval { @@ -130,15 +129,6 @@ class AppDelegate: NSObject, } override init() { - updaterController = SPUStandardUpdaterController( - // Important: we must not start the updater here because we need to read our configuration - // first to determine whether we're automatically checking, downloading, etc. The updater - // is started later in applicationDidFinishLaunching - startingUpdater: false, - updaterDelegate: updaterDelegate, - userDriverDelegate: nil - ) - super.init() ghostty.delegate = self @@ -183,7 +173,7 @@ class AppDelegate: NSObject, ghosttyConfigDidChange(config: ghostty.config) // Start our update checker. - updaterController.startUpdater() + updateController.startUpdater() // Register our service provider. This must happen after everything is initialized. NSApp.servicesProvider = ServiceProvider() @@ -810,12 +800,12 @@ class AppDelegate: NSObject, // defined by our "auto-update" configuration (if set) or fall back to Sparkle // user-based defaults. if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false { - updaterController.updater.automaticallyChecksForUpdates = false - updaterController.updater.automaticallyDownloadsUpdates = false + updateController.updater.automaticallyChecksForUpdates = false + updateController.updater.automaticallyDownloadsUpdates = false } else if let autoUpdate = config.autoUpdate { - updaterController.updater.automaticallyChecksForUpdates = + updateController.updater.automaticallyChecksForUpdates = autoUpdate == .check || autoUpdate == .download - updaterController.updater.automaticallyDownloadsUpdates = + updateController.updater.automaticallyDownloadsUpdates = autoUpdate == .download } @@ -1008,7 +998,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - UpdateSimulator.permissionRequest.simulate(with: updateViewModel) + updateController.checkForUpdates() + //UpdateSimulator.permissionRequest.simulate(with: updateViewModel) } diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift new file mode 100644 index 000000000..47e6c8def --- /dev/null +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -0,0 +1,55 @@ +import Sparkle +import Cocoa + +/// Standard controller for managing Sparkle updates in Ghostty. +/// +/// This controller wraps SPUStandardUpdaterController to provide a simpler interface +/// for managing updates with Ghostty's custom driver and delegate. It handles +/// initialization, starting the updater, and provides the check for updates action. +class UpdateController { + private(set) var updater: SPUUpdater + private let userDriver: UpdateDriver + private let updaterDelegate = UpdaterDelegate() + + var viewModel: UpdateViewModel { + userDriver.viewModel + } + + /// Initialize a new update controller. + init() { + let hostBundle = Bundle.main + self.userDriver = UpdateDriver(viewModel: .init()) + self.updater = SPUUpdater( + hostBundle: hostBundle, + applicationBundle: hostBundle, + userDriver: userDriver, + delegate: updaterDelegate + ) + } + + /// Start the updater. + /// + /// This must be called before the updater can check for updates. If starting fails, + /// an error alert will be shown after a short delay. + func startUpdater() { + try? updater.start() + } + + /// Check for updates. + /// + /// This is typically connected to a menu item action. + @objc func checkForUpdates() { + updater.checkForUpdates() + } + + /// Validate the check for updates menu item. + /// + /// - Parameter item: The menu item to validate + /// - Returns: Whether the menu item should be enabled + func validateMenuItem(_ item: NSMenuItem) -> Bool { + if item.action == #selector(checkForUpdates) { + return updater.canCheckForUpdates + } + return true + } +} diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 6627559e8..70f9341a6 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -1,13 +1,12 @@ +import Cocoa import Sparkle /// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation. class UpdateDriver: NSObject, SPUUserDriver { let viewModel: UpdateViewModel - let retryHandler: () -> Void - init(viewModel: UpdateViewModel, retryHandler: @escaping () -> Void) { + init(viewModel: UpdateViewModel) { self.viewModel = viewModel - self.retryHandler = retryHandler super.init() } @@ -38,9 +37,18 @@ class UpdateDriver: NSObject, SPUUserDriver { } func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { - viewModel.state = .error(.init(error: error, retry: retryHandler, dismiss: { [weak viewModel] in - viewModel?.state = .idle - })) + viewModel.state = .error(.init( + error: error, + retry: { + guard let delegate = NSApp.delegate as? AppDelegate else { + return + } + + // TODO fill this in + }, + dismiss: { [weak viewModel] in + viewModel?.state = .idle + })) } func showDownloadInitiated(cancellation: @escaping () -> Void) { From abab6899f93acc461f84a08cd333a6c93069dab1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:45:46 -0700 Subject: [PATCH 215/319] macos: better update descriptions --- .../Features/Update/UpdatePopoverView.swift | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 7f1886d60..ae1dc9c28 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -124,6 +124,8 @@ fileprivate struct UpdateAvailableView: View { let update: UpdateState.UpdateAvailable let dismiss: DismissAction + private let labelWidth: CGFloat = 60 + var body: some View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 12) { @@ -135,11 +137,32 @@ fileprivate struct UpdateAvailableView: View { HStack(spacing: 6) { Text("Version:") .foregroundColor(.secondary) - .frame(width: 50, alignment: .trailing) + .frame(width: labelWidth, alignment: .trailing) Text(update.appcastItem.displayVersionString) } .font(.system(size: 11)) + + if update.appcastItem.contentLength > 0 { + HStack(spacing: 6) { + Text("Size:") + .foregroundColor(.secondary) + .frame(width: labelWidth, alignment: .trailing) + Text(ByteCountFormatter.string(fromByteCount: Int64(update.appcastItem.contentLength), countStyle: .file)) + } + .font(.system(size: 11)) + } + + if let date = update.appcastItem.date { + HStack(spacing: 6) { + Text("Released:") + .foregroundColor(.secondary) + .frame(width: labelWidth, alignment: .trailing) + Text(date.formatted(date: .abbreviated, time: .omitted)) + } + .font(.system(size: 11)) + } } + .textSelection(.enabled) } HStack(spacing: 8) { From 49eb65df77c16baaf74e78fd99373266b15e5b98 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 21:51:21 -0700 Subject: [PATCH 216/319] macos: show release notes link --- .../Features/Update/UpdatePopoverView.swift | 22 +++ .../Features/Update/UpdateViewModel.swift | 70 ++++++++++ macos/Tests/Update/ReleaseNotesTests.swift | 130 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 macos/Tests/Update/ReleaseNotesTests.swift diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index ae1dc9c28..a73116ca0 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -191,6 +191,28 @@ fileprivate struct UpdateAvailableView: View { } } .padding(16) + + if let notes = update.releaseNotes { + Divider() + + Link(destination: notes.url) { + HStack { + Image(systemName: "doc.text") + .font(.system(size: 11)) + Text(notes.label) + .font(.system(size: 11, weight: .medium)) + Spacer() + Image(systemName: "arrow.up.right") + .font(.system(size: 10)) + } + .foregroundColor(.primary) + .padding(12) + .frame(maxWidth: .infinity) + .background(Color(nsColor: .controlBackgroundColor)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } } } } diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 674888bb5..7b6119771 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -173,6 +173,76 @@ enum UpdateState: Equatable { struct UpdateAvailable { let appcastItem: SUAppcastItem let reply: @Sendable (SPUUserUpdateChoice) -> Void + + var releaseNotes: ReleaseNotes? { + let currentCommit = Bundle.main.infoDictionary?["GhosttyCommit"] as? String + return ReleaseNotes(displayVersionString: appcastItem.displayVersionString, currentCommit: currentCommit) + } + } + + enum ReleaseNotes { + case commit(URL) + case compareTip(URL) + case tagged(URL) + + init?(displayVersionString: String, currentCommit: String?) { + let version = displayVersionString + + // Check for semantic version (x.y.z) + if let semver = Self.extractSemanticVersion(from: version) { + let slug = semver.replacingOccurrences(of: ".", with: "-") + if let url = URL(string: "https://ghostty.org/docs/install/release-notes/\(slug)") { + self = .tagged(url) + return + } + } + + // Fall back to git hash detection + guard let newHash = Self.extractGitHash(from: version) else { + return nil + } + + if let currentHash = currentCommit, !currentHash.isEmpty, + let url = URL(string: "https://github.com/ghostty-org/ghostty/compare/\(currentHash)...\(newHash)") { + self = .compareTip(url) + } else if let url = URL(string: "https://github.com/ghostty-org/ghostty/commit/\(newHash)") { + self = .commit(url) + } else { + return nil + } + } + + private static func extractSemanticVersion(from version: String) -> String? { + let pattern = #"^\d+\.\d+\.\d+$"# + if version.range(of: pattern, options: .regularExpression) != nil { + return version + } + return nil + } + + private static func extractGitHash(from version: String) -> String? { + let pattern = #"[0-9a-f]{7,40}"# + if let range = version.range(of: pattern, options: .regularExpression) { + return String(version[range]) + } + return nil + } + + var url: URL { + switch self { + case .commit(let url): return url + case .compareTip(let url): return url + case .tagged(let url): return url + } + } + + var label: String { + switch (self) { + case .commit: return "View GitHub Commit" + case .compareTip: return "Changes Since This Tip Release" + case .tagged: return "View Release Notes" + } + } } struct Error { diff --git a/macos/Tests/Update/ReleaseNotesTests.swift b/macos/Tests/Update/ReleaseNotesTests.swift new file mode 100644 index 000000000..b029fa6bc --- /dev/null +++ b/macos/Tests/Update/ReleaseNotesTests.swift @@ -0,0 +1,130 @@ +import Testing +import Foundation +@testable import Ghostty + +struct ReleaseNotesTests { + /// Test tagged release (semantic version) + @Test func testTaggedRelease() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "1.2.3", + currentCommit: nil + ) + + #expect(notes != nil) + if case .tagged(let url) = notes { + #expect(url.absoluteString == "https://ghostty.org/docs/install/release-notes/1-2-3") + #expect(notes?.label == "View Release Notes") + } else { + Issue.record("Expected tagged case") + } + } + + /// Test tip release comparison with current commit + @Test func testTipReleaseComparison() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-abc1234", + currentCommit: "def5678" + ) + + #expect(notes != nil) + if case .compareTip(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") + #expect(notes?.label == "Changes Since This Tip Release") + } else { + Issue.record("Expected compareTip case") + } + } + + /// Test tip release without current commit + @Test func testTipReleaseWithoutCurrentCommit() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-abc1234", + currentCommit: nil + ) + + #expect(notes != nil) + if case .commit(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") + #expect(notes?.label == "View GitHub Commit") + } else { + Issue.record("Expected commit case") + } + } + + /// Test tip release with empty current commit + @Test func testTipReleaseWithEmptyCurrentCommit() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-abc1234", + currentCommit: "" + ) + + #expect(notes != nil) + if case .commit(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") + } else { + Issue.record("Expected commit case") + } + } + + /// Test version with full 40-character hash + @Test func testFullGitHash() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-1234567890abcdef1234567890abcdef12345678", + currentCommit: nil + ) + + #expect(notes != nil) + if case .commit(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/1234567890abcdef1234567890abcdef12345678") + } else { + Issue.record("Expected commit case") + } + } + + /// Test version with no recognizable pattern + @Test func testInvalidVersion() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "unknown-version", + currentCommit: nil + ) + + #expect(notes == nil) + } + + /// Test semantic version with prerelease suffix should not match + @Test func testSemanticVersionWithSuffix() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "1.2.3-beta", + currentCommit: nil + ) + + // Should not match semantic version pattern, falls back to hash detection + #expect(notes == nil) + } + + /// Test semantic version with 4 components should not match + @Test func testSemanticVersionFourComponents() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "1.2.3.4", + currentCommit: nil + ) + + // Should not match pattern + #expect(notes == nil) + } + + /// Test version string with git hash embedded + @Test func testVersionWithEmbeddedHash() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "v2024.01.15-abc1234", + currentCommit: "def5678" + ) + + #expect(notes != nil) + if case .compareTip(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") + } else { + Issue.record("Expected compareTip case") + } + } +} From a2fbaec6136b6c58f21565c81475386dec55bb5c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 8 Oct 2025 22:18:33 -0700 Subject: [PATCH 217/319] macos: do not build updaters into iOS --- macos/Ghostty.xcodeproj/project.pbxproj | 7 +++++++ macos/Sources/Features/Update/UpdateDelegate.swift | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index e53f6d468..558937582 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -125,7 +125,14 @@ "Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift", "Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift", "Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift", + Features/Update/UpdateBadge.swift, + Features/Update/UpdateController.swift, Features/Update/UpdateDelegate.swift, + Features/Update/UpdateDriver.swift, + Features/Update/UpdatePill.swift, + Features/Update/UpdatePopoverView.swift, + Features/Update/UpdateSimulator.swift, + Features/Update/UpdateViewModel.swift, "Ghostty/FullscreenMode+Extension.swift", Ghostty/Ghostty.Command.swift, Ghostty/Ghostty.Error.swift, diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index 4699ba14a..1112c1f44 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -6,7 +6,7 @@ class UpdaterDelegate: NSObject, SPUUpdaterDelegate { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } - + // Sparkle supports a native concept of "channels" but it requires that // you share a single appcast file. We don't want to do that so we // do this instead. From f4b051a84c225a1aadc2fe16fcebd3fce24f2eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=92riks=20Remess?= Date: Thu, 9 Oct 2025 16:02:40 +0300 Subject: [PATCH 218/319] use app_version from build.zig.zon --- build.zig | 4 +++- src/build/Config.zig | 10 ++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/build.zig b/build.zig index 7b66af81a..d9d2272c5 100644 --- a/build.zig +++ b/build.zig @@ -2,6 +2,7 @@ const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); const buildpkg = @import("src/build/main.zig"); +const appVersion = @import("build.zig.zon").version; comptime { buildpkg.requireZig("0.15.1"); @@ -15,7 +16,8 @@ pub fn build(b: *std.Build) !void { // This defines all the available build options (e.g. `-D`). If you // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. - const config = try buildpkg.Config.init(b); + + const config = try buildpkg.Config.init(b, appVersion); const test_filters = b.option( [][]const u8, "test-filter", diff --git a/src/build/Config.zig b/src/build/Config.zig index 643dfe928..745fc926f 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -16,13 +16,6 @@ const expandPath = @import("../os/path.zig").expand; const gtk = @import("gtk.zig"); const GitVersion = @import("GitVersion.zig"); -/// The version of the next release. -/// -/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. -/// Until then this MUST match build.zig.zon and should always be the -/// _next_ version to release. -const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 1 }; - /// Standard build configuration options. optimize: std.builtin.OptimizeMode, target: std.Build.ResolvedTarget, @@ -69,7 +62,7 @@ emit_unicode_table_gen: bool = false, /// Environmental properties env: std.process.EnvMap, -pub fn init(b: *std.Build) !Config { +pub fn init(b: *std.Build, appVersion: []const u8) !Config { // Setup our standard Zig target and optimize options, i.e. // `-Doptimize` and `-Dtarget`. const optimize = b.standardOptimizeOption(.{}); @@ -217,6 +210,7 @@ pub fn init(b: *std.Build) !Config { // If an explicit version is given, we always use it. try std.SemanticVersion.parse(v) else version: { + const app_version = try std.SemanticVersion.parse(appVersion); // If no explicit version is given, we try to detect it from git. const vsn = GitVersion.detect(b) catch |err| switch (err) { // If Git isn't available we just make an unknown dev version. From ea5ea5f98ed90e482e12da35c8374fdbf9a5b163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=92riks=20Remess?= Date: Thu, 9 Oct 2025 16:47:27 +0300 Subject: [PATCH 219/319] set minimum required zig version from build.zig.zon --- build.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index d9d2272c5..205896390 100644 --- a/build.zig +++ b/build.zig @@ -3,9 +3,10 @@ const assert = std.debug.assert; const builtin = @import("builtin"); const buildpkg = @import("src/build/main.zig"); const appVersion = @import("build.zig.zon").version; +const minimumZigVersion = @import("build.zig.zon").minimum_zig_version; comptime { - buildpkg.requireZig("0.15.1"); + buildpkg.requireZig(minimumZigVersion); } pub fn build(b: *std.Build) !void { From 402c492d9467e0dfac1c28be3df6aec9fbac10dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=92riks=20Remess?= Date: Thu, 9 Oct 2025 17:07:58 +0300 Subject: [PATCH 220/319] set minimum required zig version from build.zig.zon in tests and dockerfile --- .github/workflows/test.yml | 8 ++++---- src/build/docker/debian/Dockerfile | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f78855290..6df4975b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -508,9 +508,9 @@ jobs: - name: Install zig shell: pwsh run: | - # Get the zig version from build.zig so that it only needs to be updated - $fileContent = Get-Content -Path "build.zig" -Raw - $pattern = 'buildpkg\.requireZig\("(.*?)"\);' + # Get the zig version from build.zig.zon so that it only needs to be updated + $fileContent = Get-Content -Path "build.zig.zon" -Raw + $pattern = 'minimum_zig_version\s*=\s*"([^"]+)"' $zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value $version = "zig-x86_64-windows-$zigVersion" Write-Output $version @@ -575,7 +575,7 @@ jobs: - name: Get required Zig version id: zig run: | - echo "version=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig)" >> $GITHUB_OUTPUT + echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT - name: Setup Cache uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile index 815d395cd..ffeef3d6a 100644 --- a/src/build/docker/debian/Dockerfile +++ b/src/build/docker/debian/Dockerfile @@ -24,12 +24,12 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \ WORKDIR /src -COPY ./build.zig /src +COPY ./build.zig ./build.zig.zon /src/ # Install zig # https://ziglang.org/download/ -RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$(uname -m)-linux-$ZIG_VERSION.tar.xz" && \ +RUN export ZIG_VERSION=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$(uname -m)-linux-$ZIG_VERSION.tar.xz" && \ tar -xf /tmp/zig.tar.xz -C /opt && \ rm /tmp/zig.tar.xz && \ ln -s "/opt/zig-$(uname -m)-linux-$ZIG_VERSION/zig" /usr/local/bin/zig @@ -41,4 +41,3 @@ RUN zig build \ -Dcpu=baseline RUN ./zig-out/bin/ghostty +version - From bbf875216f1d771daeb4fcc28a1a8c19fe67b43e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Oct 2025 08:51:26 -0700 Subject: [PATCH 221/319] macos: fix driver for retry to trigger update check again --- .../Features/Update/UpdateDriver.swift | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 70f9341a6..5ff29ef75 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -10,7 +10,8 @@ class UpdateDriver: NSObject, SPUUserDriver { super.init() } - func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { + func show(_ request: SPUUpdatePermissionRequest, + reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { viewModel.state = .permissionRequest(.init(request: request, reply: reply)) } @@ -18,7 +19,9 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel.state = .checking(.init(cancel: cancellation)) } - func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { + func showUpdateFound(with appcastItem: SUAppcastItem, + state: SPUUserUpdateState, + reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply)) } @@ -31,20 +34,22 @@ class UpdateDriver: NSObject, SPUUserDriver { // We don't do anything with release notes. See `showUpdateReleaseNotes` } - func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { + func showUpdateNotFoundWithError(_ error: any Error, + acknowledgement: @escaping () -> Void) { viewModel.state = .notFound - // TODO: Do we need to acknowledge? + acknowledgement() } - func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { + func showUpdaterError(_ error: any Error, + acknowledgement: @escaping () -> Void) { viewModel.state = .error(.init( error: error, - retry: { - guard let delegate = NSApp.delegate as? AppDelegate else { - return + retry: { [weak viewModel] in + viewModel?.state = .idle + DispatchQueue.main.async { + guard let delegate = NSApp.delegate as? AppDelegate else { return } + delegate.checkForUpdates(self) } - - // TODO fill this in }, dismiss: { [weak viewModel] in viewModel?.state = .idle From f2e5b8fb2dd0de1136c208654a6100dfdbd3187c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Oct 2025 08:57:48 -0700 Subject: [PATCH 222/319] macos: setup the standard sparkle driver for no-window scenario If there are no windows, we use the standard sparkle driver to drive the standard window-based update UI. --- macos/Sources/Features/Update/UpdateController.swift | 4 +++- macos/Sources/Features/Update/UpdateDriver.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 47e6c8def..8dc24698b 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -18,7 +18,9 @@ class UpdateController { /// Initialize a new update controller. init() { let hostBundle = Bundle.main - self.userDriver = UpdateDriver(viewModel: .init()) + self.userDriver = UpdateDriver( + viewModel: .init(), + hostBundle: hostBundle) self.updater = SPUUpdater( hostBundle: hostBundle, applicationBundle: hostBundle, diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 5ff29ef75..80064854c 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -4,9 +4,11 @@ import Sparkle /// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation. class UpdateDriver: NSObject, SPUUserDriver { let viewModel: UpdateViewModel + let standard: SPUStandardUserDriver - init(viewModel: UpdateViewModel) { + init(viewModel: UpdateViewModel, hostBundle: Bundle) { self.viewModel = viewModel + self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil) super.init() } From 36b3c1fa47a2c300a60f1473f32d6983119f6421 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Thu, 9 Oct 2025 12:36:17 -0700 Subject: [PATCH 223/319] deps: update z2d to v0.9.0 Release notes at: https://github.com/vancluever/z2d/blob/v0.9.0/CHANGELOG.md This release brings our Zig 0.15.x branch into main, now that Ghostty is on it too. Additionally, this adds major speedups to the default path (filling with a solid color using the default operator). --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 71e7cd1b4..8fe8daefb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -21,8 +21,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", - .hash = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", + .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", + .hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index b587ee5ef..c62a0ed85 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -139,10 +139,10 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef": { + "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": { "name": "z2d", - "url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", - "hash": "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg=" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", + "hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=" }, "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index dd59e709a..d699b454b 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -307,11 +307,11 @@ in }; } { - name = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef"; + name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T"; path = fetchZigArtifact { name = "z2d"; - url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz"; - hash = "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg="; + url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz"; + hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 9db796546..d8a390c40 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -26,7 +26,6 @@ https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.ta https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz -https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz @@ -34,3 +33,4 @@ https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90 https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz +https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index eb8f57028..3f573456a 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -169,9 +169,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", - "dest": "vendor/p/z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", - "sha256": "e7fa91640221d54e36bfb8ea97d5b48ebdb3cd066dbb7f43c493cb56b4b26c98" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", + "dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", + "sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10" }, { "type": "archive", From f124bb4975efaad430ff09aa4243075824cab359 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Oct 2025 17:08:21 -0700 Subject: [PATCH 224/319] macos: Fallback to standard driver when no unobtrusive targets exist --- .../Window Styles/TerminalWindow.swift | 14 +++ .../Features/Update/UpdateDriver.swift | 95 ++++++++++++++++++- .../Features/Update/UpdateViewModel.swift | 17 ++++ 3 files changed, 121 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 4737bacaf..8ffdc3e35 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -5,6 +5,12 @@ import GhosttyKit /// The base class for all standalone, "normal" terminal windows. This sets the basic /// style and configuration of the window based on the app configuration. class TerminalWindow: NSWindow { + /// Posted when a terminal window awakes from nib. + static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake") + + /// Posted when a terminal window will close + static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose") + /// This is the key in UserDefaults to use for the default `level` value. This is /// used by the manual float on top menu item feature. static let defaultLevelKey: String = "TerminalDefaultLevel" @@ -45,6 +51,9 @@ class TerminalWindow: NSWindow { } override func awakeFromNib() { + // Notify that this terminal window has loaded + NotificationCenter.default.post(name: Self.terminalDidAwake, object: self) + // This is required so that window restoration properly creates our tabs // again. I'm not sure why this is required. If you don't do this, then // tabs restore as separate windows. @@ -124,6 +133,11 @@ class TerminalWindow: NSWindow { // still become key/main and receive events. override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } + + override func close() { + NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self) + super.close() + } override func becomeKey() { super.becomeKey() diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 80064854c..cd1d051e2 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -10,21 +10,56 @@ class UpdateDriver: NSObject, SPUUserDriver { self.viewModel = viewModel self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil) super.init() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleTerminalWindowWillClose), + name: TerminalWindow.terminalWillCloseNotification, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func handleTerminalWindowWillClose() { + // If we lost the ability to show unobtrusive states, cancel whatever + // update state we're in. This will allow the manual `check for updates` + // call to initialize the standard driver. + // + // We have to do this after a short delay so that the window can fully + // close. + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in + guard let self else { return } + guard !hasUnobtrusiveTarget else { return } + viewModel.state.cancel() + viewModel.state = .idle + } } func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { viewModel.state = .permissionRequest(.init(request: request, reply: reply)) + if !hasUnobtrusiveTarget { + standard.show(request, reply: reply) + } } func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { viewModel.state = .checking(.init(cancel: cancellation)) + + if !hasUnobtrusiveTarget { + standard.showUserInitiatedUpdateCheck(cancellation: cancellation) + } } func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply)) + if !hasUnobtrusiveTarget { + standard.showUpdateFound(with: appcastItem, state: state, reply: reply) + } } func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { @@ -39,7 +74,12 @@ class UpdateDriver: NSObject, SPUUserDriver { func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { viewModel.state = .notFound - acknowledgement() + + if !hasUnobtrusiveTarget { + standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement) + } else { + acknowledgement() + } } func showUpdaterError(_ error: any Error, @@ -56,6 +96,12 @@ class UpdateDriver: NSObject, SPUUserDriver { dismiss: { [weak viewModel] in viewModel?.state = .idle })) + + if !hasUnobtrusiveTarget { + standard.showUpdaterError(error, acknowledgement: acknowledgement) + } else { + acknowledgement() + } } func showDownloadInitiated(cancellation: @escaping () -> Void) { @@ -63,6 +109,10 @@ class UpdateDriver: NSObject, SPUUserDriver { cancel: cancellation, expectedLength: nil, progress: 0)) + + if !hasUnobtrusiveTarget { + standard.showDownloadInitiated(cancellation: cancellation) + } } func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { @@ -74,6 +124,10 @@ class UpdateDriver: NSObject, SPUUserDriver { cancel: downloading.cancel, expectedLength: expectedContentLength, progress: 0)) + + if !hasUnobtrusiveTarget { + standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength) + } } func showDownloadDidReceiveData(ofLength length: UInt64) { @@ -85,36 +139,67 @@ class UpdateDriver: NSObject, SPUUserDriver { cancel: downloading.cancel, expectedLength: downloading.expectedLength, progress: downloading.progress + length)) + + if !hasUnobtrusiveTarget { + standard.showDownloadDidReceiveData(ofLength: length) + } } func showDownloadDidStartExtractingUpdate() { viewModel.state = .extracting(.init(progress: 0)) + + if !hasUnobtrusiveTarget { + standard.showDownloadDidStartExtractingUpdate() + } } func showExtractionReceivedProgress(_ progress: Double) { viewModel.state = .extracting(.init(progress: progress)) + + if !hasUnobtrusiveTarget { + standard.showExtractionReceivedProgress(progress) + } } func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { viewModel.state = .readyToInstall(.init(reply: reply)) + + if !hasUnobtrusiveTarget { + standard.showReady(toInstallAndRelaunch: reply) + } } func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { viewModel.state = .installing + + if !hasUnobtrusiveTarget { + standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication) + } } func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { - // We don't do anything here. + standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement) viewModel.state = .idle } func showUpdateInFocus() { - // We don't currently implement this because our update state is - // shown in a terminal window. We may want to implement this at some - // point to handle the case that no windows are open, though. + if !hasUnobtrusiveTarget { + standard.showUpdateInFocus() + } } func dismissUpdateInstallation() { viewModel.state = .idle + standard.dismissUpdateInstallation() + } + + // MARK: No-Window Fallback + + /// True if there is a target that can render our unobtrusive update checker. + var hasUnobtrusiveTarget: Bool { + NSApp.windows.contains { window in + window is TerminalWindow && + window.isVisible + } } } diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 7b6119771..0678997d7 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -134,6 +134,23 @@ enum UpdateState: Equatable { return false } + func cancel() { + switch self { + case .checking(let checking): + checking.cancel() + case .updateAvailable(let available): + available.reply(.dismiss) + case .downloading(let downloading): + downloading.cancel() + case .readyToInstall(let ready): + ready.reply(.dismiss) + case .error(let err): + err.dismiss() + default: + break + } + } + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): From ce47a85bf7fc5550cc6083583bf6e2549f1824d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=92riks=20Remess?= Date: Fri, 10 Oct 2025 14:40:42 +0300 Subject: [PATCH 225/319] gtk4-layer-shell: version from build.zig.zon --- pkg/gtk4-layer-shell/build.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/gtk4-layer-shell/build.zig b/pkg/gtk4-layer-shell/build.zig index b9cf78a23..451ffe9a7 100644 --- a/pkg/gtk4-layer-shell/build.zig +++ b/pkg/gtk4-layer-shell/build.zig @@ -1,7 +1,6 @@ const std = @import("std"); -// TODO: Import this from build.zig.zon when possible -const version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 0 }; +const version = @import("build.zig.zon").version; const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ .preferred_link_mode = .dynamic, @@ -32,6 +31,7 @@ pub fn build(b: *std.Build) !void { } fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile { + const lib_version = try std.SemanticVersion.parse(version); const target = options.target; const optimize = options.optimize; @@ -117,9 +117,9 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .root = upstream.path("src"), .files = srcs, .flags = &.{ - b.fmt("-DGTK_LAYER_SHELL_MAJOR={}", .{version.major}), - b.fmt("-DGTK_LAYER_SHELL_MINOR={}", .{version.minor}), - b.fmt("-DGTK_LAYER_SHELL_MICRO={}", .{version.patch}), + b.fmt("-DGTK_LAYER_SHELL_MAJOR={}", .{lib_version.major}), + b.fmt("-DGTK_LAYER_SHELL_MINOR={}", .{lib_version.minor}), + b.fmt("-DGTK_LAYER_SHELL_MICRO={}", .{lib_version.patch}), // Zig 0.14 regression: this is required because building with // ubsan results in unknown symbols. Bundling the ubsan/compiler From 82a5c177fed33a5c2f01ee3bc862388c5722a551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=92riks=20Remess?= Date: Fri, 10 Oct 2025 14:40:56 +0300 Subject: [PATCH 226/319] gtk4-layer-shell: reenable ubsan --- pkg/gtk4-layer-shell/build.zig | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/gtk4-layer-shell/build.zig b/pkg/gtk4-layer-shell/build.zig index 451ffe9a7..818b48f45 100644 --- a/pkg/gtk4-layer-shell/build.zig +++ b/pkg/gtk4-layer-shell/build.zig @@ -120,16 +120,6 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu b.fmt("-DGTK_LAYER_SHELL_MAJOR={}", .{lib_version.major}), b.fmt("-DGTK_LAYER_SHELL_MINOR={}", .{lib_version.minor}), b.fmt("-DGTK_LAYER_SHELL_MICRO={}", .{lib_version.patch}), - - // Zig 0.14 regression: this is required because building with - // ubsan results in unknown symbols. Bundling the ubsan/compiler - // RT doesn't help. I'm not sure what the root cause is but I - // suspect its related to this: - // https://github.com/ziglang/zig/issues/23052 - // - // We can remove this in the future for Zig updates and see - // if our binaries run in debug on NixOS. - "-fno-sanitize=undefined", }, }); From ba8eae027e7f5496df37bdd6acf30bf8f8b72854 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 07:18:54 -0700 Subject: [PATCH 227/319] macos: fixed width for downloading/extracting, better padding --- macos/Sources/Features/Terminal/TerminalView.swift | 4 ++-- .../Terminal/Window Styles/TerminalWindow.swift | 3 ++- macos/Sources/Features/Update/UpdatePill.swift | 13 ++++++++++++- macos/Sources/Features/Update/UpdateViewModel.swift | 13 +++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 51c4f6ddd..54cf9a02a 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -131,8 +131,8 @@ fileprivate struct UpdateOverlay: View { HStack { Spacer() UpdatePill(model: appDelegate.updateViewModel) - .padding(.bottom, 12) - .padding(.trailing, 12) + .padding(.bottom, 9) + .padding(.trailing, 9) } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 8ffdc3e35..661c89121 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -547,9 +547,10 @@ extension TerminalWindow { @ObservedObject var model: UpdateViewModel var body: some View { + // We use the same top/trailing padding so that it hugs the same. UpdatePill(model: model) .padding(.top, viewModel.accessoryTopPadding) - .padding(.trailing, 10) + .padding(.trailing, viewModel.accessoryTopPadding) } } diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index 1dc29e250..b975e81c9 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -8,6 +8,9 @@ struct UpdatePill: View { /// Whether the update popover is currently visible @State private var showPopover = false + /// The font used for the pill text + private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) + var body: some View { if !model.state.isIdle { pillButton @@ -43,8 +46,9 @@ struct UpdatePill: View { .frame(width: 14, height: 14) Text(model.text) - .font(.system(size: 11, weight: .medium)) + .font(Font(textFont)) .lineLimit(1) + .frame(width: textWidth) } .padding(.horizontal, 8) .padding(.vertical, 4) @@ -58,4 +62,11 @@ struct UpdatePill: View { .buttonStyle(.plain) .help(model.text) } + + /// Calculated width for the text to prevent resizing during progress updates + private var textWidth: CGFloat? { + let attributes: [NSAttributedString.Key: Any] = [.font: textFont] + let size = (model.maxWidthText as NSString).size(withAttributes: attributes) + return size.width + } } diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 0678997d7..6341b3b42 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -37,6 +37,19 @@ class UpdateViewModel: ObservableObject { } } + /// The maximum width text for states that show progress. + /// Used to prevent the pill from resizing as percentages change. + var maxWidthText: String { + switch state { + case .downloading: + return "Downloading: 100%" + case .extracting: + return "Preparing: 100%" + default: + return text + } + } + /// The SF Symbol icon name for the current update state. /// Returns nil for idle, downloading, and extracting states. var iconName: String? { From 6993947a3a8a8c92d849fa1fa23a9e9fa4016ea8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 07:11:22 -0700 Subject: [PATCH 228/319] macOS: Make a lot of things more robust --- macos/Sources/App/macOS/AppDelegate.swift | 4 ++-- macos/Sources/Features/Update/UpdateBadge.swift | 4 ++-- .../Features/Update/UpdateController.swift | 17 +++++++++++++++-- .../Sources/Features/Update/UpdateDriver.swift | 5 +++-- macos/Sources/Features/Update/UpdatePill.swift | 13 +++++++++---- .../Features/Update/UpdatePopoverView.swift | 6 +++--- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 216373e7e..cf717993a 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -998,8 +998,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - updateController.checkForUpdates() - //UpdateSimulator.permissionRequest.simulate(with: updateViewModel) + //updateController.checkForUpdates() + UpdateSimulator.happyPath.simulate(with: updateViewModel) } diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index fd1eb3498..afd0849be 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -17,14 +17,14 @@ struct UpdateBadge: View { switch model.state { case .downloading(let download): if let expectedLength = download.expectedLength, expectedLength > 0 { - let progress = Double(download.progress) / Double(expectedLength) + let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) ProgressRingView(progress: progress) } else { Image(systemName: "arrow.down.circle") } case .extracting(let extracting): - ProgressRingView(progress: extracting.progress) + ProgressRingView(progress: min(1, max(0, extracting.progress))) case .checking, .installing: if let iconName = model.iconName { diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 8dc24698b..446b82ebc 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -32,9 +32,22 @@ class UpdateController { /// Start the updater. /// /// This must be called before the updater can check for updates. If starting fails, - /// an error alert will be shown after a short delay. + /// the error will be shown to the user. func startUpdater() { - try? updater.start() + do { + try updater.start() + } catch { + userDriver.viewModel.state = .error(.init( + error: error, + retry: { [weak self] in + self?.userDriver.viewModel.state = .idle + self?.startUpdater() + }, + dismiss: { [weak self] in + self?.userDriver.viewModel.state = .idle + } + )) + } } /// Check for updates. diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index cd1d051e2..9196d9ad9 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -86,9 +86,10 @@ class UpdateDriver: NSObject, SPUUserDriver { acknowledgement: @escaping () -> Void) { viewModel.state = .error(.init( error: error, - retry: { [weak viewModel] in + retry: { [weak self, weak viewModel] in viewModel?.state = .idle - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self else { return } guard let delegate = NSApp.delegate as? AppDelegate else { return } delegate.checkForUpdates(self) } diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index b975e81c9..3b48ac218 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -8,6 +8,9 @@ struct UpdatePill: View { /// Whether the update popover is currently visible @State private var showPopover = false + /// Task for auto-dismissing the "No Updates" state + @State private var resetTask: Task? + /// The font used for the pill text private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) @@ -19,13 +22,15 @@ struct UpdatePill: View { } .transition(.opacity.combined(with: .scale(scale: 0.95))) .onChange(of: model.state) { newState in + resetTask?.cancel() if case .notFound = newState { - Task { + resetTask = Task { [weak model] in try? await Task.sleep(for: .seconds(5)) - if case .notFound = model.state { - model.state = .idle - } + guard !Task.isCancelled, case .notFound? = model?.state else { return } + model?.state = .idle } + } else { + resetTask = nil } } } diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index a73116ca0..7634d27de 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -228,7 +228,7 @@ fileprivate struct DownloadingView: View { .font(.system(size: 13, weight: .semibold)) if let expectedLength = download.expectedLength, expectedLength > 0 { - let progress = Double(download.progress) / Double(expectedLength) + let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) VStack(alignment: .leading, spacing: 6) { ProgressView(value: progress) Text(String(format: "%.0f%%", progress * 100)) @@ -264,8 +264,8 @@ fileprivate struct ExtractingView: View { .font(.system(size: 13, weight: .semibold)) VStack(alignment: .leading, spacing: 6) { - ProgressView(value: extracting.progress, total: 1.0) - Text(String(format: "%.0f%%", extracting.progress * 100)) + ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0) + Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100)) .font(.system(size: 11)) .foregroundColor(.secondary) } From 47f3c946401529ec7b2c90405be46ab7a4123629 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 08:34:42 -0700 Subject: [PATCH 229/319] macos: many more unit tests for update work --- AGENTS.md | 1 + macos/Tests/Update/UpdateStateTests.swift | 116 ++++++++++++++++++ macos/Tests/Update/UpdateViewModelTests.swift | 97 +++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 macos/Tests/Update/UpdateStateTests.swift create mode 100644 macos/Tests/Update/UpdateViewModelTests.swift diff --git a/AGENTS.md b/AGENTS.md index afa0fd1f2..5a885923e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,3 +29,4 @@ A file for [guiding coding agents](https://agents.md/). - Do not use `xcodebuild` - Use `zig build` to build the macOS app and any shared Zig code +- Run Xcode tests using `zig build test` diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift new file mode 100644 index 000000000..5a0832a5a --- /dev/null +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -0,0 +1,116 @@ +import Testing +import Foundation +import Sparkle +@testable import Ghostty + +struct UpdateStateTests { + // MARK: - Equatable Tests + + @Test func testIdleEquality() { + let state1: UpdateState = .idle + let state2: UpdateState = .idle + #expect(state1 == state2) + } + + @Test func testCheckingEquality() { + let state1: UpdateState = .checking(.init(cancel: {})) + let state2: UpdateState = .checking(.init(cancel: {})) + #expect(state1 == state2) + } + + @Test func testNotFoundEquality() { + let state1: UpdateState = .notFound + let state2: UpdateState = .notFound + #expect(state1 == state2) + } + + @Test func testInstallingEquality() { + let state1: UpdateState = .installing + let state2: UpdateState = .installing + #expect(state1 == state2) + } + + @Test func testPermissionRequestEquality() { + let request1 = SPUUpdatePermissionRequest(systemProfile: []) + let request2 = SPUUpdatePermissionRequest(systemProfile: []) + let state1: UpdateState = .permissionRequest(.init(request: request1, reply: { _ in })) + let state2: UpdateState = .permissionRequest(.init(request: request2, reply: { _ in })) + #expect(state1 == state2) + } + + @Test func testReadyToInstallEquality() { + let state1: UpdateState = .readyToInstall(.init(reply: { _ in })) + let state2: UpdateState = .readyToInstall(.init(reply: { _ in })) + #expect(state1 == state2) + } + + @Test func testDownloadingEqualityWithSameProgress() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + #expect(state1 == state2) + } + + @Test func testDownloadingInequalityWithDifferentProgress() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 600)) + #expect(state1 != state2) + } + + @Test func testDownloadingInequalityWithDifferentExpectedLength() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 2000, progress: 500)) + #expect(state1 != state2) + } + + @Test func testDownloadingEqualityWithNilExpectedLength() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) + #expect(state1 == state2) + } + + @Test func testExtractingEqualityWithSameProgress() { + let state1: UpdateState = .extracting(.init(progress: 0.5)) + let state2: UpdateState = .extracting(.init(progress: 0.5)) + #expect(state1 == state2) + } + + @Test func testExtractingInequalityWithDifferentProgress() { + let state1: UpdateState = .extracting(.init(progress: 0.5)) + let state2: UpdateState = .extracting(.init(progress: 0.6)) + #expect(state1 != state2) + } + + @Test func testErrorEqualityWithSameDescription() { + let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error message"]) + let error2 = NSError(domain: "Test", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error message"]) + let state1: UpdateState = .error(.init(error: error1, retry: {}, dismiss: {})) + let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) + #expect(state1 == state2) + } + + @Test func testErrorInequalityWithDifferentDescription() { + let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 1"]) + let error2 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 2"]) + let state1: UpdateState = .error(.init(error: error1, retry: {}, dismiss: {})) + let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) + #expect(state1 != state2) + } + + @Test func testDifferentStatesAreNotEqual() { + let state1: UpdateState = .idle + let state2: UpdateState = .checking(.init(cancel: {})) + #expect(state1 != state2) + } + + // MARK: - isIdle Tests + + @Test func testIsIdleTrue() { + let state: UpdateState = .idle + #expect(state.isIdle == true) + } + + @Test func testIsIdleFalse() { + let state: UpdateState = .checking(.init(cancel: {})) + #expect(state.isIdle == false) + } +} diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift new file mode 100644 index 000000000..dd88cbe83 --- /dev/null +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -0,0 +1,97 @@ +import Testing +import Foundation +import SwiftUI +import Sparkle +@testable import Ghostty + +struct UpdateViewModelTests { + // MARK: - Text Formatting Tests + + @Test func testIdleText() { + let viewModel = UpdateViewModel() + viewModel.state = .idle + #expect(viewModel.text == "") + } + + @Test func testPermissionRequestText() { + let viewModel = UpdateViewModel() + let request = SPUUpdatePermissionRequest(systemProfile: []) + viewModel.state = .permissionRequest(.init(request: request, reply: { _ in })) + #expect(viewModel.text == "Enable Automatic Updates?") + } + + @Test func testCheckingText() { + let viewModel = UpdateViewModel() + viewModel.state = .checking(.init(cancel: {})) + #expect(viewModel.text == "Checking for Updates…") + } + + @Test func testDownloadingTextWithKnownLength() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + #expect(viewModel.text == "Downloading: 50%") + } + + @Test func testDownloadingTextWithUnknownLength() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) + #expect(viewModel.text == "Downloading…") + } + + @Test func testDownloadingTextWithZeroExpectedLength() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: 0, progress: 500)) + #expect(viewModel.text == "Downloading…") + } + + @Test func testExtractingText() { + let viewModel = UpdateViewModel() + viewModel.state = .extracting(.init(progress: 0.75)) + #expect(viewModel.text == "Preparing: 75%") + } + + @Test func testReadyToInstallText() { + let viewModel = UpdateViewModel() + viewModel.state = .readyToInstall(.init(reply: { _ in })) + #expect(viewModel.text == "Install Update") + } + + @Test func testInstallingText() { + let viewModel = UpdateViewModel() + viewModel.state = .installing + #expect(viewModel.text == "Installing…") + } + + @Test func testNotFoundText() { + let viewModel = UpdateViewModel() + viewModel.state = .notFound + #expect(viewModel.text == "No Updates Available") + } + + @Test func testErrorText() { + let viewModel = UpdateViewModel() + let error = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Network error"]) + viewModel.state = .error(.init(error: error, retry: {}, dismiss: {})) + #expect(viewModel.text == "Network error") + } + + // MARK: - Max Width Text Tests + + @Test func testMaxWidthTextForDownloading() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 50)) + #expect(viewModel.maxWidthText == "Downloading: 100%") + } + + @Test func testMaxWidthTextForExtracting() { + let viewModel = UpdateViewModel() + viewModel.state = .extracting(.init(progress: 0.5)) + #expect(viewModel.maxWidthText == "Preparing: 100%") + } + + @Test func testMaxWidthTextForNonProgressState() { + let viewModel = UpdateViewModel() + viewModel.state = .checking(.init(cancel: {})) + #expect(viewModel.maxWidthText == viewModel.text) + } +} From 9dac88248f9761ef69ecb522ce8249b27b513d1c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 08:38:08 -0700 Subject: [PATCH 230/319] macos: ax for update info --- macos/Sources/Features/Update/UpdateBadge.swift | 10 ++++++++++ macos/Sources/Features/Update/UpdatePill.swift | 2 ++ 2 files changed, 12 insertions(+) diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index afd0849be..a4a95f411 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -14,6 +14,12 @@ struct UpdateBadge: View { @State private var rotationAngle: Double = 0 var body: some View { + badgeContent + .accessibilityLabel(model.text) + } + + @ViewBuilder + private var badgeContent: some View { switch model.state { case .downloading(let download): if let expectedLength = download.expectedLength, expectedLength > 0 { @@ -38,11 +44,15 @@ struct UpdateBadge: View { .onDisappear { rotationAngle = 0 } + } else { + EmptyView() } default: if let iconName = model.iconName { Image(systemName: iconName) + } else { + EmptyView() } } } diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index 3b48ac218..ff4af97dd 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -53,6 +53,7 @@ struct UpdatePill: View { Text(model.text) .font(Font(textFont)) .lineLimit(1) + .truncationMode(.tail) .frame(width: textWidth) } .padding(.horizontal, 8) @@ -66,6 +67,7 @@ struct UpdatePill: View { } .buttonStyle(.plain) .help(model.text) + .accessibilityLabel(model.text) } /// Calculated width for the text to prevent resizing during progress updates From e0ee10e9025614c8a6c35768e51c1659eed11b85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 08:44:24 -0700 Subject: [PATCH 231/319] macos: re-enable real update check --- macos/Sources/App/macOS/AppDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index cf717993a..3eff7b3e4 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -998,8 +998,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - //updateController.checkForUpdates() - UpdateSimulator.happyPath.simulate(with: updateViewModel) + updateController.checkForUpdates() + //UpdateSimulator.happyPath.simulate(with: updateViewModel) } From f0da093bdca19eb8087a43f6dd73ec90240b56a4 Mon Sep 17 00:00:00 2001 From: tlj Date: Fri, 10 Oct 2025 07:58:01 +0200 Subject: [PATCH 232/319] apprt/gtk: use configured title as fallback for closureComputedTitle --- src/apprt/gtk/class/tab.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 941fa00a9..c9928be8b 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -389,8 +389,14 @@ pub const Tab = extern struct { // the terminal title if it exists, otherwise a default string. const plain = plain: { const default = "Ghostty"; + const config_title: ?[*:0]const u8 = title: { + const config = config_ orelse break :title null; + break :title config.get().title orelse null; + }; + const plain = override_ orelse terminal_ orelse + config_title orelse break :plain default; break :plain std.mem.span(plain); }; From 2bf9c777d769e08596c62c7064fd8af086d70229 Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:21:29 +0200 Subject: [PATCH 233/319] Fix `macos-titlebar-tabs` related issues (#9090) ### This pr fixes multiple issues related to `macos-titlebar-tabs` - [Window title clipping **on Tahoe**](https://github.com/ghostty-org/ghostty/discussions/9027#discussion-8981483) - Clipped tab bar **on Tahoe** when creating new ones in fullscreen > Sequoia doesn't seem to have this issue (at least I didn't reproduce myself) - [Title missing **on Tahoe** after dragging a tab into a separate window](https://github.com/ghostty-org/ghostty/discussions/9027#discussioncomment-14617088) - [Clipped tab bar **on Tahoe** after dragging from one tab group to another](https://github.com/ghostty-org/ghostty/discussions/9027#discussioncomment-14626078) - [Stretched tab bar after switching system appearance](https://github.com/ghostty-org/ghostty/discussions/9027#discussioncomment-14626918) ### Related issues I checked all of the open sub-issues in #2349 , most of them should be fixed in latest main branch (I didn't reproduce) - [#1692](https://github.com/ghostty-org/ghostty/issues/1692) @peteschaffner - [#1945](https://github.com/ghostty-org/ghostty/issues/1945) this one I reproduce only on Tahoe, and fixed in this pr, @injust - [#1813](https://github.com/ghostty-org/ghostty/issues/1813) @jacakira - [#1787](https://github.com/ghostty-org/ghostty/issues/1787) @roguesherlock - [#1691](https://github.com/ghostty-org/ghostty/issues/1691) ~**haven't found a solution yet**(building zig on VM is a pain**)~ > Tried commenting out `isOpaque` check in `TitlebarTabsVenturaTerminalWindow`, which would fix the issue, but I see the note there about transparency issue. > > After commenting it out, it worked fine for me with blur and opacity config, so **I might need some more background on this**. Didn't include this change yet. > > [See screenshot here](https://github.com/user-attachments/assets/eb17642d-b0de-46b2-b42a-19fb38a2c7f0) ### Minor improvements - Match window title style with `window-title-font-family` and focus state image --------- Co-authored-by: Mitchell Hashimoto --- .../TitlebarTabsTahoeTerminalWindow.swift | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 855d29f52..4e067eddc 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -19,9 +19,21 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // MARK: NSWindow + override var titlebarFont: NSFont? { + didSet { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.viewModel.titleFont = self.titlebarFont + } + } + } + override var title: String { didSet { - viewModel.title = title + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.viewModel.title = self.title + } } } @@ -46,17 +58,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // Check if we have a tab bar and set it up if we have to. See the comment // on this function to learn why we need to check this here. setupTabBar() + + viewModel.isMainWindow = true } + override func resignMain() { + super.resignMain() + + viewModel.isMainWindow = false + } // This is called by macOS for native tabbing in order to add the tab bar. We hook into // this, detect the tab bar being added, and override its behavior. override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { // If this is the tab bar then we need to set it up for the titlebar guard isTabBar(childViewController) else { + // After dragging a tab into a new window, `hasTabBar` needs to be + // updated to properly review window title + viewModel.hasTabBar = false + super.addTitlebarAccessoryViewController(childViewController) return } + // When an existing tab is being dragged in to another tab group, + // system will also try to add tab bar to this window, so we want to reset observer, + // to put tab bar where we want again + tabBarObserver = nil + // Some setup needs to happen BEFORE it is added, such as layout. If // we don't do this before the call below, we'll trigger an AppKit // assertion. @@ -116,18 +144,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard tabBarObserver == nil else { return } // Find our tab bar. If it doesn't exist we don't do anything. - guard let tabBar = contentView?.rootView.firstDescendant(withClassName: "NSTabBar") else { return } + // + // In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`. + // In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes; + // in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`. + // ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205 + guard let themeFrameView = contentView?.rootView else { return } + let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) { + themeFrameView.value(forKey: "titlebarView") as? NSView + } else { + NSView?.none + } + guard let tabBar = titlebarView?.firstDescendant(withClassName: "NSTabBar") else { return } // View model updates must happen on their own ticks. - DispatchQueue.main.async { - self.viewModel.hasTabBar = true + DispatchQueue.main.async { [weak self] in + self?.viewModel.hasTabBar = true } // Find our clip view guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] else { return } - guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } + guard let titlebarView else { return } guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } + + // Make sure tabBar's height won't be stretched + guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return } + tabBar.frame.size.height = newTabButton.frame.width // The container is the view that we'll constrain our tab bar within. let container = toolbarView @@ -209,6 +252,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool case .title: let item = NSToolbarItem(itemIdentifier: .title) item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel)) + // Fix: https://github.com/ghostty-org/ghostty/discussions/9027 + item.view?.setContentCompressionResistancePriority(.required, for: .horizontal) item.visibilityPriority = .user item.isEnabled = true @@ -225,8 +270,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // MARK: SwiftUI class ViewModel: ObservableObject { + @Published var titleFont: NSFont? @Published var title: String = "👻 Ghostty" @Published var hasTabBar: Bool = false + @Published var isMainWindow: Bool = true } } @@ -249,15 +296,24 @@ extension TitlebarTabsTahoeTerminalWindow { var body: some View { if !viewModel.hasTabBar { - Text(title) - .lineLimit(1) - .truncationMode(.tail) + titleText } else { // 1x1.gif strikes again! For real: if we render a zero-sized // view here then the toolbar just disappears our view. I don't - // know. + // know. This appears fixed in 26.1 Beta but keep it safe for 26.0. Color.clear.frame(width: 1, height: 1) } } + + @ViewBuilder + var titleText: some View { + Text(title) + .font(viewModel.titleFont.flatMap(Font.init(_:))) + .foregroundStyle(viewModel.isMainWindow ? .primary : .secondary) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .greatestFiniteMagnitude, alignment: .center) + .opacity(viewModel.hasTabBar ? 0 : 1) // hide when in fullscreen mode, where title bar will appear in the leading area under window buttons + } } } From 207eccffda019f11e87d2af7c0a98c9223b228b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 09:30:12 -0700 Subject: [PATCH 234/319] macos: Sparkle notFound acknowledgement should only be called on dismiss (#9126) This was causing the "no update found" message to never really appear in the real world. --- macos/Sources/Features/Update/UpdateDriver.swift | 4 +--- macos/Sources/Features/Update/UpdatePill.swift | 6 ++++-- macos/Sources/Features/Update/UpdatePopoverView.swift | 6 ++++-- macos/Sources/Features/Update/UpdateSimulator.swift | 4 +++- macos/Sources/Features/Update/UpdateViewModel.swift | 8 +++++++- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 9196d9ad9..81477ef67 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -73,12 +73,10 @@ class UpdateDriver: NSObject, SPUUserDriver { func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { - viewModel.state = .notFound + viewModel.state = .notFound(.init(acknowledgement: acknowledgement)) if !hasUnobtrusiveTarget { standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement) - } else { - acknowledgement() } } diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index ff4af97dd..29d1669e1 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -23,11 +23,12 @@ struct UpdatePill: View { .transition(.opacity.combined(with: .scale(scale: 0.95))) .onChange(of: model.state) { newState in resetTask?.cancel() - if case .notFound = newState { + if case .notFound(let notFound) = newState { resetTask = Task { [weak model] in try? await Task.sleep(for: .seconds(5)) guard !Task.isCancelled, case .notFound? = model?.state else { return } model?.state = .idle + notFound.acknowledgement() } } else { resetTask = nil @@ -40,8 +41,9 @@ struct UpdatePill: View { @ViewBuilder private var pillButton: some View { Button(action: { - if case .notFound = model.state { + if case .notFound(let notFound) = model.state { model.state = .idle + notFound.acknowledgement() } else { showPopover.toggle() } diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 7634d27de..236649c21 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -41,8 +41,8 @@ struct UpdatePopoverView: View { case .installing: InstallingView() - case .notFound: - NotFoundView(dismiss: dismiss) + case .notFound(let notFound): + NotFoundView(notFound: notFound, dismiss: dismiss) case .error(let error): UpdateErrorView(error: error, dismiss: dismiss) @@ -331,6 +331,7 @@ fileprivate struct InstallingView: View { } fileprivate struct NotFoundView: View { + let notFound: UpdateState.NotFound let dismiss: DismissAction var body: some View { @@ -348,6 +349,7 @@ fileprivate struct NotFoundView: View { HStack { Spacer() Button("OK") { + notFound.acknowledgement() dismiss() } .keyboardShortcut(.defaultAction) diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift index 96fab4835..f40bbee1b 100644 --- a/macos/Sources/Features/Update/UpdateSimulator.swift +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -72,7 +72,9 @@ enum UpdateSimulator { })) DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - viewModel.state = .notFound + viewModel.state = .notFound(.init(acknowledgement: { + // Acknowledgement called when dismissed + })) DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { viewModel.state = .idle diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 6341b3b42..b0c6650c4 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -135,7 +135,7 @@ enum UpdateState: Equatable { case permissionRequest(PermissionRequest) case checking(Checking) case updateAvailable(UpdateAvailable) - case notFound + case notFound(NotFound) case error(Error) case downloading(Downloading) case extracting(Extracting) @@ -157,6 +157,8 @@ enum UpdateState: Equatable { downloading.cancel() case .readyToInstall(let ready): ready.reply(.dismiss) + case .notFound(let notFound): + notFound.acknowledgement() case .error(let err): err.dismiss() default: @@ -191,6 +193,10 @@ enum UpdateState: Equatable { } } + struct NotFound { + let acknowledgement: () -> Void + } + struct PermissionRequest { let request: SPUUpdatePermissionRequest let reply: @Sendable (SUUpdatePermissionResponse) -> Void From c8ea42b8943e2fb49aa68bdeaaf695eb493b95be Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:31:55 +0200 Subject: [PATCH 235/319] macOS: Fix New Tab behaviours (#9124) - Fix `macos-dock-drop-behavior = new-tab` not working, which also affects `open /path/to/directory -a Ghostty.app` - Fix 'New Tab' in dock icon not working **when Ghostty's not active** ### Issue preview with `1.2.2(12187)` https://github.com/user-attachments/assets/18068cc2-c25d-4360-97ab-cec22d5d3ff4 --- macos/Sources/App/macOS/AppDelegate.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 3eff7b3e4..6f387f4ae 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -465,7 +465,12 @@ class AppDelegate: NSObject, } switch ghostty.config.macosDockDropBehavior { - case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config) + case .new_tab: + _ = TerminalController.newTab( + ghostty, + from: TerminalController.preferredParent?.window, + withBaseConfig: config + ) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } @@ -1002,13 +1007,15 @@ class AppDelegate: NSObject, //UpdateSimulator.happyPath.simulate(with: updateViewModel) } - @IBAction func newWindow(_ sender: Any?) { _ = TerminalController.newWindow(ghostty) } @IBAction func newTab(_ sender: Any?) { - _ = TerminalController.newTab(ghostty) + _ = TerminalController.newTab( + ghostty, + from: TerminalController.preferredParent?.window + ) } @IBAction func closeAllWindows(_ sender: Any?) { From bac2419343e921f57ff56a6f2cd5e5d9ded8d6c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 09:45:32 -0700 Subject: [PATCH 236/319] macos: fix unit tests for notFound change --- macos/Tests/Update/UpdateStateTests.swift | 4 ++-- macos/Tests/Update/UpdateViewModelTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift index 5a0832a5a..01819bb25 100644 --- a/macos/Tests/Update/UpdateStateTests.swift +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -19,8 +19,8 @@ struct UpdateStateTests { } @Test func testNotFoundEquality() { - let state1: UpdateState = .notFound - let state2: UpdateState = .notFound + let state1: UpdateState = .notFound(.init(acknowledgement: {})) + let state2: UpdateState = .notFound(.init(acknowledgement: {})) #expect(state1 == state2) } diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift index dd88cbe83..d3b2e060b 100644 --- a/macos/Tests/Update/UpdateViewModelTests.swift +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -64,7 +64,7 @@ struct UpdateViewModelTests { @Test func testNotFoundText() { let viewModel = UpdateViewModel() - viewModel.state = .notFound + viewModel.state = .notFound(.init(acknowledgement: {})) #expect(viewModel.text == "No Updates Available") } From 7767a4577989b788baa8ebe74b9e6c9cd37e5cbb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 10 Oct 2025 12:00:50 -0500 Subject: [PATCH 237/319] osc: do inplace decoding of cmdline passed in OSC 133;C (#9127) --- src/os/string_encoding.zig | 267 +++++++++++++++++++++++++++++++++++++ src/terminal/osc.zig | 255 ++++++++++++++++++++++++++++++++--- 2 files changed, 504 insertions(+), 18 deletions(-) create mode 100644 src/os/string_encoding.zig diff --git a/src/os/string_encoding.zig b/src/os/string_encoding.zig new file mode 100644 index 000000000..162023ad2 --- /dev/null +++ b/src/os/string_encoding.zig @@ -0,0 +1,267 @@ +const std = @import("std"); + +/// Do an in-place decode of a string that has been encoded in the same way +/// that `bash`'s `printf %q` encodes a string. This is safe because a string +/// can only get shorter after decoding. This destructively modifies the buffer +/// given to it. If an error is returned the buffer may be in an unusable state. +pub fn printfQDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { + const data: [:0]u8 = data: { + // Strip off `$''` quoting. + if (std.mem.startsWith(u8, buf, "$'")) { + if (buf.len < 3 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError; + buf[buf.len - 1] = 0; + break :data buf[2 .. buf.len - 1 :0]; + } + // Strip off `''` quoting. + if (std.mem.startsWith(u8, buf, "'")) { + if (buf.len < 2 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError; + buf[buf.len - 1] = 0; + break :data buf[1 .. buf.len - 1 :0]; + } + break :data buf; + }; + + var src: usize = 0; + var dst: usize = 0; + + while (src < data.len) { + switch (data[src]) { + else => { + data[dst] = data[src]; + src += 1; + dst += 1; + }, + '\\' => { + if (src + 1 >= data.len) return error.DecodeError; + switch (data[src + 1]) { + ' ', + '\\', + '"', + '\'', + '$', + => |c| { + data[dst] = c; + src += 2; + dst += 1; + }, + 'e' => { + data[dst] = std.ascii.control_code.esc; + src += 2; + dst += 1; + }, + 'n' => { + data[dst] = std.ascii.control_code.lf; + src += 2; + dst += 1; + }, + 'r' => { + data[dst] = std.ascii.control_code.cr; + src += 2; + dst += 1; + }, + 't' => { + data[dst] = std.ascii.control_code.ht; + src += 2; + dst += 1; + }, + 'v' => { + data[dst] = std.ascii.control_code.vt; + src += 2; + dst += 1; + }, + else => return error.DecodeError, + } + }, + } + } + + data[dst] = 0; + return data[0..dst :0]; +} + +test "printf_q 1" { + const s: [:0]const u8 = "bobr\\ kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "printf_q 2" { + const s: [:0]const u8 = "bobr\\nkurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr\nkurwa", dst); +} + +test "printf_q 3" { + const s: [:0]const u8 = "bobr\\dkurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 4" { + const s: [:0]const u8 = "bobr kurwa\\"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 5" { + const s: [:0]const u8 = "$'bobr kurwa'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "printf_q 6" { + const s: [:0]const u8 = "'bobr kurwa'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "printf_q 7" { + const s: [:0]const u8 = "$'bobr kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 8" { + const s: [:0]const u8 = "$'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 9" { + const s: [:0]const u8 = "'bobr kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 10" { + const s: [:0]const u8 = "'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +/// Do an in-place decode of a string that has been URL percent encoded. +/// This is safe because a string can only get shorter after decoding. This +/// destructively modifies the buffer given to it. If an error is returned the +/// buffer may be in an unusable state. +pub fn urlPercentDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { + var src: usize = 0; + var dst: usize = 0; + while (src < buf.len) { + switch (buf[src]) { + else => { + buf[dst] = buf[src]; + src += 1; + dst += 1; + }, + '%' => { + if (src + 2 >= buf.len) return error.DecodeError; + switch (buf[src + 1]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + switch (buf[src + 2]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + buf[dst] = std.math.shl(u8, hex(buf[src + 1]), 4) | hex(buf[src + 2]); + src += 3; + dst += 1; + }, + else => return error.DecodeError, + } + }, + else => return error.DecodeError, + } + }, + } + } + buf[dst] = 0; + return buf[0..dst :0]; +} + +inline fn hex(c: u8) u4 { + switch (c) { + '0'...'9' => return @truncate(c - '0'), + 'a'...'f' => return @truncate(c - 'a' + 10), + 'A'...'F' => return @truncate(c - 'A' + 10), + else => unreachable, + } +} + +test "singles percent" { + for (0..255) |c| { + var buf_: [4]u8 = undefined; + const buf = try std.fmt.bufPrintZ(&buf_, "%{x:0>2}", .{c}); + const decoded = try urlPercentDecode(buf); + try std.testing.expectEqual(1, decoded.len); + try std.testing.expectEqual(c, decoded[0]); + } + for (0..255) |c| { + var buf_: [4]u8 = undefined; + const buf = try std.fmt.bufPrintZ(&buf_, "%{X:0>2}", .{c}); + const decoded = try urlPercentDecode(buf); + try std.testing.expectEqual(1, decoded.len); + try std.testing.expectEqual(c, decoded[0]); + } +} + +test "percent 1" { + const s: [:0]const u8 = "bobr%20kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try urlPercentDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "percent 2" { + const s: [:0]const u8 = "bobr%2kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 3" { + const s: [:0]const u8 = "bobr%kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 4" { + const s: [:0]const u8 = "bobr%%kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 5" { + const s: [:0]const u8 = "bobr%20kurwa%20"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try urlPercentDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa ", dst); +} + +test "percent 6" { + const s: [:0]const u8 = "bobr%20kurwa%2"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 7" { + const s: [:0]const u8 = "bobr%20kurwa%"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 4b0f9553c..12a6d1f5c 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -15,6 +15,7 @@ const LibEnum = @import("../lib/enum.zig").Enum; const RGB = @import("color.zig").RGB; const kitty_color = @import("kitty/color.zig"); const osc_color = @import("osc/color.zig"); +const string_encoding = @import("../os/string_encoding.zig"); pub const color = osc_color; const log = std.log.scoped(.osc); @@ -89,12 +90,7 @@ pub const Command = union(Key) { end_of_input: struct { /// The command line that the user entered. /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - cmdline: ?union(enum) { - /// The command line has been encoded with bash's 'printf "%q"'. - printf_q_encoded: [:0]const u8, - /// The command line has been encoded with URL percent encoding. - percent_encoded: [:0]const u8, - } = null, + cmdline: ?[:0]const u8 = null, }, /// End of current command. @@ -1482,17 +1478,13 @@ pub const Parser = struct { } else if (mem.eql(u8, self.temp_state.key, "cmdline")) { // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers switch (self.command) { - .end_of_input => |*v| v.cmdline = .{ - .printf_q_encoded = value, - }, + .end_of_input => |*v| v.cmdline = string_encoding.printfQDecode(value) catch null, else => {}, } } else if (mem.eql(u8, self.temp_state.key, "cmdline_url")) { // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers switch (self.command) { - .end_of_input => |*v| v.cmdline = .{ - .percent_encoded = value, - }, + .end_of_input => |*v| v.cmdline = string_encoding.urlPercentDecode(value) catch null, else => {}, } } else if (mem.eql(u8, self.temp_state.key, "redraw")) { @@ -3063,7 +3055,7 @@ test "OSC 133: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC 133: end_of_input with cmdline" { +test "OSC 133: end_of_input with cmdline 1" { const testing = std.testing; var p: Parser = .init(); @@ -3074,11 +3066,132 @@ test "OSC 133: end_of_input with cmdline" { const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_input); try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expect(cmd.end_of_input.cmdline.? == .printf_q_encoded); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?.printf_q_encoded); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); } -test "OSC 133: end_of_input with cmdline_url" { +test "OSC 133: end_of_input with cmdline 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=echo bobr\\ kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 3" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=echo bobr\\nkurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr\nkurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 4" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=$'echo bobr kurwa'"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 5" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline='echo bobr kurwa'"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 6" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline='echo bobr kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 7" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=$'echo bobr kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 8" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=$'"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 9" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline=$'"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 10" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline="; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 1" { const testing = std.testing; var p: Parser = .init(); @@ -3089,8 +3202,114 @@ test "OSC 133: end_of_input with cmdline_url" { const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_input); try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expect(cmd.end_of_input.cmdline.? == .percent_encoded); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?.percent_encoded); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr%20kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 3" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr%3bkurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr;kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 4" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr%3kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 5" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr%kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 6" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr%kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 7" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr kurwa%20"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa ", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 8" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr kurwa%2"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 9" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "133;C;cmdline_url=echo bobr kurwa%2"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); } test "OSC: OSC 777 show desktop notification with title" { From c28104e62fe552db6846c58b75c0e0e682c660d0 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 10 Oct 2025 12:01:06 -0500 Subject: [PATCH 238/319] gtk: properly check for amount of time elapsed before notifying about command finish (#9128) --- src/apprt/gtk/class/surface.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cc8359b7e..cc17e3470 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -838,6 +838,8 @@ pub const Surface = extern struct { if (cfg.@"notify-on-command-finish" == .unfocused and self.getFocused()) return true; } + if (value.duration.lte(cfg.@"notify-on-command-finish-after")) return true; + const action = cfg.@"notify-on-command-finish-action"; if (action.bell) self.setBellRinging(true); From cd7621167fb9be3f3d7b3f547283f71771941a86 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 10:19:33 -0700 Subject: [PATCH 239/319] macos: update accessory in the titlebar should not move the window This is annoyingly easy to trigger, just disable this. --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../Terminal/Window Styles/TerminalWindow.swift | 2 +- macos/Sources/Helpers/NonDraggableHostingView.swift | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 macos/Sources/Helpers/NonDraggableHostingView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 558937582..c2baf834d 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -167,6 +167,7 @@ Helpers/KeyboardLayout.swift, Helpers/LastWindowPosition.swift, Helpers/MetalView.swift, + Helpers/NonDraggableHostingView.swift, Helpers/PermissionRequest.swift, Helpers/Private/CGS.swift, Helpers/Private/Dock.swift, diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 661c89121..95126e188 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -108,7 +108,7 @@ class TerminalWindow: NSWindow { // Create update notification accessory if supportsUpdateAccessory { updateAccessory.layoutAttribute = .right - updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( + updateAccessory.view = NonDraggableHostingView(rootView: UpdateAccessoryView( viewModel: viewModel, model: appDelegate.updateViewModel )) diff --git a/macos/Sources/Helpers/NonDraggableHostingView.swift b/macos/Sources/Helpers/NonDraggableHostingView.swift new file mode 100644 index 000000000..26238182f --- /dev/null +++ b/macos/Sources/Helpers/NonDraggableHostingView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +/// An NSHostingView subclass that prevents window dragging when clicking on the view. +/// +/// By default, NSHostingViews in the titlebar allow the window to be dragged when +/// clicked. This subclass overrides `mouseDownCanMoveWindow` to return false, +/// preventing the window from being dragged when the user clicks on this view. +/// +/// This is useful for titlebar accessories that contain interactive elements +/// (buttons, links, etc.) where you don't want accidental window dragging. +class NonDraggableHostingView: NSHostingView { + override var mouseDownCanMoveWindow: Bool { false } +} From ac2f040b3140f4a77353d0c5f63909b36835a337 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Oct 2025 13:40:35 -0700 Subject: [PATCH 240/319] macos: Show "Update and Restart" in the Command Palette (#9131) If an update is available, you can now trigger the full download, install, and restart from a single command palette action. This allows for a fully keyboard-driven update process. While an update is being installed, an option to cancel or skip the current update is also shown as an option, so that can also be keyboard-driven. This currently can't be bound to a keyboard action, but that may be added in the future if there's demand for it. **AI Disclosure:** Amp was used considerably. I reviewed all the code and understand it. ## Demo https://github.com/user-attachments/assets/df6307f8-9967-40d4-9a62-04feddf00ac2 --- .../Command Palette/CommandPalette.swift | 50 +++++++++- .../TerminalCommandPalette.swift | 48 +++++++++- .../Features/Terminal/TerminalView.swift | 3 +- .../Features/Update/UpdateController.swift | 34 +++++++ .../Features/Update/UpdateViewModel.swift | 92 ++++++++++++++++++- 5 files changed, 216 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 8d15cbf9a..537137fe6 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -5,7 +5,28 @@ struct CommandOption: Identifiable, Hashable { let title: String let description: String? let symbols: [String]? + let leadingIcon: String? + let badge: String? + let emphasis: Bool let action: () -> Void + + init( + title: String, + description: String? = nil, + symbols: [String]? = nil, + leadingIcon: String? = nil, + badge: String? = nil, + emphasis: Bool = false, + action: @escaping () -> Void + ) { + self.title = title + self.description = description + self.symbols = symbols + self.leadingIcon = leadingIcon + self.badge = badge + self.emphasis = emphasis + self.action = action + } static func == (lhs: CommandOption, rhs: CommandOption) -> Bool { lhs.id == rhs.id @@ -198,7 +219,7 @@ fileprivate struct CommandTable: View { } else { ScrollViewReader { proxy in ScrollView { - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 4) { ForEach(Array(options.enumerated()), id: \.1.id) { index, option in CommandRow( option: option, @@ -240,15 +261,36 @@ fileprivate struct CommandRow: View { var body: some View { Button(action: action) { - HStack { + HStack(spacing: 8) { + if let icon = option.leadingIcon { + Image(systemName: icon) + .foregroundStyle(option.emphasis ? Color.accentColor : .secondary) + .font(.system(size: 14, weight: .medium)) + } + Text(option.title) + .fontWeight(option.emphasis ? .medium : .regular) + Spacer() + + if let badge = option.badge, !badge.isEmpty { + Text(badge) + .font(.caption2.weight(.medium)) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background( + Capsule().fill(Color.accentColor.opacity(0.15)) + ) + .foregroundStyle(Color.accentColor) + } + if let symbols = option.symbols { ShortcutSymbolsView(symbols: symbols) .foregroundStyle(.secondary) } } .padding(8) + .contentShape(Rectangle()) .background( isSelected ? Color.accentColor.opacity(0.2) @@ -256,6 +298,10 @@ fileprivate struct CommandRow: View { ? Color.secondary.opacity(0.2) : Color.clear) ) + .overlay( + RoundedRectangle(cornerRadius: 5) + .strokeBorder(Color.accentColor.opacity(option.emphasis && !isSelected ? 0.3 : 0), lineWidth: 1.5) + ) .cornerRadius(5) } .help(option.description ?? "") diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index d02828494..673f5dd78 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -11,15 +11,54 @@ struct TerminalCommandPaletteView: View { /// The configuration so we can lookup keyboard shortcuts. @ObservedObject var ghosttyConfig: Ghostty.Config + + /// The update view model for showing update commands. + var updateViewModel: UpdateViewModel? /// The callback when an action is submitted. var onAction: ((String) -> Void) // The commands available to the command palette. private var commandOptions: [CommandOption] { - guard let surface = surfaceView.surfaceModel else { return [] } + var options: [CommandOption] = [] + + // Add update command if an update is installable. This must always be the first so + // it is at the top. + if let updateViewModel, updateViewModel.state.isInstallable { + // We override the update available one only because we want to properly + // convey it'll go all the way through. + let title: String + if case .updateAvailable = updateViewModel.state { + title = "Update Ghostty and Restart" + } else { + title = updateViewModel.text + } + + options.append(CommandOption( + title: title, + description: updateViewModel.description, + leadingIcon: updateViewModel.iconName ?? "shippingbox.fill", + badge: updateViewModel.badge, + emphasis: true + ) { + (NSApp.delegate as? AppDelegate)?.updateController.installUpdate() + }) + } + + // Add cancel/skip update command if the update is installable + if let updateViewModel, updateViewModel.state.isInstallable { + options.append(CommandOption( + title: "Cancel or Skip Update", + description: "Dismiss the current update process" + ) { + updateViewModel.state.cancel() + }) + } + + // Add terminal commands + guard let surface = surfaceView.surfaceModel else { return options } do { - return try surface.commands().map { c in + let terminalCommands = try surface.commands().map { c in return CommandOption( title: c.title, description: c.description, @@ -28,9 +67,12 @@ struct TerminalCommandPaletteView: View { onAction(c.action) } } + options.append(contentsOf: terminalCommands) } catch { - return [] + return options } + + return options } var body: some View { diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 54cf9a02a..0cdff7c1f 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -108,7 +108,8 @@ struct TerminalView: View { TerminalCommandPaletteView( surfaceView: surfaceView, isPresented: $viewModel.commandPaletteIsShowing, - ghosttyConfig: ghostty.config) { action in + ghosttyConfig: ghostty.config, + updateViewModel: (NSApp.delegate as? AppDelegate)?.updateViewModel) { action in self.delegate?.performAction(action, on: surfaceView) } } diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 446b82ebc..aa875567c 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -1,5 +1,6 @@ import Sparkle import Cocoa +import Combine /// Standard controller for managing Sparkle updates in Ghostty. /// @@ -10,6 +11,7 @@ class UpdateController { private(set) var updater: SPUUpdater private let userDriver: UpdateDriver private let updaterDelegate = UpdaterDelegate() + private var installCancellable: AnyCancellable? var viewModel: UpdateViewModel { userDriver.viewModel @@ -29,6 +31,10 @@ class UpdateController { ) } + deinit { + installCancellable?.cancel() + } + /// Start the updater. /// /// This must be called before the updater can check for updates. If starting fails, @@ -50,6 +56,34 @@ class UpdateController { } } + /// Force install the current update. As long as we're in some "update available" state this will + /// trigger all the steps necessary to complete the update. + func installUpdate() { + // Must be in an installable state + guard viewModel.state.isInstallable else { return } + + // If we're already force installing then do nothing. + guard installCancellable == nil else { return } + + // Setup a combine listener to listen for state changes and to always + // confirm them. If we go to a non-installable state, cancel the listener. + // The sink runs immediately with the current state, so we don't need to + // manually confirm the first state. + installCancellable = viewModel.$state.sink { [weak self] state in + guard let self else { return } + + // If we move to a non-installable state (error, idle, etc.) then we + // stop force installing. + guard state.isInstallable else { + self.installCancellable = nil + return + } + + // Continue the `yes` chain! + state.confirm() + } + } + /// Check for updates. /// /// This is typically connected to a menu item action. diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index b0c6650c4..ccb03e731 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -17,7 +17,11 @@ class UpdateViewModel: ObservableObject { case .checking: return "Checking for Updates…" case .updateAvailable(let update): - return "Update Available: \(update.appcastItem.displayVersionString)" + let version = update.appcastItem.displayVersionString + if !version.isEmpty { + return "Update Available: \(version)" + } + return "Update Available" case .downloading(let download): if let expectedLength = download.expectedLength, expectedLength > 0 { let progress = Double(download.progress) / Double(expectedLength) @@ -51,7 +55,6 @@ class UpdateViewModel: ObservableObject { } /// The SF Symbol icon name for the current update state. - /// Returns nil for idle, downloading, and extracting states. var iconName: String? { switch state { case .idle: @@ -61,9 +64,11 @@ class UpdateViewModel: ObservableObject { case .checking: return "arrow.triangle.2.circlepath" case .updateAvailable: - return "arrow.down.circle.fill" - case .downloading, .extracting: - return nil + return "shippingbox.fill" + case .downloading: + return "arrow.down.circle" + case .extracting: + return "shippingbox" case .readyToInstall: return "checkmark.circle.fill" case .installing: @@ -75,6 +80,53 @@ class UpdateViewModel: ObservableObject { } } + /// A longer description for the current update state. + /// Used in contexts like the command palette where more detail is helpful. + var description: String { + switch state { + case .idle: + return "" + case .permissionRequest: + return "Configure automatic update preferences" + case .checking: + return "Please wait while we check for available updates" + case .updateAvailable(let update): + return update.releaseNotes?.label ?? "Download and install the latest version" + case .downloading: + return "Downloading the update package" + case .extracting: + return "Extracting and preparing the update" + case .readyToInstall: + return "Update is ready to install" + case .installing: + return "Installing update and preparing to restart" + case .notFound: + return "You are running the latest version" + case .error: + return "An error occurred during the update process" + } + } + + /// A badge to display for the current update state. + /// Returns version numbers, progress percentages, or nil. + var badge: String? { + switch state { + case .updateAvailable(let update): + let version = update.appcastItem.displayVersionString + return version.isEmpty ? nil : version + case .downloading(let download): + if let expectedLength = download.expectedLength, expectedLength > 0 { + let percentage = Double(download.progress) / Double(expectedLength) * 100 + return String(format: "%.0f%%", percentage) + } + return nil + case .extracting(let extracting): + return String(format: "%.0f%%", extracting.progress * 100) + default: + return nil + } + } + /// The color to apply to the icon for the current update state. var iconColor: Color { switch state { @@ -147,6 +199,22 @@ enum UpdateState: Equatable { return false } + /// This is true if we're in a state that can be force installed. + var isInstallable: Bool { + switch (self) { + case .checking, + .updateAvailable, + .downloading, + .extracting, + .readyToInstall, + .installing: + return true + + default: + return false + } + } + func cancel() { switch self { case .checking(let checking): @@ -166,6 +234,20 @@ enum UpdateState: Equatable { } } + /// Confirms or accepts the current update state. + /// - For available updates: begins installation + /// - For ready-to-install: proceeds with installation + func confirm() { + switch self { + case .updateAvailable(let available): + available.reply(.install) + case .readyToInstall(let ready): + ready.reply(.install) + default: + break + } + } + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): From 854c8e6975806c86765dbc2af5222d07cf28e3d7 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 10 Oct 2025 21:41:38 +0100 Subject: [PATCH 241/319] Set title as argv[0] for commands specified with `-e` (#9121) I want to see #7932 get merged, so applied the latest proposed patch. Will close if the original PR gets some traction, as I do _not_ know Zig nor this project. Co-authored-by: rhodes-b <59537185+rhodes-b@users.noreply.github.com> --- src/Surface.zig | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index b1553edff..82d6615b6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -702,7 +702,22 @@ pub fn init( .set_title, .{ .title = title }, ); - } + } else if (command) |cmd| switch (cmd) { + // If a user specifies a command it is appropriate to set the title as argv[0] + // we know in the case of a direct command it has been supplied by the user + .direct => |cmd_str| if (cmd_str.len != 0) { + _ = try rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = cmd_str[0] }, + ); + }, + + // We won't set the title in the case the shell expands the command + // as that should typically be used to launch a shell which should + // set its own titles + .shell => {}, + }; // We are no longer the first surface app.first = false; From c5ad7563f92656ec02bd08856b46431f2e222e69 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 10 Oct 2025 15:41:58 -0500 Subject: [PATCH 242/319] gtk: better reporting for CSS parsing problems (#9129) Log messages will include the problematic CSS, simplifying debugging. Especially helpful since some of our CSS is generated at runtime so it could be difficult to examine the CSS "source". ``` info(gtk_ghostty_application): loading gtk-custom-css path=/home/ghostty/dev/ghostty/x.css warning(gtk_ghostty_application): css parsing failed at :2:3-14: gtk-css-parser-error-quark 4 No property named "border-poop" * { border-poop: 0; warning(gtk_ghostty_application): css parsing failed at :1:3-3:1: gtk-css-parser-warning-quark 1 Unterminated block at end of document * { border-poop: 0; ``` vs: ``` info(gtk_ghostty_application): loading gtk-custom-css path=/home/ghostty/dev/ghostty/x.css warning(glib): WARNING: Gtk: Theme parser error: :2:3-14: No property named "border-poop" warning(glib): WARNING: Gtk: Theme parser warning: :1:3-3:1: Unterminated block at end of document ``` --- src/apprt/gtk/class/application.zig | 107 ++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index a35cd5b3f..d75a0ef7f 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -394,6 +394,14 @@ pub const Application = extern struct { .{ .detail = "config" }, ); + _ = gtk.CssProvider.signals.parsing_error.connect( + css_provider, + *Self, + signalCssParsingError, + self, + .{}, + ); + // Trigger initial config changes self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); @@ -812,8 +820,8 @@ pub const Application = extern struct { fn loadRuntimeCss(self: *Self) (Allocator.Error || std.Io.Writer.Error)!void { const alloc = self.allocator(); - - const config = self.private().config.get(); + const priv: *Private = self.private(); + const config = priv.config.get(); var buf: std.Io.Writer.Allocating = try .initCapacity(alloc, 2048); defer buf.deinit(); @@ -862,19 +870,15 @@ pub const Application = extern struct { , .{ .font_family = font_family }); } - // ensure that we have a sentinel - try writer.writeByte(0); + const contents = buf.written(); - const data_ = buf.written(); - const data = data_[0 .. data_.len - 1 :0]; + log.debug("runtime CSS is {d} bytes", .{contents.len}); - log.debug("runtime CSS is {d} bytes", .{data.len + 1}); + const bytes = glib.Bytes.new(contents.ptr, contents.len); + defer bytes.unref(); // Clears any previously loaded CSS from this provider - loadCssProviderFromData( - self.private().css_provider, - data, - ); + priv.css_provider.loadFromBytes(bytes); } /// Load runtime CSS for older than GTK 4.16 @@ -1013,8 +1017,8 @@ pub const Application = extern struct { } } - fn loadCustomCss(self: *Self) !void { - const priv = self.private(); + fn loadCustomCss(self: *Self) (std.fs.File.ReadError || Allocator.Error)!void { + const priv: *Private = self.private(); const alloc = self.allocator(); const display = gdk.Display.getDefault() orelse { log.warn("unable to get display", .{}); @@ -1031,7 +1035,7 @@ pub const Application = extern struct { } priv.custom_css_providers.clearRetainingCapacity(); - const config = priv.config.getMut(); + const config = priv.config.get(); for (config.@"gtk-custom-css".value.items) |p| { const path, const optional = switch (p) { .optional => |path| .{ path, true }, @@ -1048,23 +1052,42 @@ pub const Application = extern struct { }; defer file.close(); + const css_file_size_limit = 5 * 1024 * 1024; // 5MB + log.info("loading gtk-custom-css path={s}", .{path}); - const contents = try file.readToEndAlloc( + const contents = file.readToEndAlloc( alloc, - 5 * 1024 * 1024, // 5MB, - ); + css_file_size_limit, + ) catch |err| switch (err) { + error.FileTooBig => { + log.warn("gtk-custom-css file {s} was larger than {Bi}", .{ path, css_file_size_limit }); + continue; + }, + else => |e| return e, + }; defer alloc.free(contents); - const data = try alloc.dupeZ(u8, contents); - defer alloc.free(data); + const bytes = glib.Bytes.new(contents.ptr, contents.len); + defer bytes.unref(); + + const css_provider = gtk.CssProvider.new(); + errdefer css_provider.unref(); + + _ = gtk.CssProvider.signals.parsing_error.connect( + css_provider, + *Self, + signalCssParsingError, + self, + .{}, + ); + + try priv.custom_css_providers.append(alloc, css_provider); + + css_provider.loadFromBytes(bytes); - const provider = gtk.CssProvider.new(); - errdefer provider.unref(); - try priv.custom_css_providers.append(alloc, provider); - loadCssProviderFromData(provider, data); gtk.StyleContext.addProviderForDisplay( display, - provider.as(gtk.StyleProvider), + css_provider.as(gtk.StyleProvider), gtk.STYLE_PROVIDER_PRIORITY_USER, ); } @@ -1180,6 +1203,37 @@ pub const Application = extern struct { }; } + /// Log CSS parsing error + fn signalCssParsingError( + _: *gtk.CssProvider, + css_section: *gtk.CssSection, + err: *glib.Error, + _: *Self, + ) callconv(.c) void { + const location = css_section.toString(); + defer glib.free(location); + if (comptime gtk_version.atLeast(4, 16, 0)) bytes: { + const bytes = css_section.getBytes() orelse break :bytes; + var len: usize = undefined; + const ptr = bytes.getData(&len) orelse break :bytes; + const data = ptr[0..len]; + log.warn("css parsing failed at {s}: {s} {d} {s}\n{s}", .{ + location, + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "«unknown»", + data, + }); + return; + } + log.warn("css parsing failed at {s}: {s} {d} {s}", .{ + location, + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "«unknown»", + }); + } + //--------------------------------------------------------------- // Libghostty Callbacks @@ -2583,8 +2637,3 @@ fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) // Abusing integers to be enums and booleans is a terrible idea, C. return if (window.isActive() != 0) 0 else -1; } - -fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void { - assert(gtk_version.runtimeAtLeast(4, 12, 0)); - provider.loadFromString(data); -} From 81c982df96985c398fca41acc28a386bcb121679 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 11 Oct 2025 14:51:52 -0500 Subject: [PATCH 243/319] gtk: fix clicking on desktop notifications (#9146) Clicking on desktop notifications sent by Ghostty _should_ cause the window that sent the notification to come to the top. However, because the notification that was sent targeted the wrong surface (apprt surface vs core surface) and the window did not call `present()` on itself the window would never be brought to the surface, the correct tab would not be selected, etc. Fixes #9145 --- src/apprt/gtk/class/surface.zig | 8 +++++++- src/apprt/gtk/class/window.zig | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cc17e3470..51e4ea7b2 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1468,6 +1468,12 @@ pub const Surface = extern struct { pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void { const app = Application.default(); + const priv: *Private = self.private(); + + const core_surface = priv.core_surface orelse { + log.warn("can't send notification because there is no core surface", .{}); + return; + }; const t = switch (title.len) { 0 => "Ghostty", @@ -1482,7 +1488,7 @@ pub const Surface = extern struct { defer icon.unref(); notification.setIcon(icon.as(gio.Icon)); - const pointer = glib.Variant.newUint64(@intFromPtr(self)); + const pointer = glib.Variant.newUint64(@intFromPtr(core_surface)); notification.setDefaultActionAndTargetValue( "app.present-surface", pointer, diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 8efff8729..746bcd379 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1585,6 +1585,9 @@ pub const Window = extern struct { // Grab focus surface.grabFocus(); + + // Bring the window to the front. + self.as(gtk.Window).present(); } fn surfaceToggleFullscreen( From c7058143c76aede87ba6d51efff4cc9990719d7d Mon Sep 17 00:00:00 2001 From: Brice <59537185+rhodes-b@users.noreply.github.com> Date: Sat, 11 Oct 2025 14:52:35 -0500 Subject: [PATCH 244/319] GTK fix quick terminal autohide (#9139) This is pretty much a direct port of the previous GTK app. still inside of the `isActive` handler for a window https://github.com/ghostty-org/ghostty/blob/7e429d73d6af65a397c6264b18ab60609ae8eefe/src/apprt/gtk/Window.zig#L822-L837 Fixes: https://github.com/ghostty-org/ghostty/discussions/9137 --- src/apprt/gtk/class/window.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 746bcd379..4febebfc6 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1015,6 +1015,15 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { + // Hide quick-terminal if set to autohide + if (self.isQuickTerminal()) { + if (self.getConfig()) |cfg| { + if (cfg.get().@"quick-terminal-autohide" and self.as(gtk.Window).isActive() == 0) { + self.toggleVisibility(); + } + } + } + // Don't change urgency if we're not the active window. if (self.as(gtk.Window).isActive() == 0) return; From 6ab416376a6ba529bca55203435f61cb9ac6afbf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Oct 2025 12:54:23 -0700 Subject: [PATCH 245/319] chore: mark the nerd font tables as a generated file. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 6bf5ceb13..fa59a364c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,4 +9,5 @@ pkg/glfw/wayland-headers/** linguist-vendored pkg/libintl/config.h linguist-generated=true pkg/libintl/libintl.h linguist-generated=true pkg/simdutf/vendor/** linguist-vendored +src/font/nerd_font_codepoint_tables.py linguist-generated=true src/terminal/res/** linguist-vendored From 7087eea1e23297fb23ffb79b04a6c119b8acfb98 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Oct 2025 13:08:09 -0700 Subject: [PATCH 246/319] chore: add nerd font attributes as generated too --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index fa59a364c..87f1eb32e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,5 +9,6 @@ pkg/glfw/wayland-headers/** linguist-vendored pkg/libintl/config.h linguist-generated=true pkg/libintl/libintl.h linguist-generated=true pkg/simdutf/vendor/** linguist-vendored +src/font/nerd_font_attributes.zig linguist-generated=true src/font/nerd_font_codepoint_tables.py linguist-generated=true src/terminal/res/** linguist-vendored From 4af93975ed69ade2fcb4a915bab7d81dcbd5ebb8 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 11 Oct 2025 13:12:13 -0700 Subject: [PATCH 247/319] font(fix): Extract and apply Nerd Font codepoint mapping table (#9142) Fixes #9076 **Before** Screenshot 2025-10-11 at 00 07 09 **After** Screenshot 2025-10-11 at 00 07 31 These screenshots show the chevrons mentioned in https://github.com/ghostty-org/ghostty/discussions/7820#discussioncomment-14617170, which should be scaled as a group but were not until this PR. The added code downloads each individual symbol font file from the Nerd Fonts github repo (making sure to get the version corresponding to the vendored `font-patcher.py`) and iterates over all of them to build the correct and complete codepoint mapping table. The table is saved to `nerd_font_codepoint_tables.py`, which `nerd_font_codegen.py` will reuse if possible instead of downloading the font files again. I'm not going to utter any famous last words or anything, but... after this, I don't think the number of remaining issues with icon scaling/alignment is _large._ --- src/font/nerd_font_attributes.zig | 1730 +--- src/font/nerd_font_codegen.py | 228 +- src/font/nerd_font_codepoint_tables.py | 10449 +++++++++++++++++++++++ 3 files changed, 10971 insertions(+), 1436 deletions(-) create mode 100644 src/font/nerd_font_codepoint_tables.py diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 138108288..73fcf0bd3 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -310,33 +310,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .pad_bottom = -0.01, .max_xy_ratio = 0.7, }, - 0xe300, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8984375000000000, - .relative_y = 0.0986328125000000, - }, - 0xe301, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8798828125000000, - .relative_y = 0.1171875000000000, - }, - 0xe302, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7646484375000000, - .relative_y = 0.2314453125000000, - }, 0xe303, => .{ .size = .fit_cover1, @@ -355,187 +328,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9755859375000000, .relative_y = 0.0244140625000000, }, - 0xe305, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9960937500000000, - .relative_y = 0.0019531250000000, - }, - 0xe306, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9863281250000000, - .relative_y = 0.0097656250000000, - }, - 0xe307, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9951171875000000, - .relative_y = 0.0039062500000000, - }, - 0xe308, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9785156250000000, - .relative_y = 0.0195312500000000, - }, - 0xe309, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9736328125000000, - .relative_y = 0.0214843750000000, - }, - 0xe30a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9648437500000000, - .relative_y = 0.0302734375000000, - }, - 0xe30b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8437500000000000, - .relative_y = 0.1513671875000000, - }, - 0xe30c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8027343750000000, - .relative_y = 0.1835937500000000, - }, - 0xe30d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7753906250000000, - .relative_y = 0.1083984375000000, - }, - 0xe30e, - 0xe365, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9833984375000000, - .relative_y = 0.0166015625000000, - }, - 0xe30f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9716796875000000, - .relative_y = 0.0263671875000000, - }, - 0xe310, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6621093750000000, - .relative_y = 0.0986328125000000, - }, - 0xe311, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6425781250000000, - .relative_y = 0.1171875000000000, - }, - 0xe312, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5322265625000000, - .relative_y = 0.2314453125000000, - }, - 0xe313, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6416015625000000, - .relative_y = 0.1181640625000000, - }, - 0xe314, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7382812500000000, - .relative_y = 0.0195312500000000, - }, - 0xe315, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6787109375000000, - .relative_y = 0.1357421875000000, - }, - 0xe316, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7480468750000000, - .relative_y = 0.0097656250000000, - }, - 0xe317, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7529296875000000, - .relative_y = 0.0048828125000000, - }, - 0xe318, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7314453125000000, - .relative_y = 0.0263671875000000, - }, 0xe319, => .{ .size = .fit_cover1, @@ -599,24 +391,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7363281250000000, .relative_y = 0.0986328125000000, }, - 0xe320, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7177734375000000, - .relative_y = 0.1171875000000000, - }, - 0xe321, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8085937500000000, - .relative_y = 0.0253906250000000, - }, 0xe322, => .{ .size = .fit_cover1, @@ -626,32 +400,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7509765625000000, .relative_y = 0.0839843750000000, }, - 0xe323, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8281250000000000, - .relative_y = 0.0097656250000000, - }, - 0xe324, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8349609375000000, - }, - 0xe325, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8154296875000000, - .relative_y = 0.0214843750000000, - }, 0xe326, => .{ .size = .fit_cover1, @@ -661,7 +409,43 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8144531250000000, .relative_y = 0.0195312500000000, }, - 0xe327, + 0xe347, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5273437500000000, + .relative_y = 0.2617187500000000, + }, + 0xe34f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5751953125000000, + .relative_y = 0.1142578125000000, + }, + 0xe35f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9648437500000000, + .relative_y = 0.0302734375000000, + }, + 0xe360, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7695312500000000, + .relative_y = 0.0302734375000000, + }, + 0xe361, => .{ .size = .fit_cover1, .height = .icon, @@ -670,482 +454,15 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8076171875000000, .relative_y = 0.0273437500000000, }, - 0xe328, + 0xe363, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.6845703125000000, - .relative_y = 0.1503906250000000, - }, - 0xe329, - 0xe367, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8173828125000000, - .relative_y = 0.0175781250000000, - }, - 0xe32a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8105468750000000, - .relative_y = 0.0263671875000000, - }, - 0xe32b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5175781250000000, - .relative_y = 0.2421875000000000, - }, - 0xe32c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6992187500000000, - .relative_y = 0.1005859375000000, - }, - 0xe32d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6787109375000000, - .relative_y = 0.1201171875000000, - }, - 0xe32e, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5654296875000000, - .relative_y = 0.2324218750000000, - }, - 0xe32f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7714843750000000, - .relative_y = 0.0273437500000000, - }, - 0xe330, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7148437500000000, - .relative_y = 0.0830078125000000, - }, - 0xe331, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7919921875000000, + .relative_height = 0.7900390625000000, .relative_y = 0.0097656250000000, }, - 0xe332, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7871093750000000, - .relative_y = 0.0126953125000000, - }, - 0xe333, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7714843750000000, - .relative_y = 0.0263671875000000, - }, - 0xe334, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7773437500000000, - .relative_y = 0.0195312500000000, - }, - 0xe335, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7714843750000000, - .relative_y = 0.0283203125000000, - }, - 0xe336, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6503906250000000, - .relative_y = 0.1503906250000000, - }, - 0xe337, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7753906250000000, - .relative_y = 0.0234375000000000, - }, - 0xe338, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7792968750000000, - .relative_y = 0.0185546875000000, - }, - 0xe339, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4882812500000000, - .relative_y = 0.2109375000000000, - }, - 0xe33a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5283203125000000, - .relative_y = 0.2324218750000000, - }, - 0xe33b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5449218750000000, - .relative_y = 0.2148437500000000, - }, - 0xe33c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6006674082313682, - .relative_y = 0.1952169076751947, - }, - 0xe33d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5273437500000000, - .relative_y = 0.2324218750000000, - }, - 0xe33e, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.1904296875000000, - .relative_y = 0.5986328125000000, - }, - 0xe33f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.3300781250000000, - .relative_y = 0.3544921875000000, - }, - 0xe340, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5273437500000000, - .relative_y = 0.2373046875000000, - }, - 0xe341, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4814453125000000, - .relative_y = 0.2138671875000000, - }, - 0xe343, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6816406250000000, - .relative_y = 0.1591796875000000, - }, - 0xe344, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.3369140625000000, - .relative_y = 0.3154296875000000, - }, - 0xe345, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6073507601038191, - .relative_y = 0.1629495736002966, - }, - 0xe348, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6347656250000000, - .relative_y = 0.1826171875000000, - }, - 0xe349, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.3402542969850663, - .relative_y = 0.3471400394477318, - }, - 0xe34b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6621093750000000, - .relative_y = 0.1689453125000000, - }, - 0xe34c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8662109375000000, - .relative_y = 0.1337890625000000, - }, - 0xe34d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9892578125000000, - }, - 0xe351, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7992831541218638, - .relative_y = 0.0919952210274791, - }, - 0xe352, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4050179211469534, - .relative_y = 0.3739545997610514, - }, - 0xe353, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7040688830943068, - .relative_y = 0.1811983920034767, - }, - 0xe356, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7564102564102564, - .relative_y = 0.1213017751479290, - }, - 0xe357, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7509765625000000, - .relative_y = 0.1230468750000000, - }, - 0xe358, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7490234375000000, - .relative_y = 0.1250000000000000, - }, - 0xe359, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7643248629795715, - .relative_y = 0.1121076233183857, - }, - 0xe35a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7643248629795715, - .relative_y = 0.1111111111111111, - }, - 0xe35b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7683109118086696, - .relative_y = 0.1111111111111111, - }, - 0xe35c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9895366218236173, - }, - 0xe35d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5590433482810164, - .relative_y = 0.2152466367713005, - }, - 0xe35e, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7443946188340808, - .relative_y = 0.0134529147982063, - }, - 0xe35f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9845540607872446, - .relative_y = 0.0154459392127554, - }, - 0xe360, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7852516193323368, - .relative_y = 0.0154459392127554, - }, - 0xe361, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8241155954160438, - .relative_y = 0.0124564025909317, - }, - 0xe364, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8251953125000000, - .relative_y = 0.0097656250000000, - }, - 0xe366, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7832031250000000, - .relative_y = 0.0166015625000000, - }, - 0xe369, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4902343750000000, - .relative_y = 0.2548828125000000, - }, - 0xe36b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9335937500000000, - .relative_y = 0.0263671875000000, - }, 0xe36c, => .{ .size = .fit_cover1, @@ -1164,31 +481,14 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8427734375000000, .relative_y = 0.0625000000000000, }, - 0xe36e, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8712355686563498, - .relative_y = 0.0383689511176615, - }, - 0xe371, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6163708086785010, - .relative_y = 0.1903353057199211, - }, 0xe372, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.9725209080047790, + .relative_height = 0.7949218750000000, + .relative_y = 0.0576171875000000, }, 0xe373, => .{ @@ -1196,62 +496,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8737672583826430, - .relative_y = 0.0009861932938856, - }, - 0xe374, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.3185404339250493, - .relative_y = 0.2840236686390533, - }, - 0xe375, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6839250493096647, - .relative_y = 0.1267258382642998, - }, - 0xe376, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7061143984220908, - .relative_y = 0.1301775147928994, - }, - 0xe377, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7386587771203156, - .relative_y = 0.1518737672583826, - }, - 0xe378, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7386587771203156, - .relative_y = 0.1508875739644970, - }, - 0xe379, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5808678500986193, - .relative_y = 0.1794871794871795, + .relative_height = 0.8652343750000000, + .relative_y = 0.0058593750000000, }, 0xe37a, => .{ @@ -1259,8 +505,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.5315581854043393, - .relative_y = 0.2258382642998027, + .relative_height = 0.5263671875000000, + .relative_y = 0.2285156250000000, }, 0xe37b, => .{ @@ -1268,17 +514,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.5808678500986193, - .relative_y = 0.1804733727810651, - }, - 0xe37d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9003906250000000, - .relative_y = 0.0957031250000000, + .relative_height = 0.5751953125000000, + .relative_y = 0.1835937500000000, }, 0xe37e, => .{ @@ -1289,27 +526,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.6015625000000000, .relative_y = 0.2324218750000000, }, - 0xe37f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.3300781250000000, - .relative_y = 0.3593750000000000, - }, - 0xe380, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.3300781250000000, - .relative_y = 0.3496093750000000, - }, 0xe381...0xe383, - 0xe385...0xe388, - 0xf451...0xf453, + 0xe386, + 0xe388, + 0xe38b, => .{ .size = .fit_cover1, .height = .icon, @@ -1318,65 +538,27 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7500000000000000, .relative_y = 0.1250000000000000, }, - 0xe389...0xe38c, + 0xe38d, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.9987004548408057, - .relative_height = 0.9974025974025974, - .relative_y = 0.0012987012987013, + .relative_height = 0.7519531250000000, + .relative_y = 0.1240234375000000, }, - 0xe38e...0xe391, - 0xe394, + 0xe390, + 0xe393, + 0xe396, + 0xe3a3, + 0xe3a6...0xe3a7, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_width = 0.4990253411306043, - .relative_height = 0.9987012987012988, - .relative_x = 0.4996751137102014, - }, - 0xe392...0xe393, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_width = 0.4996751137102014, - .relative_height = 0.9987012987012988, - .relative_x = 0.4990253411306043, - }, - 0xe395...0xe396, - 0xe39b, - 0xe3a2...0xe3a8, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7568897637795275, - .relative_y = 0.1190944881889764, - }, - 0xe397...0xe39a, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7578740157480315, - .relative_y = 0.1190944881889764, - }, - 0xe39c, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7529527559055118, - .relative_y = 0.1190944881889764, + .relative_height = 0.7509765625000000, + .relative_y = 0.1240234375000000, }, 0xe39d, => .{ @@ -1384,8 +566,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7539370078740157, - .relative_y = 0.1190944881889764, + .relative_height = 0.7480468750000000, + .relative_y = 0.1240234375000000, }, 0xe39e...0xe3a0, => .{ @@ -1393,8 +575,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7549212598425197, - .relative_y = 0.1190944881889764, + .relative_height = 0.7490234375000000, + .relative_y = 0.1240234375000000, }, 0xe3a1, => .{ @@ -1402,26 +584,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7559055118110236, - .relative_y = 0.1190944881889764, - }, - 0xe3a9, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7568897637795275, - .relative_y = 0.1181102362204724, - }, - 0xe3aa, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9980314960629921, - .relative_y = 0.0019685039370079, + .relative_height = 0.7500000000000000, + .relative_y = 0.1240234375000000, }, 0xe3ab, => .{ @@ -1429,7 +593,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7962598425196851, + .relative_height = 0.7900390625000000, + .relative_y = 0.0058593750000000, }, 0xe3ac, => .{ @@ -1437,8 +602,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8316929133858267, - .relative_y = 0.0019685039370079, + .relative_height = 0.8251953125000000, + .relative_y = 0.0078125000000000, }, 0xe3ad, => .{ @@ -1446,56 +611,13 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7578740157480315, - .relative_y = 0.0009842519685039, - }, - 0xe3ae, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6200787401574803, - .relative_y = 0.2283464566929134, + .relative_height = 0.7519531250000000, + .relative_y = 0.0068359375000000, }, 0xe3af, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7057086614173228, - .relative_y = 0.1456692913385827, - }, - 0xe3b0, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7037401574803149, - .relative_y = 0.1476377952755905, - }, - 0xe3b1, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7125062282012955, - .relative_y = 0.1400099651220728, - }, - 0xe3b2, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6982421875000000, - .relative_y = 0.1523437500000000, - }, 0xe3b3, - 0xe3b5...0xe3b6, + 0xe3b5, + 0xe3b7...0xe3bb, => .{ .size = .fit_cover1, .height = .icon, @@ -1504,6 +626,15 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7001953125000000, .relative_y = 0.1503906250000000, }, + 0xe3b0...0xe3b1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6982421875000000, + .relative_y = 0.1523437500000000, + }, 0xe3b4, => .{ .size = .fit_cover1, @@ -1513,99 +644,40 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7011718750000000, .relative_y = 0.1494140625000000, }, - 0xe3b7...0xe3bb, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7000244081034903, - .relative_y = 0.1505979985355138, - }, - 0xe3bc, - 0xe3c0, - 0xe3c3, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9997559189650964, - .relative_y = 0.0002440810349036, - }, - 0xe3bd, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9431291188674640, - .relative_y = 0.0285574810837198, - }, - 0xe3be, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9896346920510943, - .relative_y = 0.0051257017329753, - }, - 0xe3bf, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9060288015621186, - .relative_y = 0.0471076397363925, - }, - 0xe3c1, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.6590187942396876, - .relative_y = 0.1349768123016842, - }, - 0xe3c2, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7939956065413717, - }, - 0xe3c9...0xe3ca, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9175627240143369, - .relative_y = 0.0824372759856631, - }, 0x23fb...0x23fe, 0x2665, 0x26a1, 0x2b58, 0xe000...0xe00a, 0xe200...0xe2a9, - 0xe342, - 0xe346...0xe347, - 0xe34a, - 0xe34e...0xe350, - 0xe354...0xe355, - 0xe362...0xe363, - 0xe368, - 0xe36a, - 0xe36f...0xe370, - 0xe37c, - 0xe384, - 0xe38d, - 0xe3c4...0xe3c8, - 0xe3cb...0xe3e3, + 0xe300...0xe302, + 0xe305...0xe318, + 0xe320...0xe321, + 0xe323...0xe325, + 0xe327...0xe346, + 0xe348...0xe34e, + 0xe350...0xe35e, + 0xe362, + 0xe364...0xe36b, + 0xe36e...0xe371, + 0xe374...0xe379, + 0xe37c...0xe37d, + 0xe37f...0xe380, + 0xe384...0xe385, + 0xe387, + 0xe389...0xe38a, + 0xe38c, + 0xe38e...0xe38f, + 0xe391...0xe392, + 0xe394...0xe395, + 0xe397...0xe39c, + 0xe3a2, + 0xe3a4...0xe3a5, + 0xe3a8...0xe3aa, + 0xe3ae, + 0xe3b2, + 0xe3b6, + 0xe3bc...0xe3e3, 0xe5fa...0xe6b8, 0xe700...0xe8ef, 0xea60, @@ -1629,23 +701,9 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xec0d...0xec1e, 0xed00...0xedff, 0xee0c...0xefce, - 0xf000...0xf004, - 0xf006...0xf025, - 0xf028...0xf02a, - 0xf02c...0xf030, - 0xf034, - 0xf036...0xf043, - 0xf045, - 0xf047, - 0xf053...0xf05f, - 0xf062, - 0xf064...0xf076, - 0xf079...0xf07d, - 0xf07f...0xf088, - 0xf08a...0xf0a3, - 0xf0a6...0xf0d6, - 0xf0db, + 0xf000...0xf0db, 0xf0df...0xf0ff, + 0xf101...0xf105, 0xf108...0xf12f, 0xf131...0xf140, 0xf142...0xf152, @@ -1658,23 +716,27 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf22e...0xf254, 0xf259, 0xf25c...0xf381, - 0xf400...0xf418, - 0xf41a...0xf42f, - 0xf431...0xf43d, - 0xf43f, - 0xf441...0xf443, - 0xf445...0xf449, - 0xf44b...0xf450, - 0xf454...0xf459, - 0xf45c...0xf470, - 0xf472...0xf47a, - 0xf47c...0xf480, - 0xf482...0xf491, - 0xf493...0xf49e, - 0xf4a0...0xf4c2, + 0xf400...0xf415, + 0xf417...0xf423, + 0xf425...0xf430, + 0xf435...0xf437, + 0xf439...0xf43d, + 0xf43f...0xf442, + 0xf446...0xf449, + 0xf44c...0xf45b, + 0xf45d...0xf45f, + 0xf462...0xf466, + 0xf468...0xf46b, + 0xf46d...0xf46f, + 0xf471...0xf475, + 0xf477...0xf479, + 0xf47f...0xf48a, + 0xf48c...0xf492, + 0xf494...0xf499, + 0xf49b...0xf4c2, 0xf4c4...0xf4ee, 0xf4f3...0xf51c, - 0xf51e...0xf532, + 0xf51e...0xf533, 0xf0001...0xf1af0, => .{ .size = .fit_cover1, @@ -2078,274 +1140,20 @@ pub fn getConstraint(cp: u21) ?Constraint { .pad_top = 0.03, .pad_bottom = 0.03, }, - 0xf005, + 0xf0dc...0xf0de, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + }, + 0xf100, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.9999664113932554, - .relative_y = 0.0000335886067446, - }, - 0xf026...0xf027, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9786184354605580, - .relative_y = 0.0103951316192896, - }, - 0xf02b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9758052740827267, - .relative_y = 0.0238869355863696, - }, - 0xf031...0xf033, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9987922705314010, - .relative_y = 0.0006038647342995, - }, - 0xf035, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9989935587761675, - .relative_y = 0.0004025764895330, - }, - 0xf044, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9925925925925926, - }, - 0xf046, - 0xf153...0xf154, - 0xf158, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8751322751322751, - .relative_y = 0.0624338624338624, - }, - 0xf048, - 0xf04a, - 0xf04e, - 0xf051, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8577706898990622, - .relative_y = 0.0711892586341537, - }, - 0xf049, - 0xf050, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8579450878868969, - .relative_y = 0.0710148606463189, - }, - 0xf04b, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9997041418532618, - .relative_y = 0.0002958581467381, - }, - 0xf04c...0xf04d, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8572940020656472, - .relative_y = 0.0713404035569438, - }, - 0xf04f, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7138835298072554, - .relative_y = 0.1433479295317200, - }, - 0xf052, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9999748091795350, - }, - 0xf060...0xf061, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8567975830815709, - .relative_y = 0.0719033232628399, - }, - 0xf063, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9987915407854985, - .relative_y = 0.0006042296072508, - }, - 0xf077, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5700483091787439, - .relative_y = 0.2862318840579710, - }, - 0xf078, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.5700483091787439, - .relative_y = 0.1437198067632850, - }, - 0xf07e, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4989429175475687, - .relative_y = 0.2505285412262157, - }, - 0xf089, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9998488512696494, - .relative_y = 0.0001511487303507, - }, - 0xf0a4...0xf0a5, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7502645502645503, - .relative_y = 0.1248677248677249, - }, - 0xf0d7, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4281400966183575, - .relative_y = 0.2053140096618357, - }, - 0xf0d8, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.4281400966183575, - .relative_y = 0.3472222222222222, - }, - 0xf0d9, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7140772371750631, - .relative_y = 0.1333462732919255, - }, - 0xf0da, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.7140396210163651, - .relative_y = 0.1333838894506235, - }, - 0xf0dc, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - }, - 0xf0dd, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .relative_height = 0.4275362318840580, - .relative_y = 0.0012077294685990, - }, - 0xf0de, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .relative_height = 0.4287439613526570, - .relative_y = 0.5712560386473430, - }, - 0xf100...0xf101, - 0xf104...0xf105, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8573155985489722, - .relative_y = 0.0713422007255139, - }, - 0xf102, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9286577992744861, - .relative_y = 0.0713422007255139, - }, - 0xf103, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9286577992744861, + .relative_height = 0.6923828125000000, + .relative_y = 0.1538085937500000, }, 0xf106...0xf107, => .{ @@ -2353,8 +1161,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.5000000000000000, - .relative_y = 0.2853688029020556, + .relative_height = 0.4038085937500000, + .relative_y = 0.3266601562500000, }, 0xf130, => .{ @@ -2373,6 +1181,16 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.2593984962406015, .relative_y = 0.3696741854636592, }, + 0xf153...0xf154, + 0xf158, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, + }, 0xf156, => .{ .size = .fit_cover1, @@ -2502,24 +1320,80 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_height = 0.9975006099019084, }, - 0xf419, - 0xf45a, + 0xf416, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8750000000000000, - .relative_y = 0.0625000000000000, + .relative_height = 0.6090604026845637, + .relative_y = 0.2119686800894855, }, - 0xf430, + 0xf424, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8496093750000000, - .relative_y = 0.0751953125000000, + .relative_width = 0.5019531250000000, + .relative_height = 0.5755033557046980, + .relative_x = 0.2480468750000000, + .relative_y = 0.2108501118568233, + }, + 0xf431, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6240234375000000, + .relative_height = 0.7695749440715883, + .relative_x = 0.2031250000000000, + .relative_y = 0.1420581655480984, + }, + 0xf432, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6718750000000000, + .relative_height = 0.7147651006711410, + .relative_x = 0.1875000000000000, + .relative_y = 0.1610738255033557, + }, + 0xf433, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6240234375000000, + .relative_height = 0.7695749440715883, + .relative_x = 0.2041015625000000, + .relative_y = 0.0883668903803132, + }, + 0xf434, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6718750000000000, + .relative_height = 0.7147651006711410, + .relative_x = 0.1406250000000000, + .relative_y = 0.1599552572706935, + }, + 0xf438, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.2436523437500000, + .relative_height = 0.4560546875000000, + .relative_x = 0.3813476562500000, + .relative_y = 0.2719726562500000, }, 0xf43e, => .{ @@ -2527,26 +1401,31 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.5024414062500000, - .relative_y = 0.2500000000000000, + .relative_width = 0.5029296875000000, + .relative_height = 0.5755033557046980, + .relative_x = 0.2500000000000000, + .relative_y = 0.2136465324384788, }, - 0xf440, - 0xf492, + 0xf443, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8437500000000000, - .relative_y = 0.0781250000000000, + .relative_width = 0.7500000000000000, + .relative_x = 0.1250000000000000, }, - 0xf444, + 0xf444...0xf445, + 0xf4c3, + 0xf51d, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, + .relative_width = 0.5000000000000000, .relative_height = 0.5000000000000000, + .relative_x = 0.2500000000000000, .relative_y = 0.2500000000000000, }, 0xf44a, @@ -2555,55 +1434,169 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, + .relative_width = 0.2436523437500000, .relative_height = 0.4560546875000000, + .relative_x = 0.3750000000000000, .relative_y = 0.2719726562500000, }, - 0xf45b, + 0xf44b, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.0937500000000000, - .relative_y = 0.4531250000000000, + .relative_width = 0.4560546875000000, + .relative_height = 0.2436523437500000, + .relative_x = 0.2719726562500000, + .relative_y = 0.3188476562500000, }, - 0xf471, - 0xf481, + 0xf45c, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.9375000000000000, - .relative_y = 0.0312500000000000, + .relative_width = 0.5019531250000000, + .relative_height = 0.5749440715883669, + .relative_x = 0.2480468750000000, + .relative_y = 0.2114093959731544, }, - 0xf47b, + 0xf460, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, + .relative_width = 0.3593750000000000, + .relative_height = 0.6240234375000000, + .relative_x = 0.3750000000000000, + .relative_y = 0.1884765625000000, + }, + 0xf461, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6237816764132553, + .relative_height = 0.9988851727982163, + .relative_x = 0.1881091617933723, + }, + 0xf467, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5639648437500000, + .relative_height = 0.5649414062500000, + .relative_x = 0.2187500000000000, + .relative_y = 0.2177734375000000, + }, + 0xf46c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5039062500000000, + .relative_height = 0.5771812080536913, + .relative_x = 0.2490234375000000, + .relative_y = 0.2091722595078300, + }, + 0xf470, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9926757812500000, + .relative_height = 0.2690429687500000, + .relative_y = 0.6865234375000000, + }, + 0xf476, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8732325694783033, + .relative_x = 0.0633837152608484, + }, + 0xf47a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5843079922027290, + .relative_height = 0.9509476031215162, + .relative_x = 0.2066276803118908, + .relative_y = 0.0234113712374582, + }, + 0xf47b...0xf47c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6250000000000000, .relative_height = 0.3593750000000000, + .relative_x = 0.1875000000000000, .relative_y = 0.3281250000000000, }, - 0xf49f, + 0xf47d, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7680000000000000, - .relative_y = 0.1160000000000000, + .relative_width = 0.3593750000000000, + .relative_height = 0.6240234375000000, + .relative_x = 0.2656250000000000, + .relative_y = 0.1875000000000000, }, - 0xf4c3, - 0xf51d, + 0xf47e, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.5417989417989418, - .relative_y = 0.2291005291005291, + .relative_width = 0.4560546875000000, + .relative_height = 0.2436523437500000, + .relative_x = 0.2719726562500000, + .relative_y = 0.3750000000000000, + }, + 0xf48b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7187500000000000, + .relative_height = 0.0937500000000000, + .relative_x = 0.1250000000000000, + .relative_y = 0.4687500000000000, + }, + 0xf493, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8313840155945419, + .relative_height = 0.9509476031215162, + .relative_x = 0.0843079922027290, + .relative_y = 0.0234113712374582, + }, + 0xf49a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8727450024378351, + .relative_x = 0.0633837152608484, }, 0xf4ef, 0xf4f2, @@ -2636,15 +1629,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_x = 0.0357142857142857, .relative_y = 0.1111111111111111, }, - 0xf533, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.9228395061728395, - .relative_y = 0.0390946502057613, - }, else => null, }; } diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 4b1a2b857..f5bac5e86 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -15,13 +15,14 @@ SymbolsNerdFont (not Mono!) font is passed as the first argument to it. import ast import sys import math -from fontTools.ttLib import TTFont +from fontTools.ttLib import TTFont, TTLibError from fontTools.pens.boundsPen import BoundsPen from collections import defaultdict from contextlib import suppress from pathlib import Path from types import SimpleNamespace from typing import Literal, TypedDict, cast +from urllib.request import urlretrieve type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry] type AttributeHash = tuple[ @@ -58,6 +59,8 @@ class PatchSetAttributeEntry(TypedDict): class PatchSet(TypedDict): Name: str + Filename: str + Exact: bool SymStart: int SymEnd: int SrcStart: int | None @@ -69,6 +72,18 @@ class PatchSetExtractor(ast.NodeVisitor): def __init__(self) -> None: self.symbol_table: dict[str, ast.expr] = {} self.patch_set_values: list[PatchSet] = [] + self.nf_version: str = "" + + def visit_Assign(self, node): + if ( + node.col_offset == 0 # top-level assignment + and len(node.targets) == 1 # no funny destructuring business + and isinstance(node.targets[0], ast.Name) # no setitem et cetera + and node.targets[0].id == "version" # it's the version string! + ): + self.nf_version = ast.literal_eval(node.value) + else: + return self.generic_visit(node) def visit_ClassDef(self, node: ast.ClassDef) -> None: if node.name != "font_patcher": @@ -140,12 +155,8 @@ class PatchSetExtractor(ast.NodeVisitor): def process_patch_entry(self, dict_node: ast.Dict) -> None: entry = {} - disallowed_key_nodes = frozenset({"Filename", "Exact"}) for key_node, value_node in zip(dict_node.keys, dict_node.values): - if ( - isinstance(key_node, ast.Constant) - and key_node.value not in disallowed_key_nodes - ): + if isinstance(key_node, ast.Constant): if key_node.value == "Enabled": if self.safe_literal_eval(value_node): continue # This patch set is enabled, continue to next key @@ -156,11 +167,11 @@ class PatchSetExtractor(ast.NodeVisitor): self.patch_set_values.append(cast("PatchSet", entry)) -def extract_patch_set_values(source_code: str) -> list[PatchSet]: +def extract_patch_set_values(source_code: str) -> tuple[list[PatchSet], str]: tree = ast.parse(source_code) extractor = PatchSetExtractor() extractor.visit(tree) - return extractor.patch_set_values + return extractor.patch_set_values, extractor.nf_version def parse_alignment(val: str) -> str | None: @@ -290,12 +301,127 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) return s +def generate_codepoint_tables( + patch_sets: list[PatchSet], + nerd_font: TTFont, + nf_version: str, +) -> dict[str, dict[int, int]]: + # We may already have the table saved from a previous run. + if Path("nerd_font_codepoint_tables.py").exists(): + import nerd_font_codepoint_tables + + if nerd_font_codepoint_tables.version == nf_version: + return nerd_font_codepoint_tables.cp_tables + + cp_tables: dict[str, dict[int, int]] = {} + cp_table_full: dict[int, int] = {} + cmap = nerd_font.getBestCmap() + for entry in patch_sets: + patch_set_name = entry["Name"] + print(f"Info: Extracting codepoint table from patch set '{patch_set_name}'") + + # Extract codepoint map from original font file; download if needed + source_filename = entry["Filename"] + target_folder = Path("nerd_font_symbol_fonts") + target_folder.mkdir(exist_ok=True) + target_file = target_folder / Path(source_filename).name + if not target_file.exists(): + print(f"Info: Downloading '{source_filename}'") + urlretrieve( + f"https://github.com/ryanoasis/nerd-fonts/raw/refs/tags/v{nf_version}/src/glyphs/{source_filename}", + target_file, + ) + try: + with TTFont(target_file) as patchfont: + patch_cmap = patchfont.getBestCmap() + except TTLibError: + # Not a TTF/OTF font. This is OK if this patch set is exact, so we + # let if pass. If there's a problem, later checks will catch it. + patch_cmap = None + + # A glyph's scale rules are specified using its codepoint in + # the original font, which is sometimes different from its + # Nerd Font codepoint. If entry["Exact"] is False, the codepoints are + # mapped according to the following rules: + # * entry["SymStart"] and entry["SymEnd"] denote the patch set's codepoint + # range in the original font. + # * entry["SrcStart"] is the starting point of the patch set's mapped + # codepoint range. It must not be None if entry["Exact"] is False. + # * The destination codepoint range is packed; that is, while there may be + # gaps without glyphs in the original font's codepoint range, there are + # none in the Nerd Font range. Hence there is no constant codepoint + # offset; instead we must iterate through the range and increment the + # destination codepoint every time we encounter a glyph in the original + # font. + # If entry["Exact"] is True, the origin and Nerd Font codepoints are the + # same, gaps included, and entry["SrcStart"] must be None. + if entry["Exact"]: + assert entry["SrcStart"] is None + cp_nerdfont = 0 + else: + assert entry["SrcStart"] + assert patch_cmap is not None + cp_nerdfont = entry["SrcStart"] - 1 + + if patch_set_name not in cp_tables: + # There are several patch sets with the same name, representing + # different codepoint ranges within the same original font. Merging + # these into a single table is OK. However, we need to keep separate + # tables for the different fonts to correctly deal with cases where + # they fill in each other's gaps. + cp_tables[patch_set_name] = {} + for cp_original in range(entry["SymStart"], entry["SymEnd"] + 1): + if patch_cmap and cp_original not in patch_cmap: + continue + if not entry["Exact"]: + cp_nerdfont += 1 + else: + cp_nerdfont = cp_original + if cp_nerdfont not in cmap: + raise ValueError( + f"Missing codepoint in Symbols Only Font: {hex(cp_nerdfont)} in patch set '{patch_set_name}'" + ) + elif cp_nerdfont in cp_table_full.values(): + raise ValueError( + f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'" + ) + cp_tables[patch_set_name][cp_original] = cp_nerdfont + cp_table_full |= cp_tables[patch_set_name] + + # Store the table and corresponding Nerd Fonts version together in a module. + with open("nerd_font_codepoint_tables.py", "w") as f: + print( + """#! This is a generated file, produced by nerd_font_codegen.py +#! DO NOT EDIT BY HAND! +#! +#! This file specifies the mapping of codepoints in the original symbol +#! fonts to codepoints in a patched Nerd Font. This is extracted from +#! the nerd fonts patcher script and the symbol font files.""", + file=f, + ) + print(f'version = "{nf_version}"', file=f) + print("cp_tables = {", file=f) + for name, table in cp_tables.items(): + print(f' "{name}": {{', file=f) + for key, value in table.items(): + print(f" {hex(key)}: {hex(value)},", file=f) + print(" },", file=f) + print("}", file=f) + + return cp_tables + + def generate_zig_switch_arms( patch_sets: list[PatchSet], nerd_font: TTFont, + nf_version: str, ) -> str: cmap = nerd_font.getBestCmap() glyphs = nerd_font.getGlyphSet() + cp_tables = generate_codepoint_tables(patch_sets, nerd_font, nf_version) + cp_table_full: dict[int, int] = {} + for cp_table in cp_tables.values(): + cp_table_full |= cp_table entries: dict[int, PatchSetAttributeEntry] = {} for entry in patch_sets: @@ -305,47 +431,21 @@ def generate_zig_switch_arms( attributes = entry["Attributes"] patch_set_entries: dict[int, PatchSetAttributeEntry] = {} - # A glyph's scale rules are specified using its codepoint in - # the original font, which is sometimes different from its - # Nerd Font codepoint. In font_patcher, the font to be patched - # (including the Symbols Only font embedded in Ghostty) is - # termed the sourceFont, while the original font is the - # symbolFont. Thus, the offset that maps the scale rule - # codepoint to the Nerd Font codepoint is SrcStart - SymStart. - cp_offset = entry["SrcStart"] - entry["SymStart"] if entry["SrcStart"] else 0 - for cp_rule in range(entry["SymStart"], entry["SymEnd"] + 1): - cp_font = cp_rule + cp_offset - if cp_font not in cmap: - print(f"Info: Skipping missing codepoint {hex(cp_font)}") + cp_table = cp_tables[patch_set_name] + for cp_original in range(entry["SymStart"], entry["SymEnd"] + 1): + if cp_original not in cp_table: continue - elif cp_font in entries: - # Patch sets sometimes have overlapping codepoint ranges. - # Sometimes a later set is a smaller set filling in a gap - # in the range of a larger, preceding set. Sometimes it's - # the other way around. The best thing we can do is hardcode - # each case. - if patch_set_name == "Font Awesome": - # The Font Awesome range has a gap matching the - # prededing Progress Indicators range. - print(f"Info: Not overwriting existing codepoint {hex(cp_font)}") - continue - elif patch_set_name == "Octicons": - # The fourth Octicons range overlaps with the first. - print(f"Info: Overwriting existing codepoint {hex(cp_font)}") - else: - raise ValueError( - f"Unknown case of overlap for codepoint {hex(cp_font)} in patch set '{patch_set_name}'" - ) - if cp_rule in attributes: - patch_set_entries[cp_font] = attributes[cp_rule].copy() + cp_nerdfont = cp_table[cp_original] + if cp_nerdfont in entries: + raise ValueError( + f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'" + ) + if cp_original in attributes: + patch_set_entries[cp_nerdfont] = attributes[cp_original].copy() else: - patch_set_entries[cp_font] = attributes["default"].copy() + patch_set_entries[cp_nerdfont] = attributes["default"].copy() if entry["ScaleRules"] is not None: - if "ScaleGroups" not in entry["ScaleRules"]: - raise ValueError( - f"Scale rule format {entry['ScaleRules']} not implemented." - ) for group in entry["ScaleRules"]["ScaleGroups"]: xMin = math.inf yMin = math.inf @@ -353,15 +453,15 @@ def generate_zig_switch_arms( yMax = -math.inf individual_bounds: dict[int, tuple[int, int, int, int]] = {} individual_advances: set[float] = set() - for cp_rule in group: - cp_font = cp_rule + cp_offset - if cp_font not in cmap: - continue - glyph = glyphs[cmap[cp_font]] + for cp_original in group: + # Scale groups may cut across patch sets, so we need to use + # the full lookup table here + cp_nerdfont = cp_table_full[cp_original] + glyph = glyphs[cmap[cp_nerdfont]] individual_advances.add(glyph.width) bounds = BoundsPen(glyphSet=glyphs) glyph.draw(bounds) - individual_bounds[cp_font] = bounds.bounds + individual_bounds[cp_nerdfont] = bounds.bounds xMin = min(bounds.bounds[0], xMin) yMin = min(bounds.bounds[1], yMin) xMax = max(bounds.bounds[2], xMax) @@ -371,34 +471,36 @@ def generate_zig_switch_arms( group_is_monospace = (len(individual_bounds) > 1) and ( len(individual_advances) == 1 ) - for cp_rule in group: - cp_font = cp_rule + cp_offset + for cp_original in group: + cp_nerdfont = cp_table_full[cp_original] if ( - cp_font not in cmap - or cp_font not in patch_set_entries + # Scale groups may cut across patch sets, but we're only + # updating a single patch set at a time, so we skip + # codepoints not in it. + cp_nerdfont not in patch_set_entries # Codepoints may contribute to the bounding box of multiple groups, # but should be scaled according to the first group they are found # in. Hence, to avoid overwriting, we need to skip codepoints that # have already been assigned a scale group. - or "relative_height" in patch_set_entries[cp_font] + or "relative_height" in patch_set_entries[cp_nerdfont] ): continue - this_bounds = individual_bounds[cp_font] + this_bounds = individual_bounds[cp_nerdfont] this_height = this_bounds[3] - this_bounds[1] - patch_set_entries[cp_font]["relative_height"] = ( + patch_set_entries[cp_nerdfont]["relative_height"] = ( this_height / group_height ) - patch_set_entries[cp_font]["relative_y"] = ( + patch_set_entries[cp_nerdfont]["relative_y"] = ( this_bounds[1] - yMin ) / group_height # Horizontal alignment should only be grouped if the group is monospace, # that is, if all glyphs in the group have the same advance width. if group_is_monospace: this_width = this_bounds[2] - this_bounds[0] - patch_set_entries[cp_font]["relative_width"] = ( + patch_set_entries[cp_nerdfont]["relative_width"] = ( this_width / group_width ) - patch_set_entries[cp_font]["relative_x"] = ( + patch_set_entries[cp_nerdfont]["relative_x"] = ( this_bounds[0] - xMin ) / group_width entries |= patch_set_entries @@ -427,7 +529,7 @@ if __name__ == "__main__": patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py" source = patcher_path.read_text(encoding="utf-8") - patch_set = extract_patch_set_values(source) + patch_set, nf_version = extract_patch_set_values(source) out_path = project_root / "src" / "font" / "nerd_font_attributes.zig" @@ -444,5 +546,5 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { """) - f.write(generate_zig_switch_arms(patch_set, nerd_font)) + f.write(generate_zig_switch_arms(patch_set, nerd_font, nf_version)) f.write("\n else => null,\n };\n}\n") diff --git a/src/font/nerd_font_codepoint_tables.py b/src/font/nerd_font_codepoint_tables.py new file mode 100644 index 000000000..89a623f1c --- /dev/null +++ b/src/font/nerd_font_codepoint_tables.py @@ -0,0 +1,10449 @@ +#! This is a generated file, produced by nerd_font_codegen.py +#! DO NOT EDIT BY HAND! +#! +#! This file specifies the mapping of codepoints in the original symbol +#! fonts to codepoints in a patched Nerd Font. This is extracted from +#! the nerd fonts patcher script and the symbol font files. +version = "3.4.0" +cp_tables = { + "Seti-UI + Custom": { + 0xe4fa: 0xe5fa, + 0xe4fb: 0xe5fb, + 0xe4fc: 0xe5fc, + 0xe4fd: 0xe5fd, + 0xe4fe: 0xe5fe, + 0xe4ff: 0xe5ff, + 0xe500: 0xe600, + 0xe501: 0xe601, + 0xe502: 0xe602, + 0xe503: 0xe603, + 0xe504: 0xe604, + 0xe505: 0xe605, + 0xe506: 0xe606, + 0xe507: 0xe607, + 0xe508: 0xe608, + 0xe509: 0xe609, + 0xe50a: 0xe60a, + 0xe50b: 0xe60b, + 0xe50c: 0xe60c, + 0xe50d: 0xe60d, + 0xe50e: 0xe60e, + 0xe50f: 0xe60f, + 0xe510: 0xe610, + 0xe511: 0xe611, + 0xe512: 0xe612, + 0xe513: 0xe613, + 0xe514: 0xe614, + 0xe515: 0xe615, + 0xe516: 0xe616, + 0xe517: 0xe617, + 0xe518: 0xe618, + 0xe519: 0xe619, + 0xe51a: 0xe61a, + 0xe51b: 0xe61b, + 0xe51c: 0xe61c, + 0xe51d: 0xe61d, + 0xe51e: 0xe61e, + 0xe51f: 0xe61f, + 0xe520: 0xe620, + 0xe521: 0xe621, + 0xe522: 0xe622, + 0xe523: 0xe623, + 0xe524: 0xe624, + 0xe525: 0xe625, + 0xe526: 0xe626, + 0xe527: 0xe627, + 0xe528: 0xe628, + 0xe529: 0xe629, + 0xe52a: 0xe62a, + 0xe52b: 0xe62b, + 0xe52c: 0xe62c, + 0xe52d: 0xe62d, + 0xe52e: 0xe62e, + 0xe52f: 0xe62f, + 0xe530: 0xe630, + 0xe531: 0xe631, + 0xe532: 0xe632, + 0xe533: 0xe633, + 0xe534: 0xe634, + 0xe535: 0xe635, + 0xe536: 0xe636, + 0xe537: 0xe637, + 0xe538: 0xe638, + 0xe539: 0xe639, + 0xe53a: 0xe63a, + 0xe53b: 0xe63b, + 0xe53c: 0xe63c, + 0xe53d: 0xe63d, + 0xe53e: 0xe63e, + 0xe53f: 0xe63f, + 0xe540: 0xe640, + 0xe541: 0xe641, + 0xe542: 0xe642, + 0xe543: 0xe643, + 0xe544: 0xe644, + 0xe545: 0xe645, + 0xe546: 0xe646, + 0xe547: 0xe647, + 0xe548: 0xe648, + 0xe549: 0xe649, + 0xe54a: 0xe64a, + 0xe54b: 0xe64b, + 0xe54c: 0xe64c, + 0xe54d: 0xe64d, + 0xe54e: 0xe64e, + 0xe54f: 0xe64f, + 0xe550: 0xe650, + 0xe551: 0xe651, + 0xe552: 0xe652, + 0xe553: 0xe653, + 0xe554: 0xe654, + 0xe555: 0xe655, + 0xe556: 0xe656, + 0xe557: 0xe657, + 0xe558: 0xe658, + 0xe559: 0xe659, + 0xe55a: 0xe65a, + 0xe55b: 0xe65b, + 0xe55c: 0xe65c, + 0xe55d: 0xe65d, + 0xe55e: 0xe65e, + 0xe55f: 0xe65f, + 0xe560: 0xe660, + 0xe561: 0xe661, + 0xe562: 0xe662, + 0xe563: 0xe663, + 0xe564: 0xe664, + 0xe565: 0xe665, + 0xe566: 0xe666, + 0xe567: 0xe667, + 0xe568: 0xe668, + 0xe569: 0xe669, + 0xe56a: 0xe66a, + 0xe56b: 0xe66b, + 0xe56c: 0xe66c, + 0xe56d: 0xe66d, + 0xe56e: 0xe66e, + 0xe56f: 0xe66f, + 0xe570: 0xe670, + 0xe571: 0xe671, + 0xe572: 0xe672, + 0xe573: 0xe673, + 0xe574: 0xe674, + 0xe575: 0xe675, + 0xe576: 0xe676, + 0xe577: 0xe677, + 0xe578: 0xe678, + 0xe579: 0xe679, + 0xe57a: 0xe67a, + 0xe57b: 0xe67b, + 0xe57c: 0xe67c, + 0xe57d: 0xe67d, + 0xe57e: 0xe67e, + 0xe57f: 0xe67f, + 0xe580: 0xe680, + 0xe581: 0xe681, + 0xe582: 0xe682, + 0xe583: 0xe683, + 0xe584: 0xe684, + 0xe585: 0xe685, + 0xe586: 0xe686, + 0xe587: 0xe687, + 0xe588: 0xe688, + 0xe589: 0xe689, + 0xe58a: 0xe68a, + 0xe58b: 0xe68b, + 0xe58c: 0xe68c, + 0xe58d: 0xe68d, + 0xe58e: 0xe68e, + 0xe58f: 0xe68f, + 0xe590: 0xe690, + 0xe591: 0xe691, + 0xe592: 0xe692, + 0xe593: 0xe693, + 0xe594: 0xe694, + 0xe595: 0xe695, + 0xe596: 0xe696, + 0xe597: 0xe697, + 0xe598: 0xe698, + 0xe599: 0xe699, + 0xe59a: 0xe69a, + 0xe59b: 0xe69b, + 0xe59c: 0xe69c, + 0xe59d: 0xe69d, + 0xe59e: 0xe69e, + 0xe59f: 0xe69f, + 0xe5a0: 0xe6a0, + 0xe5a1: 0xe6a1, + 0xe5a2: 0xe6a2, + 0xe5a3: 0xe6a3, + 0xe5a4: 0xe6a4, + 0xe5a5: 0xe6a5, + 0xe5a6: 0xe6a6, + 0xe5a7: 0xe6a7, + 0xe5a8: 0xe6a8, + 0xe5a9: 0xe6a9, + 0xe5aa: 0xe6aa, + 0xe5ab: 0xe6ab, + 0xe5ac: 0xe6ac, + 0xe5ad: 0xe6ad, + 0xe5ae: 0xe6ae, + 0xe5af: 0xe6af, + 0xe5b0: 0xe6b0, + 0xe5b1: 0xe6b1, + 0xe5b2: 0xe6b2, + 0xe5b3: 0xe6b3, + 0xe5b4: 0xe6b4, + 0xe5b5: 0xe6b5, + 0xe5b6: 0xe6b6, + 0xe5b7: 0xe6b7, + 0xe5b8: 0xe6b8, + }, + "Heavy Angle Brackets": { + 0x276c: 0x276c, + 0x276d: 0x276d, + 0x276e: 0x276e, + 0x276f: 0x276f, + 0x2770: 0x2770, + 0x2771: 0x2771, + }, + "Progress Indicators": { + 0xee00: 0xee00, + 0xee01: 0xee01, + 0xee02: 0xee02, + 0xee03: 0xee03, + 0xee04: 0xee04, + 0xee05: 0xee05, + 0xee06: 0xee06, + 0xee07: 0xee07, + 0xee08: 0xee08, + 0xee09: 0xee09, + 0xee0a: 0xee0a, + 0xee0b: 0xee0b, + }, + "Devicons": { + 0xe600: 0xe700, + 0xe601: 0xe701, + 0xe602: 0xe702, + 0xe603: 0xe703, + 0xe604: 0xe704, + 0xe605: 0xe705, + 0xe606: 0xe706, + 0xe607: 0xe707, + 0xe608: 0xe708, + 0xe609: 0xe709, + 0xe60a: 0xe70a, + 0xe60b: 0xe70b, + 0xe60c: 0xe70c, + 0xe60d: 0xe70d, + 0xe60e: 0xe70e, + 0xe60f: 0xe70f, + 0xe610: 0xe710, + 0xe611: 0xe711, + 0xe612: 0xe712, + 0xe613: 0xe713, + 0xe614: 0xe714, + 0xe615: 0xe715, + 0xe616: 0xe716, + 0xe617: 0xe717, + 0xe618: 0xe718, + 0xe619: 0xe719, + 0xe61a: 0xe71a, + 0xe61b: 0xe71b, + 0xe61c: 0xe71c, + 0xe61d: 0xe71d, + 0xe61e: 0xe71e, + 0xe61f: 0xe71f, + 0xe620: 0xe720, + 0xe621: 0xe721, + 0xe622: 0xe722, + 0xe623: 0xe723, + 0xe624: 0xe724, + 0xe625: 0xe725, + 0xe626: 0xe726, + 0xe627: 0xe727, + 0xe628: 0xe728, + 0xe629: 0xe729, + 0xe62a: 0xe72a, + 0xe62b: 0xe72b, + 0xe62c: 0xe72c, + 0xe62d: 0xe72d, + 0xe62e: 0xe72e, + 0xe62f: 0xe72f, + 0xe630: 0xe730, + 0xe631: 0xe731, + 0xe632: 0xe732, + 0xe633: 0xe733, + 0xe634: 0xe734, + 0xe635: 0xe735, + 0xe636: 0xe736, + 0xe637: 0xe737, + 0xe638: 0xe738, + 0xe639: 0xe739, + 0xe63a: 0xe73a, + 0xe63b: 0xe73b, + 0xe63c: 0xe73c, + 0xe63d: 0xe73d, + 0xe63e: 0xe73e, + 0xe63f: 0xe73f, + 0xe640: 0xe740, + 0xe641: 0xe741, + 0xe642: 0xe742, + 0xe643: 0xe743, + 0xe644: 0xe744, + 0xe645: 0xe745, + 0xe646: 0xe746, + 0xe647: 0xe747, + 0xe648: 0xe748, + 0xe649: 0xe749, + 0xe64a: 0xe74a, + 0xe64b: 0xe74b, + 0xe64c: 0xe74c, + 0xe64d: 0xe74d, + 0xe64e: 0xe74e, + 0xe64f: 0xe74f, + 0xe650: 0xe750, + 0xe651: 0xe751, + 0xe652: 0xe752, + 0xe653: 0xe753, + 0xe654: 0xe754, + 0xe655: 0xe755, + 0xe656: 0xe756, + 0xe657: 0xe757, + 0xe658: 0xe758, + 0xe659: 0xe759, + 0xe65a: 0xe75a, + 0xe65b: 0xe75b, + 0xe65c: 0xe75c, + 0xe65d: 0xe75d, + 0xe65e: 0xe75e, + 0xe65f: 0xe75f, + 0xe660: 0xe760, + 0xe661: 0xe761, + 0xe662: 0xe762, + 0xe663: 0xe763, + 0xe664: 0xe764, + 0xe665: 0xe765, + 0xe666: 0xe766, + 0xe667: 0xe767, + 0xe668: 0xe768, + 0xe669: 0xe769, + 0xe66a: 0xe76a, + 0xe66b: 0xe76b, + 0xe66c: 0xe76c, + 0xe66d: 0xe76d, + 0xe66e: 0xe76e, + 0xe66f: 0xe76f, + 0xe670: 0xe770, + 0xe671: 0xe771, + 0xe672: 0xe772, + 0xe673: 0xe773, + 0xe674: 0xe774, + 0xe675: 0xe775, + 0xe676: 0xe776, + 0xe677: 0xe777, + 0xe678: 0xe778, + 0xe679: 0xe779, + 0xe67a: 0xe77a, + 0xe67b: 0xe77b, + 0xe67c: 0xe77c, + 0xe67d: 0xe77d, + 0xe67e: 0xe77e, + 0xe67f: 0xe77f, + 0xe680: 0xe780, + 0xe681: 0xe781, + 0xe682: 0xe782, + 0xe683: 0xe783, + 0xe684: 0xe784, + 0xe685: 0xe785, + 0xe686: 0xe786, + 0xe687: 0xe787, + 0xe688: 0xe788, + 0xe689: 0xe789, + 0xe68a: 0xe78a, + 0xe68b: 0xe78b, + 0xe68c: 0xe78c, + 0xe68d: 0xe78d, + 0xe68e: 0xe78e, + 0xe68f: 0xe78f, + 0xe690: 0xe790, + 0xe691: 0xe791, + 0xe692: 0xe792, + 0xe693: 0xe793, + 0xe694: 0xe794, + 0xe695: 0xe795, + 0xe696: 0xe796, + 0xe697: 0xe797, + 0xe698: 0xe798, + 0xe699: 0xe799, + 0xe69a: 0xe79a, + 0xe69b: 0xe79b, + 0xe69c: 0xe79c, + 0xe69d: 0xe79d, + 0xe69e: 0xe79e, + 0xe69f: 0xe79f, + 0xe6a0: 0xe7a0, + 0xe6a1: 0xe7a1, + 0xe6a2: 0xe7a2, + 0xe6a3: 0xe7a3, + 0xe6a4: 0xe7a4, + 0xe6a5: 0xe7a5, + 0xe6a6: 0xe7a6, + 0xe6a7: 0xe7a7, + 0xe6a8: 0xe7a8, + 0xe6a9: 0xe7a9, + 0xe6aa: 0xe7aa, + 0xe6ab: 0xe7ab, + 0xe6ac: 0xe7ac, + 0xe6ad: 0xe7ad, + 0xe6ae: 0xe7ae, + 0xe6af: 0xe7af, + 0xe6b0: 0xe7b0, + 0xe6b1: 0xe7b1, + 0xe6b2: 0xe7b2, + 0xe6b3: 0xe7b3, + 0xe6b4: 0xe7b4, + 0xe6b5: 0xe7b5, + 0xe6b6: 0xe7b6, + 0xe6b7: 0xe7b7, + 0xe6b8: 0xe7b8, + 0xe6b9: 0xe7b9, + 0xe6ba: 0xe7ba, + 0xe6bb: 0xe7bb, + 0xe6bc: 0xe7bc, + 0xe6bd: 0xe7bd, + 0xe6be: 0xe7be, + 0xe6bf: 0xe7bf, + 0xe6c0: 0xe7c0, + 0xe6c1: 0xe7c1, + 0xe6c2: 0xe7c2, + 0xe6c3: 0xe7c3, + 0xe6c4: 0xe7c4, + 0xe6c5: 0xe7c5, + 0xe6c6: 0xe7c6, + 0xe6c7: 0xe7c7, + 0xe6c8: 0xe7c8, + 0xe6c9: 0xe7c9, + 0xe6ca: 0xe7ca, + 0xe6cb: 0xe7cb, + 0xe6cc: 0xe7cc, + 0xe6cd: 0xe7cd, + 0xe6ce: 0xe7ce, + 0xe6cf: 0xe7cf, + 0xe6d0: 0xe7d0, + 0xe6d1: 0xe7d1, + 0xe6d2: 0xe7d2, + 0xe6d3: 0xe7d3, + 0xe6d4: 0xe7d4, + 0xe6d5: 0xe7d5, + 0xe6d6: 0xe7d6, + 0xe6d7: 0xe7d7, + 0xe6d8: 0xe7d8, + 0xe6d9: 0xe7d9, + 0xe6da: 0xe7da, + 0xe6db: 0xe7db, + 0xe6dc: 0xe7dc, + 0xe6dd: 0xe7dd, + 0xe6de: 0xe7de, + 0xe6df: 0xe7df, + 0xe6e0: 0xe7e0, + 0xe6e1: 0xe7e1, + 0xe6e2: 0xe7e2, + 0xe6e3: 0xe7e3, + 0xe6e4: 0xe7e4, + 0xe6e5: 0xe7e5, + 0xe6e6: 0xe7e6, + 0xe6e7: 0xe7e7, + 0xe6e8: 0xe7e8, + 0xe6e9: 0xe7e9, + 0xe6ea: 0xe7ea, + 0xe6eb: 0xe7eb, + 0xe6ec: 0xe7ec, + 0xe6ed: 0xe7ed, + 0xe6ee: 0xe7ee, + 0xe6ef: 0xe7ef, + 0xe6f0: 0xe7f0, + 0xe6f1: 0xe7f1, + 0xe6f2: 0xe7f2, + 0xe6f3: 0xe7f3, + 0xe6f4: 0xe7f4, + 0xe6f5: 0xe7f5, + 0xe6f6: 0xe7f6, + 0xe6f7: 0xe7f7, + 0xe6f8: 0xe7f8, + 0xe6f9: 0xe7f9, + 0xe6fa: 0xe7fa, + 0xe6fb: 0xe7fb, + 0xe6fc: 0xe7fc, + 0xe6fd: 0xe7fd, + 0xe6fe: 0xe7fe, + 0xe6ff: 0xe7ff, + 0xe700: 0xe800, + 0xe701: 0xe801, + 0xe702: 0xe802, + 0xe703: 0xe803, + 0xe704: 0xe804, + 0xe705: 0xe805, + 0xe706: 0xe806, + 0xe707: 0xe807, + 0xe708: 0xe808, + 0xe709: 0xe809, + 0xe70a: 0xe80a, + 0xe70b: 0xe80b, + 0xe70c: 0xe80c, + 0xe70d: 0xe80d, + 0xe70e: 0xe80e, + 0xe70f: 0xe80f, + 0xe710: 0xe810, + 0xe711: 0xe811, + 0xe712: 0xe812, + 0xe713: 0xe813, + 0xe714: 0xe814, + 0xe715: 0xe815, + 0xe716: 0xe816, + 0xe717: 0xe817, + 0xe718: 0xe818, + 0xe719: 0xe819, + 0xe71a: 0xe81a, + 0xe71b: 0xe81b, + 0xe71c: 0xe81c, + 0xe71d: 0xe81d, + 0xe71e: 0xe81e, + 0xe71f: 0xe81f, + 0xe720: 0xe820, + 0xe721: 0xe821, + 0xe722: 0xe822, + 0xe723: 0xe823, + 0xe724: 0xe824, + 0xe725: 0xe825, + 0xe726: 0xe826, + 0xe727: 0xe827, + 0xe728: 0xe828, + 0xe729: 0xe829, + 0xe72a: 0xe82a, + 0xe72b: 0xe82b, + 0xe72c: 0xe82c, + 0xe72d: 0xe82d, + 0xe72e: 0xe82e, + 0xe72f: 0xe82f, + 0xe730: 0xe830, + 0xe731: 0xe831, + 0xe732: 0xe832, + 0xe733: 0xe833, + 0xe734: 0xe834, + 0xe735: 0xe835, + 0xe736: 0xe836, + 0xe737: 0xe837, + 0xe738: 0xe838, + 0xe739: 0xe839, + 0xe73a: 0xe83a, + 0xe73b: 0xe83b, + 0xe73c: 0xe83c, + 0xe73d: 0xe83d, + 0xe73e: 0xe83e, + 0xe73f: 0xe83f, + 0xe740: 0xe840, + 0xe741: 0xe841, + 0xe742: 0xe842, + 0xe743: 0xe843, + 0xe744: 0xe844, + 0xe745: 0xe845, + 0xe746: 0xe846, + 0xe747: 0xe847, + 0xe748: 0xe848, + 0xe749: 0xe849, + 0xe74a: 0xe84a, + 0xe74b: 0xe84b, + 0xe74c: 0xe84c, + 0xe74d: 0xe84d, + 0xe74e: 0xe84e, + 0xe74f: 0xe84f, + 0xe750: 0xe850, + 0xe751: 0xe851, + 0xe752: 0xe852, + 0xe753: 0xe853, + 0xe754: 0xe854, + 0xe755: 0xe855, + 0xe756: 0xe856, + 0xe757: 0xe857, + 0xe758: 0xe858, + 0xe759: 0xe859, + 0xe75a: 0xe85a, + 0xe75b: 0xe85b, + 0xe75c: 0xe85c, + 0xe75d: 0xe85d, + 0xe75e: 0xe85e, + 0xe75f: 0xe85f, + 0xe760: 0xe860, + 0xe761: 0xe861, + 0xe762: 0xe862, + 0xe763: 0xe863, + 0xe764: 0xe864, + 0xe765: 0xe865, + 0xe766: 0xe866, + 0xe767: 0xe867, + 0xe768: 0xe868, + 0xe769: 0xe869, + 0xe76a: 0xe86a, + 0xe76b: 0xe86b, + 0xe76c: 0xe86c, + 0xe76d: 0xe86d, + 0xe76e: 0xe86e, + 0xe76f: 0xe86f, + 0xe770: 0xe870, + 0xe771: 0xe871, + 0xe772: 0xe872, + 0xe773: 0xe873, + 0xe774: 0xe874, + 0xe775: 0xe875, + 0xe776: 0xe876, + 0xe777: 0xe877, + 0xe778: 0xe878, + 0xe779: 0xe879, + 0xe77a: 0xe87a, + 0xe77b: 0xe87b, + 0xe77c: 0xe87c, + 0xe77d: 0xe87d, + 0xe77e: 0xe87e, + 0xe77f: 0xe87f, + 0xe780: 0xe880, + 0xe781: 0xe881, + 0xe782: 0xe882, + 0xe783: 0xe883, + 0xe784: 0xe884, + 0xe785: 0xe885, + 0xe786: 0xe886, + 0xe787: 0xe887, + 0xe788: 0xe888, + 0xe789: 0xe889, + 0xe78a: 0xe88a, + 0xe78b: 0xe88b, + 0xe78c: 0xe88c, + 0xe78d: 0xe88d, + 0xe78e: 0xe88e, + 0xe78f: 0xe88f, + 0xe790: 0xe890, + 0xe791: 0xe891, + 0xe792: 0xe892, + 0xe793: 0xe893, + 0xe794: 0xe894, + 0xe795: 0xe895, + 0xe796: 0xe896, + 0xe797: 0xe897, + 0xe798: 0xe898, + 0xe799: 0xe899, + 0xe79a: 0xe89a, + 0xe79b: 0xe89b, + 0xe79c: 0xe89c, + 0xe79d: 0xe89d, + 0xe79e: 0xe89e, + 0xe79f: 0xe89f, + 0xe7a0: 0xe8a0, + 0xe7a1: 0xe8a1, + 0xe7a2: 0xe8a2, + 0xe7a3: 0xe8a3, + 0xe7a4: 0xe8a4, + 0xe7a5: 0xe8a5, + 0xe7a6: 0xe8a6, + 0xe7a7: 0xe8a7, + 0xe7a8: 0xe8a8, + 0xe7a9: 0xe8a9, + 0xe7aa: 0xe8aa, + 0xe7ab: 0xe8ab, + 0xe7ac: 0xe8ac, + 0xe7ad: 0xe8ad, + 0xe7ae: 0xe8ae, + 0xe7af: 0xe8af, + 0xe7b0: 0xe8b0, + 0xe7b1: 0xe8b1, + 0xe7b2: 0xe8b2, + 0xe7b3: 0xe8b3, + 0xe7b4: 0xe8b4, + 0xe7b5: 0xe8b5, + 0xe7b6: 0xe8b6, + 0xe7b7: 0xe8b7, + 0xe7b8: 0xe8b8, + 0xe7b9: 0xe8b9, + 0xe7ba: 0xe8ba, + 0xe7bb: 0xe8bb, + 0xe7bc: 0xe8bc, + 0xe7bd: 0xe8bd, + 0xe7be: 0xe8be, + 0xe7bf: 0xe8bf, + 0xe7c0: 0xe8c0, + 0xe7c1: 0xe8c1, + 0xe7c2: 0xe8c2, + 0xe7c3: 0xe8c3, + 0xe7c4: 0xe8c4, + 0xe7c5: 0xe8c5, + 0xe7c6: 0xe8c6, + 0xe7c7: 0xe8c7, + 0xe7c8: 0xe8c8, + 0xe7c9: 0xe8c9, + 0xe7ca: 0xe8ca, + 0xe7cb: 0xe8cb, + 0xe7cc: 0xe8cc, + 0xe7cd: 0xe8cd, + 0xe7ce: 0xe8ce, + 0xe7cf: 0xe8cf, + 0xe7d0: 0xe8d0, + 0xe7d1: 0xe8d1, + 0xe7d2: 0xe8d2, + 0xe7d3: 0xe8d3, + 0xe7d4: 0xe8d4, + 0xe7d5: 0xe8d5, + 0xe7d6: 0xe8d6, + 0xe7d7: 0xe8d7, + 0xe7d8: 0xe8d8, + 0xe7d9: 0xe8d9, + 0xe7da: 0xe8da, + 0xe7db: 0xe8db, + 0xe7dc: 0xe8dc, + 0xe7dd: 0xe8dd, + 0xe7de: 0xe8de, + 0xe7df: 0xe8df, + 0xe7e0: 0xe8e0, + 0xe7e1: 0xe8e1, + 0xe7e2: 0xe8e2, + 0xe7e3: 0xe8e3, + 0xe7e4: 0xe8e4, + 0xe7e5: 0xe8e5, + 0xe7e6: 0xe8e6, + 0xe7e7: 0xe8e7, + 0xe7e8: 0xe8e8, + 0xe7e9: 0xe8e9, + 0xe7ea: 0xe8ea, + 0xe7eb: 0xe8eb, + 0xe7ec: 0xe8ec, + 0xe7ed: 0xe8ed, + 0xe7ee: 0xe8ee, + 0xe7ef: 0xe8ef, + }, + "Powerline Symbols": { + 0xe0a0: 0xe0a0, + 0xe0a1: 0xe0a1, + 0xe0a2: 0xe0a2, + 0xe0b0: 0xe0b0, + 0xe0b1: 0xe0b1, + 0xe0b2: 0xe0b2, + 0xe0b3: 0xe0b3, + }, + "Powerline Extra Symbols": { + 0xe0a3: 0xe0a3, + 0xe0b4: 0xe0b4, + 0xe0b5: 0xe0b5, + 0xe0b6: 0xe0b6, + 0xe0b7: 0xe0b7, + 0xe0b8: 0xe0b8, + 0xe0b9: 0xe0b9, + 0xe0ba: 0xe0ba, + 0xe0bb: 0xe0bb, + 0xe0bc: 0xe0bc, + 0xe0bd: 0xe0bd, + 0xe0be: 0xe0be, + 0xe0bf: 0xe0bf, + 0xe0c0: 0xe0c0, + 0xe0c1: 0xe0c1, + 0xe0c2: 0xe0c2, + 0xe0c3: 0xe0c3, + 0xe0c4: 0xe0c4, + 0xe0c5: 0xe0c5, + 0xe0c6: 0xe0c6, + 0xe0c7: 0xe0c7, + 0xe0c8: 0xe0c8, + 0xe0ca: 0xe0ca, + 0xe0cc: 0xe0cc, + 0xe0cd: 0xe0cd, + 0xe0ce: 0xe0ce, + 0xe0cf: 0xe0cf, + 0xe0d0: 0xe0d0, + 0xe0d1: 0xe0d1, + 0xe0d2: 0xe0d2, + 0xe0d4: 0xe0d4, + 0xe0d6: 0xe0d6, + 0xe0d7: 0xe0d7, + 0x2630: 0x2630, + }, + "Pomicons": { + 0xe000: 0xe000, + 0xe001: 0xe001, + 0xe002: 0xe002, + 0xe003: 0xe003, + 0xe004: 0xe004, + 0xe005: 0xe005, + 0xe006: 0xe006, + 0xe007: 0xe007, + 0xe008: 0xe008, + 0xe009: 0xe009, + 0xe00a: 0xe00a, + }, + "Font Awesome": { + 0xed00: 0xed00, + 0xed01: 0xed01, + 0xed02: 0xed02, + 0xed03: 0xed03, + 0xed04: 0xed04, + 0xed05: 0xed05, + 0xed06: 0xed06, + 0xed07: 0xed07, + 0xed08: 0xed08, + 0xed09: 0xed09, + 0xed0a: 0xed0a, + 0xed0b: 0xed0b, + 0xed0c: 0xed0c, + 0xed0d: 0xed0d, + 0xed0e: 0xed0e, + 0xed0f: 0xed0f, + 0xed10: 0xed10, + 0xed11: 0xed11, + 0xed12: 0xed12, + 0xed13: 0xed13, + 0xed14: 0xed14, + 0xed15: 0xed15, + 0xed16: 0xed16, + 0xed17: 0xed17, + 0xed18: 0xed18, + 0xed19: 0xed19, + 0xed1a: 0xed1a, + 0xed1b: 0xed1b, + 0xed1c: 0xed1c, + 0xed1d: 0xed1d, + 0xed1e: 0xed1e, + 0xed1f: 0xed1f, + 0xed20: 0xed20, + 0xed21: 0xed21, + 0xed22: 0xed22, + 0xed23: 0xed23, + 0xed24: 0xed24, + 0xed25: 0xed25, + 0xed26: 0xed26, + 0xed27: 0xed27, + 0xed28: 0xed28, + 0xed29: 0xed29, + 0xed2a: 0xed2a, + 0xed2b: 0xed2b, + 0xed2c: 0xed2c, + 0xed2d: 0xed2d, + 0xed2e: 0xed2e, + 0xed2f: 0xed2f, + 0xed30: 0xed30, + 0xed31: 0xed31, + 0xed32: 0xed32, + 0xed33: 0xed33, + 0xed34: 0xed34, + 0xed35: 0xed35, + 0xed36: 0xed36, + 0xed37: 0xed37, + 0xed38: 0xed38, + 0xed39: 0xed39, + 0xed3a: 0xed3a, + 0xed3b: 0xed3b, + 0xed3c: 0xed3c, + 0xed3d: 0xed3d, + 0xed3e: 0xed3e, + 0xed3f: 0xed3f, + 0xed40: 0xed40, + 0xed41: 0xed41, + 0xed42: 0xed42, + 0xed43: 0xed43, + 0xed44: 0xed44, + 0xed45: 0xed45, + 0xed46: 0xed46, + 0xed47: 0xed47, + 0xed48: 0xed48, + 0xed49: 0xed49, + 0xed4a: 0xed4a, + 0xed4b: 0xed4b, + 0xed4c: 0xed4c, + 0xed4d: 0xed4d, + 0xed4e: 0xed4e, + 0xed4f: 0xed4f, + 0xed50: 0xed50, + 0xed51: 0xed51, + 0xed52: 0xed52, + 0xed53: 0xed53, + 0xed54: 0xed54, + 0xed55: 0xed55, + 0xed56: 0xed56, + 0xed57: 0xed57, + 0xed58: 0xed58, + 0xed59: 0xed59, + 0xed5a: 0xed5a, + 0xed5b: 0xed5b, + 0xed5c: 0xed5c, + 0xed5d: 0xed5d, + 0xed5e: 0xed5e, + 0xed5f: 0xed5f, + 0xed60: 0xed60, + 0xed61: 0xed61, + 0xed62: 0xed62, + 0xed63: 0xed63, + 0xed64: 0xed64, + 0xed65: 0xed65, + 0xed66: 0xed66, + 0xed67: 0xed67, + 0xed68: 0xed68, + 0xed69: 0xed69, + 0xed6a: 0xed6a, + 0xed6b: 0xed6b, + 0xed6c: 0xed6c, + 0xed6d: 0xed6d, + 0xed6e: 0xed6e, + 0xed6f: 0xed6f, + 0xed70: 0xed70, + 0xed71: 0xed71, + 0xed72: 0xed72, + 0xed73: 0xed73, + 0xed74: 0xed74, + 0xed75: 0xed75, + 0xed76: 0xed76, + 0xed77: 0xed77, + 0xed78: 0xed78, + 0xed79: 0xed79, + 0xed7a: 0xed7a, + 0xed7b: 0xed7b, + 0xed7c: 0xed7c, + 0xed7d: 0xed7d, + 0xed7e: 0xed7e, + 0xed7f: 0xed7f, + 0xed80: 0xed80, + 0xed81: 0xed81, + 0xed82: 0xed82, + 0xed83: 0xed83, + 0xed84: 0xed84, + 0xed85: 0xed85, + 0xed86: 0xed86, + 0xed87: 0xed87, + 0xed88: 0xed88, + 0xed89: 0xed89, + 0xed8a: 0xed8a, + 0xed8b: 0xed8b, + 0xed8c: 0xed8c, + 0xed8d: 0xed8d, + 0xed8e: 0xed8e, + 0xed8f: 0xed8f, + 0xed90: 0xed90, + 0xed91: 0xed91, + 0xed92: 0xed92, + 0xed93: 0xed93, + 0xed94: 0xed94, + 0xed95: 0xed95, + 0xed96: 0xed96, + 0xed97: 0xed97, + 0xed98: 0xed98, + 0xed99: 0xed99, + 0xed9a: 0xed9a, + 0xed9b: 0xed9b, + 0xed9c: 0xed9c, + 0xed9d: 0xed9d, + 0xed9e: 0xed9e, + 0xed9f: 0xed9f, + 0xeda0: 0xeda0, + 0xeda1: 0xeda1, + 0xeda2: 0xeda2, + 0xeda3: 0xeda3, + 0xeda4: 0xeda4, + 0xeda5: 0xeda5, + 0xeda6: 0xeda6, + 0xeda7: 0xeda7, + 0xeda8: 0xeda8, + 0xeda9: 0xeda9, + 0xedaa: 0xedaa, + 0xedab: 0xedab, + 0xedac: 0xedac, + 0xedad: 0xedad, + 0xedae: 0xedae, + 0xedaf: 0xedaf, + 0xedb0: 0xedb0, + 0xedb1: 0xedb1, + 0xedb2: 0xedb2, + 0xedb3: 0xedb3, + 0xedb4: 0xedb4, + 0xedb5: 0xedb5, + 0xedb6: 0xedb6, + 0xedb7: 0xedb7, + 0xedb8: 0xedb8, + 0xedb9: 0xedb9, + 0xedba: 0xedba, + 0xedbb: 0xedbb, + 0xedbc: 0xedbc, + 0xedbd: 0xedbd, + 0xedbe: 0xedbe, + 0xedbf: 0xedbf, + 0xedc0: 0xedc0, + 0xedc1: 0xedc1, + 0xedc2: 0xedc2, + 0xedc3: 0xedc3, + 0xedc4: 0xedc4, + 0xedc5: 0xedc5, + 0xedc6: 0xedc6, + 0xedc7: 0xedc7, + 0xedc8: 0xedc8, + 0xedc9: 0xedc9, + 0xedca: 0xedca, + 0xedcb: 0xedcb, + 0xedcc: 0xedcc, + 0xedcd: 0xedcd, + 0xedce: 0xedce, + 0xedcf: 0xedcf, + 0xedd0: 0xedd0, + 0xedd1: 0xedd1, + 0xedd2: 0xedd2, + 0xedd3: 0xedd3, + 0xedd4: 0xedd4, + 0xedd5: 0xedd5, + 0xedd6: 0xedd6, + 0xedd7: 0xedd7, + 0xedd8: 0xedd8, + 0xedd9: 0xedd9, + 0xedda: 0xedda, + 0xeddb: 0xeddb, + 0xeddc: 0xeddc, + 0xeddd: 0xeddd, + 0xedde: 0xedde, + 0xeddf: 0xeddf, + 0xede0: 0xede0, + 0xede1: 0xede1, + 0xede2: 0xede2, + 0xede3: 0xede3, + 0xede4: 0xede4, + 0xede5: 0xede5, + 0xede6: 0xede6, + 0xede7: 0xede7, + 0xede8: 0xede8, + 0xede9: 0xede9, + 0xedea: 0xedea, + 0xedeb: 0xedeb, + 0xedec: 0xedec, + 0xeded: 0xeded, + 0xedee: 0xedee, + 0xedef: 0xedef, + 0xedf0: 0xedf0, + 0xedf1: 0xedf1, + 0xedf2: 0xedf2, + 0xedf3: 0xedf3, + 0xedf4: 0xedf4, + 0xedf5: 0xedf5, + 0xedf6: 0xedf6, + 0xedf7: 0xedf7, + 0xedf8: 0xedf8, + 0xedf9: 0xedf9, + 0xedfa: 0xedfa, + 0xedfb: 0xedfb, + 0xedfc: 0xedfc, + 0xedfd: 0xedfd, + 0xedfe: 0xedfe, + 0xedff: 0xedff, + 0xee0c: 0xee0c, + 0xee0d: 0xee0d, + 0xee0e: 0xee0e, + 0xee0f: 0xee0f, + 0xee10: 0xee10, + 0xee11: 0xee11, + 0xee12: 0xee12, + 0xee13: 0xee13, + 0xee14: 0xee14, + 0xee15: 0xee15, + 0xee16: 0xee16, + 0xee17: 0xee17, + 0xee18: 0xee18, + 0xee19: 0xee19, + 0xee1a: 0xee1a, + 0xee1b: 0xee1b, + 0xee1c: 0xee1c, + 0xee1d: 0xee1d, + 0xee1e: 0xee1e, + 0xee1f: 0xee1f, + 0xee20: 0xee20, + 0xee21: 0xee21, + 0xee22: 0xee22, + 0xee23: 0xee23, + 0xee24: 0xee24, + 0xee25: 0xee25, + 0xee26: 0xee26, + 0xee27: 0xee27, + 0xee28: 0xee28, + 0xee29: 0xee29, + 0xee2a: 0xee2a, + 0xee2b: 0xee2b, + 0xee2c: 0xee2c, + 0xee2d: 0xee2d, + 0xee2e: 0xee2e, + 0xee2f: 0xee2f, + 0xee30: 0xee30, + 0xee31: 0xee31, + 0xee32: 0xee32, + 0xee33: 0xee33, + 0xee34: 0xee34, + 0xee35: 0xee35, + 0xee36: 0xee36, + 0xee37: 0xee37, + 0xee38: 0xee38, + 0xee39: 0xee39, + 0xee3a: 0xee3a, + 0xee3b: 0xee3b, + 0xee3c: 0xee3c, + 0xee3d: 0xee3d, + 0xee3e: 0xee3e, + 0xee3f: 0xee3f, + 0xee40: 0xee40, + 0xee41: 0xee41, + 0xee42: 0xee42, + 0xee43: 0xee43, + 0xee44: 0xee44, + 0xee45: 0xee45, + 0xee46: 0xee46, + 0xee47: 0xee47, + 0xee48: 0xee48, + 0xee49: 0xee49, + 0xee4a: 0xee4a, + 0xee4b: 0xee4b, + 0xee4c: 0xee4c, + 0xee4d: 0xee4d, + 0xee4e: 0xee4e, + 0xee4f: 0xee4f, + 0xee50: 0xee50, + 0xee51: 0xee51, + 0xee52: 0xee52, + 0xee53: 0xee53, + 0xee54: 0xee54, + 0xee55: 0xee55, + 0xee56: 0xee56, + 0xee57: 0xee57, + 0xee58: 0xee58, + 0xee59: 0xee59, + 0xee5a: 0xee5a, + 0xee5b: 0xee5b, + 0xee5c: 0xee5c, + 0xee5d: 0xee5d, + 0xee5e: 0xee5e, + 0xee5f: 0xee5f, + 0xee60: 0xee60, + 0xee61: 0xee61, + 0xee62: 0xee62, + 0xee63: 0xee63, + 0xee64: 0xee64, + 0xee65: 0xee65, + 0xee66: 0xee66, + 0xee67: 0xee67, + 0xee68: 0xee68, + 0xee69: 0xee69, + 0xee6a: 0xee6a, + 0xee6b: 0xee6b, + 0xee6c: 0xee6c, + 0xee6d: 0xee6d, + 0xee6e: 0xee6e, + 0xee6f: 0xee6f, + 0xee70: 0xee70, + 0xee71: 0xee71, + 0xee72: 0xee72, + 0xee73: 0xee73, + 0xee74: 0xee74, + 0xee75: 0xee75, + 0xee76: 0xee76, + 0xee77: 0xee77, + 0xee78: 0xee78, + 0xee79: 0xee79, + 0xee7a: 0xee7a, + 0xee7b: 0xee7b, + 0xee7c: 0xee7c, + 0xee7d: 0xee7d, + 0xee7e: 0xee7e, + 0xee7f: 0xee7f, + 0xee80: 0xee80, + 0xee81: 0xee81, + 0xee82: 0xee82, + 0xee83: 0xee83, + 0xee84: 0xee84, + 0xee85: 0xee85, + 0xee86: 0xee86, + 0xee87: 0xee87, + 0xee88: 0xee88, + 0xee89: 0xee89, + 0xee8a: 0xee8a, + 0xee8b: 0xee8b, + 0xee8c: 0xee8c, + 0xee8d: 0xee8d, + 0xee8e: 0xee8e, + 0xee8f: 0xee8f, + 0xee90: 0xee90, + 0xee91: 0xee91, + 0xee92: 0xee92, + 0xee93: 0xee93, + 0xee94: 0xee94, + 0xee95: 0xee95, + 0xee96: 0xee96, + 0xee97: 0xee97, + 0xee98: 0xee98, + 0xee99: 0xee99, + 0xee9a: 0xee9a, + 0xee9b: 0xee9b, + 0xee9c: 0xee9c, + 0xee9d: 0xee9d, + 0xee9e: 0xee9e, + 0xee9f: 0xee9f, + 0xeea0: 0xeea0, + 0xeea1: 0xeea1, + 0xeea2: 0xeea2, + 0xeea3: 0xeea3, + 0xeea4: 0xeea4, + 0xeea5: 0xeea5, + 0xeea6: 0xeea6, + 0xeea7: 0xeea7, + 0xeea8: 0xeea8, + 0xeea9: 0xeea9, + 0xeeaa: 0xeeaa, + 0xeeab: 0xeeab, + 0xeeac: 0xeeac, + 0xeead: 0xeead, + 0xeeae: 0xeeae, + 0xeeaf: 0xeeaf, + 0xeeb0: 0xeeb0, + 0xeeb1: 0xeeb1, + 0xeeb2: 0xeeb2, + 0xeeb3: 0xeeb3, + 0xeeb4: 0xeeb4, + 0xeeb5: 0xeeb5, + 0xeeb6: 0xeeb6, + 0xeeb7: 0xeeb7, + 0xeeb8: 0xeeb8, + 0xeeb9: 0xeeb9, + 0xeeba: 0xeeba, + 0xeebb: 0xeebb, + 0xeebc: 0xeebc, + 0xeebd: 0xeebd, + 0xeebe: 0xeebe, + 0xeebf: 0xeebf, + 0xeec0: 0xeec0, + 0xeec1: 0xeec1, + 0xeec2: 0xeec2, + 0xeec3: 0xeec3, + 0xeec4: 0xeec4, + 0xeec5: 0xeec5, + 0xeec6: 0xeec6, + 0xeec7: 0xeec7, + 0xeec8: 0xeec8, + 0xeec9: 0xeec9, + 0xeeca: 0xeeca, + 0xeecb: 0xeecb, + 0xeecc: 0xeecc, + 0xeecd: 0xeecd, + 0xeece: 0xeece, + 0xeecf: 0xeecf, + 0xeed0: 0xeed0, + 0xeed1: 0xeed1, + 0xeed2: 0xeed2, + 0xeed3: 0xeed3, + 0xeed4: 0xeed4, + 0xeed5: 0xeed5, + 0xeed6: 0xeed6, + 0xeed7: 0xeed7, + 0xeed8: 0xeed8, + 0xeed9: 0xeed9, + 0xeeda: 0xeeda, + 0xeedb: 0xeedb, + 0xeedc: 0xeedc, + 0xeedd: 0xeedd, + 0xeede: 0xeede, + 0xeedf: 0xeedf, + 0xeee0: 0xeee0, + 0xeee1: 0xeee1, + 0xeee2: 0xeee2, + 0xeee3: 0xeee3, + 0xeee4: 0xeee4, + 0xeee5: 0xeee5, + 0xeee6: 0xeee6, + 0xeee7: 0xeee7, + 0xeee8: 0xeee8, + 0xeee9: 0xeee9, + 0xeeea: 0xeeea, + 0xeeeb: 0xeeeb, + 0xeeec: 0xeeec, + 0xeeed: 0xeeed, + 0xeeee: 0xeeee, + 0xeeef: 0xeeef, + 0xeef0: 0xeef0, + 0xeef1: 0xeef1, + 0xeef2: 0xeef2, + 0xeef3: 0xeef3, + 0xeef4: 0xeef4, + 0xeef5: 0xeef5, + 0xeef6: 0xeef6, + 0xeef7: 0xeef7, + 0xeef8: 0xeef8, + 0xeef9: 0xeef9, + 0xeefa: 0xeefa, + 0xeefb: 0xeefb, + 0xeefc: 0xeefc, + 0xeefd: 0xeefd, + 0xeefe: 0xeefe, + 0xeeff: 0xeeff, + 0xef00: 0xef00, + 0xef01: 0xef01, + 0xef02: 0xef02, + 0xef03: 0xef03, + 0xef04: 0xef04, + 0xef05: 0xef05, + 0xef06: 0xef06, + 0xef07: 0xef07, + 0xef08: 0xef08, + 0xef09: 0xef09, + 0xef0a: 0xef0a, + 0xef0b: 0xef0b, + 0xef0c: 0xef0c, + 0xef0d: 0xef0d, + 0xef0e: 0xef0e, + 0xef0f: 0xef0f, + 0xef10: 0xef10, + 0xef11: 0xef11, + 0xef12: 0xef12, + 0xef13: 0xef13, + 0xef14: 0xef14, + 0xef15: 0xef15, + 0xef16: 0xef16, + 0xef17: 0xef17, + 0xef18: 0xef18, + 0xef19: 0xef19, + 0xef1a: 0xef1a, + 0xef1b: 0xef1b, + 0xef1c: 0xef1c, + 0xef1d: 0xef1d, + 0xef1e: 0xef1e, + 0xef1f: 0xef1f, + 0xef20: 0xef20, + 0xef21: 0xef21, + 0xef22: 0xef22, + 0xef23: 0xef23, + 0xef24: 0xef24, + 0xef25: 0xef25, + 0xef26: 0xef26, + 0xef27: 0xef27, + 0xef28: 0xef28, + 0xef29: 0xef29, + 0xef2a: 0xef2a, + 0xef2b: 0xef2b, + 0xef2c: 0xef2c, + 0xef2d: 0xef2d, + 0xef2e: 0xef2e, + 0xef2f: 0xef2f, + 0xef30: 0xef30, + 0xef31: 0xef31, + 0xef32: 0xef32, + 0xef33: 0xef33, + 0xef34: 0xef34, + 0xef35: 0xef35, + 0xef36: 0xef36, + 0xef37: 0xef37, + 0xef38: 0xef38, + 0xef39: 0xef39, + 0xef3a: 0xef3a, + 0xef3b: 0xef3b, + 0xef3c: 0xef3c, + 0xef3d: 0xef3d, + 0xef3e: 0xef3e, + 0xef3f: 0xef3f, + 0xef40: 0xef40, + 0xef41: 0xef41, + 0xef42: 0xef42, + 0xef43: 0xef43, + 0xef44: 0xef44, + 0xef45: 0xef45, + 0xef46: 0xef46, + 0xef47: 0xef47, + 0xef48: 0xef48, + 0xef49: 0xef49, + 0xef4a: 0xef4a, + 0xef4b: 0xef4b, + 0xef4c: 0xef4c, + 0xef4d: 0xef4d, + 0xef4e: 0xef4e, + 0xef4f: 0xef4f, + 0xef50: 0xef50, + 0xef51: 0xef51, + 0xef52: 0xef52, + 0xef53: 0xef53, + 0xef54: 0xef54, + 0xef55: 0xef55, + 0xef56: 0xef56, + 0xef57: 0xef57, + 0xef58: 0xef58, + 0xef59: 0xef59, + 0xef5a: 0xef5a, + 0xef5b: 0xef5b, + 0xef5c: 0xef5c, + 0xef5d: 0xef5d, + 0xef5e: 0xef5e, + 0xef5f: 0xef5f, + 0xef60: 0xef60, + 0xef61: 0xef61, + 0xef62: 0xef62, + 0xef63: 0xef63, + 0xef64: 0xef64, + 0xef65: 0xef65, + 0xef66: 0xef66, + 0xef67: 0xef67, + 0xef68: 0xef68, + 0xef69: 0xef69, + 0xef6a: 0xef6a, + 0xef6b: 0xef6b, + 0xef6c: 0xef6c, + 0xef6d: 0xef6d, + 0xef6e: 0xef6e, + 0xef6f: 0xef6f, + 0xef70: 0xef70, + 0xef71: 0xef71, + 0xef72: 0xef72, + 0xef73: 0xef73, + 0xef74: 0xef74, + 0xef75: 0xef75, + 0xef76: 0xef76, + 0xef77: 0xef77, + 0xef78: 0xef78, + 0xef79: 0xef79, + 0xef7a: 0xef7a, + 0xef7b: 0xef7b, + 0xef7c: 0xef7c, + 0xef7d: 0xef7d, + 0xef7e: 0xef7e, + 0xef7f: 0xef7f, + 0xef80: 0xef80, + 0xef81: 0xef81, + 0xef82: 0xef82, + 0xef83: 0xef83, + 0xef84: 0xef84, + 0xef85: 0xef85, + 0xef86: 0xef86, + 0xef87: 0xef87, + 0xef88: 0xef88, + 0xef89: 0xef89, + 0xef8a: 0xef8a, + 0xef8b: 0xef8b, + 0xef8c: 0xef8c, + 0xef8d: 0xef8d, + 0xef8e: 0xef8e, + 0xef8f: 0xef8f, + 0xef90: 0xef90, + 0xef91: 0xef91, + 0xef92: 0xef92, + 0xef93: 0xef93, + 0xef94: 0xef94, + 0xef95: 0xef95, + 0xef96: 0xef96, + 0xef97: 0xef97, + 0xef98: 0xef98, + 0xef99: 0xef99, + 0xef9a: 0xef9a, + 0xef9b: 0xef9b, + 0xef9c: 0xef9c, + 0xef9d: 0xef9d, + 0xef9e: 0xef9e, + 0xef9f: 0xef9f, + 0xefa0: 0xefa0, + 0xefa1: 0xefa1, + 0xefa2: 0xefa2, + 0xefa3: 0xefa3, + 0xefa4: 0xefa4, + 0xefa5: 0xefa5, + 0xefa6: 0xefa6, + 0xefa7: 0xefa7, + 0xefa8: 0xefa8, + 0xefa9: 0xefa9, + 0xefaa: 0xefaa, + 0xefab: 0xefab, + 0xefac: 0xefac, + 0xefad: 0xefad, + 0xefae: 0xefae, + 0xefaf: 0xefaf, + 0xefb0: 0xefb0, + 0xefb1: 0xefb1, + 0xefb2: 0xefb2, + 0xefb3: 0xefb3, + 0xefb4: 0xefb4, + 0xefb5: 0xefb5, + 0xefb6: 0xefb6, + 0xefb7: 0xefb7, + 0xefb8: 0xefb8, + 0xefb9: 0xefb9, + 0xefba: 0xefba, + 0xefbb: 0xefbb, + 0xefbc: 0xefbc, + 0xefbd: 0xefbd, + 0xefbe: 0xefbe, + 0xefbf: 0xefbf, + 0xefc0: 0xefc0, + 0xefc1: 0xefc1, + 0xefc2: 0xefc2, + 0xefc3: 0xefc3, + 0xefc4: 0xefc4, + 0xefc5: 0xefc5, + 0xefc6: 0xefc6, + 0xefc7: 0xefc7, + 0xefc8: 0xefc8, + 0xefc9: 0xefc9, + 0xefca: 0xefca, + 0xefcb: 0xefcb, + 0xefcc: 0xefcc, + 0xefcd: 0xefcd, + 0xefce: 0xefce, + 0xf000: 0xf000, + 0xf001: 0xf001, + 0xf002: 0xf002, + 0xf003: 0xf003, + 0xf004: 0xf004, + 0xf005: 0xf005, + 0xf006: 0xf006, + 0xf007: 0xf007, + 0xf008: 0xf008, + 0xf009: 0xf009, + 0xf00a: 0xf00a, + 0xf00b: 0xf00b, + 0xf00c: 0xf00c, + 0xf00d: 0xf00d, + 0xf00e: 0xf00e, + 0xf00f: 0xf00f, + 0xf010: 0xf010, + 0xf011: 0xf011, + 0xf012: 0xf012, + 0xf013: 0xf013, + 0xf014: 0xf014, + 0xf015: 0xf015, + 0xf016: 0xf016, + 0xf017: 0xf017, + 0xf018: 0xf018, + 0xf019: 0xf019, + 0xf01a: 0xf01a, + 0xf01b: 0xf01b, + 0xf01c: 0xf01c, + 0xf01d: 0xf01d, + 0xf01e: 0xf01e, + 0xf01f: 0xf01f, + 0xf020: 0xf020, + 0xf021: 0xf021, + 0xf022: 0xf022, + 0xf023: 0xf023, + 0xf024: 0xf024, + 0xf025: 0xf025, + 0xf026: 0xf026, + 0xf027: 0xf027, + 0xf028: 0xf028, + 0xf029: 0xf029, + 0xf02a: 0xf02a, + 0xf02b: 0xf02b, + 0xf02c: 0xf02c, + 0xf02d: 0xf02d, + 0xf02e: 0xf02e, + 0xf02f: 0xf02f, + 0xf030: 0xf030, + 0xf031: 0xf031, + 0xf032: 0xf032, + 0xf033: 0xf033, + 0xf034: 0xf034, + 0xf035: 0xf035, + 0xf036: 0xf036, + 0xf037: 0xf037, + 0xf038: 0xf038, + 0xf039: 0xf039, + 0xf03a: 0xf03a, + 0xf03b: 0xf03b, + 0xf03c: 0xf03c, + 0xf03d: 0xf03d, + 0xf03e: 0xf03e, + 0xf03f: 0xf03f, + 0xf040: 0xf040, + 0xf041: 0xf041, + 0xf042: 0xf042, + 0xf043: 0xf043, + 0xf044: 0xf044, + 0xf045: 0xf045, + 0xf046: 0xf046, + 0xf047: 0xf047, + 0xf048: 0xf048, + 0xf049: 0xf049, + 0xf04a: 0xf04a, + 0xf04b: 0xf04b, + 0xf04c: 0xf04c, + 0xf04d: 0xf04d, + 0xf04e: 0xf04e, + 0xf04f: 0xf04f, + 0xf050: 0xf050, + 0xf051: 0xf051, + 0xf052: 0xf052, + 0xf053: 0xf053, + 0xf054: 0xf054, + 0xf055: 0xf055, + 0xf056: 0xf056, + 0xf057: 0xf057, + 0xf058: 0xf058, + 0xf059: 0xf059, + 0xf05a: 0xf05a, + 0xf05b: 0xf05b, + 0xf05c: 0xf05c, + 0xf05d: 0xf05d, + 0xf05e: 0xf05e, + 0xf05f: 0xf05f, + 0xf060: 0xf060, + 0xf061: 0xf061, + 0xf062: 0xf062, + 0xf063: 0xf063, + 0xf064: 0xf064, + 0xf065: 0xf065, + 0xf066: 0xf066, + 0xf067: 0xf067, + 0xf068: 0xf068, + 0xf069: 0xf069, + 0xf06a: 0xf06a, + 0xf06b: 0xf06b, + 0xf06c: 0xf06c, + 0xf06d: 0xf06d, + 0xf06e: 0xf06e, + 0xf06f: 0xf06f, + 0xf070: 0xf070, + 0xf071: 0xf071, + 0xf072: 0xf072, + 0xf073: 0xf073, + 0xf074: 0xf074, + 0xf075: 0xf075, + 0xf076: 0xf076, + 0xf077: 0xf077, + 0xf078: 0xf078, + 0xf079: 0xf079, + 0xf07a: 0xf07a, + 0xf07b: 0xf07b, + 0xf07c: 0xf07c, + 0xf07d: 0xf07d, + 0xf07e: 0xf07e, + 0xf07f: 0xf07f, + 0xf080: 0xf080, + 0xf081: 0xf081, + 0xf082: 0xf082, + 0xf083: 0xf083, + 0xf084: 0xf084, + 0xf085: 0xf085, + 0xf086: 0xf086, + 0xf087: 0xf087, + 0xf088: 0xf088, + 0xf089: 0xf089, + 0xf08a: 0xf08a, + 0xf08b: 0xf08b, + 0xf08c: 0xf08c, + 0xf08d: 0xf08d, + 0xf08e: 0xf08e, + 0xf08f: 0xf08f, + 0xf090: 0xf090, + 0xf091: 0xf091, + 0xf092: 0xf092, + 0xf093: 0xf093, + 0xf094: 0xf094, + 0xf095: 0xf095, + 0xf096: 0xf096, + 0xf097: 0xf097, + 0xf098: 0xf098, + 0xf099: 0xf099, + 0xf09a: 0xf09a, + 0xf09b: 0xf09b, + 0xf09c: 0xf09c, + 0xf09d: 0xf09d, + 0xf09e: 0xf09e, + 0xf09f: 0xf09f, + 0xf0a0: 0xf0a0, + 0xf0a1: 0xf0a1, + 0xf0a2: 0xf0a2, + 0xf0a3: 0xf0a3, + 0xf0a4: 0xf0a4, + 0xf0a5: 0xf0a5, + 0xf0a6: 0xf0a6, + 0xf0a7: 0xf0a7, + 0xf0a8: 0xf0a8, + 0xf0a9: 0xf0a9, + 0xf0aa: 0xf0aa, + 0xf0ab: 0xf0ab, + 0xf0ac: 0xf0ac, + 0xf0ad: 0xf0ad, + 0xf0ae: 0xf0ae, + 0xf0af: 0xf0af, + 0xf0b0: 0xf0b0, + 0xf0b1: 0xf0b1, + 0xf0b2: 0xf0b2, + 0xf0b3: 0xf0b3, + 0xf0b4: 0xf0b4, + 0xf0b5: 0xf0b5, + 0xf0b6: 0xf0b6, + 0xf0b7: 0xf0b7, + 0xf0b8: 0xf0b8, + 0xf0b9: 0xf0b9, + 0xf0ba: 0xf0ba, + 0xf0bb: 0xf0bb, + 0xf0bc: 0xf0bc, + 0xf0bd: 0xf0bd, + 0xf0be: 0xf0be, + 0xf0bf: 0xf0bf, + 0xf0c0: 0xf0c0, + 0xf0c1: 0xf0c1, + 0xf0c2: 0xf0c2, + 0xf0c3: 0xf0c3, + 0xf0c4: 0xf0c4, + 0xf0c5: 0xf0c5, + 0xf0c6: 0xf0c6, + 0xf0c7: 0xf0c7, + 0xf0c8: 0xf0c8, + 0xf0c9: 0xf0c9, + 0xf0ca: 0xf0ca, + 0xf0cb: 0xf0cb, + 0xf0cc: 0xf0cc, + 0xf0cd: 0xf0cd, + 0xf0ce: 0xf0ce, + 0xf0cf: 0xf0cf, + 0xf0d0: 0xf0d0, + 0xf0d1: 0xf0d1, + 0xf0d2: 0xf0d2, + 0xf0d3: 0xf0d3, + 0xf0d4: 0xf0d4, + 0xf0d5: 0xf0d5, + 0xf0d6: 0xf0d6, + 0xf0d7: 0xf0d7, + 0xf0d8: 0xf0d8, + 0xf0d9: 0xf0d9, + 0xf0da: 0xf0da, + 0xf0db: 0xf0db, + 0xf0dc: 0xf0dc, + 0xf0dd: 0xf0dd, + 0xf0de: 0xf0de, + 0xf0df: 0xf0df, + 0xf0e0: 0xf0e0, + 0xf0e1: 0xf0e1, + 0xf0e2: 0xf0e2, + 0xf0e3: 0xf0e3, + 0xf0e4: 0xf0e4, + 0xf0e5: 0xf0e5, + 0xf0e6: 0xf0e6, + 0xf0e7: 0xf0e7, + 0xf0e8: 0xf0e8, + 0xf0e9: 0xf0e9, + 0xf0ea: 0xf0ea, + 0xf0eb: 0xf0eb, + 0xf0ec: 0xf0ec, + 0xf0ed: 0xf0ed, + 0xf0ee: 0xf0ee, + 0xf0ef: 0xf0ef, + 0xf0f0: 0xf0f0, + 0xf0f1: 0xf0f1, + 0xf0f2: 0xf0f2, + 0xf0f3: 0xf0f3, + 0xf0f4: 0xf0f4, + 0xf0f5: 0xf0f5, + 0xf0f6: 0xf0f6, + 0xf0f7: 0xf0f7, + 0xf0f8: 0xf0f8, + 0xf0f9: 0xf0f9, + 0xf0fa: 0xf0fa, + 0xf0fb: 0xf0fb, + 0xf0fc: 0xf0fc, + 0xf0fd: 0xf0fd, + 0xf0fe: 0xf0fe, + 0xf0ff: 0xf0ff, + 0xf100: 0xf100, + 0xf101: 0xf101, + 0xf102: 0xf102, + 0xf103: 0xf103, + 0xf104: 0xf104, + 0xf105: 0xf105, + 0xf106: 0xf106, + 0xf107: 0xf107, + 0xf108: 0xf108, + 0xf109: 0xf109, + 0xf10a: 0xf10a, + 0xf10b: 0xf10b, + 0xf10c: 0xf10c, + 0xf10d: 0xf10d, + 0xf10e: 0xf10e, + 0xf10f: 0xf10f, + 0xf110: 0xf110, + 0xf111: 0xf111, + 0xf112: 0xf112, + 0xf113: 0xf113, + 0xf114: 0xf114, + 0xf115: 0xf115, + 0xf116: 0xf116, + 0xf117: 0xf117, + 0xf118: 0xf118, + 0xf119: 0xf119, + 0xf11a: 0xf11a, + 0xf11b: 0xf11b, + 0xf11c: 0xf11c, + 0xf11d: 0xf11d, + 0xf11e: 0xf11e, + 0xf11f: 0xf11f, + 0xf120: 0xf120, + 0xf121: 0xf121, + 0xf122: 0xf122, + 0xf123: 0xf123, + 0xf124: 0xf124, + 0xf125: 0xf125, + 0xf126: 0xf126, + 0xf127: 0xf127, + 0xf128: 0xf128, + 0xf129: 0xf129, + 0xf12a: 0xf12a, + 0xf12b: 0xf12b, + 0xf12c: 0xf12c, + 0xf12d: 0xf12d, + 0xf12e: 0xf12e, + 0xf12f: 0xf12f, + 0xf130: 0xf130, + 0xf131: 0xf131, + 0xf132: 0xf132, + 0xf133: 0xf133, + 0xf134: 0xf134, + 0xf135: 0xf135, + 0xf136: 0xf136, + 0xf137: 0xf137, + 0xf138: 0xf138, + 0xf139: 0xf139, + 0xf13a: 0xf13a, + 0xf13b: 0xf13b, + 0xf13c: 0xf13c, + 0xf13d: 0xf13d, + 0xf13e: 0xf13e, + 0xf13f: 0xf13f, + 0xf140: 0xf140, + 0xf141: 0xf141, + 0xf142: 0xf142, + 0xf143: 0xf143, + 0xf144: 0xf144, + 0xf145: 0xf145, + 0xf146: 0xf146, + 0xf147: 0xf147, + 0xf148: 0xf148, + 0xf149: 0xf149, + 0xf14a: 0xf14a, + 0xf14b: 0xf14b, + 0xf14c: 0xf14c, + 0xf14d: 0xf14d, + 0xf14e: 0xf14e, + 0xf14f: 0xf14f, + 0xf150: 0xf150, + 0xf151: 0xf151, + 0xf152: 0xf152, + 0xf153: 0xf153, + 0xf154: 0xf154, + 0xf155: 0xf155, + 0xf156: 0xf156, + 0xf157: 0xf157, + 0xf158: 0xf158, + 0xf159: 0xf159, + 0xf15a: 0xf15a, + 0xf15b: 0xf15b, + 0xf15c: 0xf15c, + 0xf15d: 0xf15d, + 0xf15e: 0xf15e, + 0xf15f: 0xf15f, + 0xf160: 0xf160, + 0xf161: 0xf161, + 0xf162: 0xf162, + 0xf163: 0xf163, + 0xf164: 0xf164, + 0xf165: 0xf165, + 0xf166: 0xf166, + 0xf167: 0xf167, + 0xf168: 0xf168, + 0xf169: 0xf169, + 0xf16a: 0xf16a, + 0xf16b: 0xf16b, + 0xf16c: 0xf16c, + 0xf16d: 0xf16d, + 0xf16e: 0xf16e, + 0xf16f: 0xf16f, + 0xf170: 0xf170, + 0xf171: 0xf171, + 0xf172: 0xf172, + 0xf173: 0xf173, + 0xf174: 0xf174, + 0xf175: 0xf175, + 0xf176: 0xf176, + 0xf177: 0xf177, + 0xf178: 0xf178, + 0xf179: 0xf179, + 0xf17a: 0xf17a, + 0xf17b: 0xf17b, + 0xf17c: 0xf17c, + 0xf17d: 0xf17d, + 0xf17e: 0xf17e, + 0xf17f: 0xf17f, + 0xf180: 0xf180, + 0xf181: 0xf181, + 0xf182: 0xf182, + 0xf183: 0xf183, + 0xf184: 0xf184, + 0xf185: 0xf185, + 0xf186: 0xf186, + 0xf187: 0xf187, + 0xf188: 0xf188, + 0xf189: 0xf189, + 0xf18a: 0xf18a, + 0xf18b: 0xf18b, + 0xf18c: 0xf18c, + 0xf18d: 0xf18d, + 0xf18e: 0xf18e, + 0xf18f: 0xf18f, + 0xf190: 0xf190, + 0xf191: 0xf191, + 0xf192: 0xf192, + 0xf193: 0xf193, + 0xf194: 0xf194, + 0xf195: 0xf195, + 0xf196: 0xf196, + 0xf197: 0xf197, + 0xf198: 0xf198, + 0xf199: 0xf199, + 0xf19a: 0xf19a, + 0xf19b: 0xf19b, + 0xf19c: 0xf19c, + 0xf19d: 0xf19d, + 0xf19e: 0xf19e, + 0xf19f: 0xf19f, + 0xf1a0: 0xf1a0, + 0xf1a1: 0xf1a1, + 0xf1a2: 0xf1a2, + 0xf1a3: 0xf1a3, + 0xf1a4: 0xf1a4, + 0xf1a5: 0xf1a5, + 0xf1a6: 0xf1a6, + 0xf1a7: 0xf1a7, + 0xf1a8: 0xf1a8, + 0xf1a9: 0xf1a9, + 0xf1aa: 0xf1aa, + 0xf1ab: 0xf1ab, + 0xf1ac: 0xf1ac, + 0xf1ad: 0xf1ad, + 0xf1ae: 0xf1ae, + 0xf1af: 0xf1af, + 0xf1b0: 0xf1b0, + 0xf1b1: 0xf1b1, + 0xf1b2: 0xf1b2, + 0xf1b3: 0xf1b3, + 0xf1b4: 0xf1b4, + 0xf1b5: 0xf1b5, + 0xf1b6: 0xf1b6, + 0xf1b7: 0xf1b7, + 0xf1b8: 0xf1b8, + 0xf1b9: 0xf1b9, + 0xf1ba: 0xf1ba, + 0xf1bb: 0xf1bb, + 0xf1bc: 0xf1bc, + 0xf1bd: 0xf1bd, + 0xf1be: 0xf1be, + 0xf1bf: 0xf1bf, + 0xf1c0: 0xf1c0, + 0xf1c1: 0xf1c1, + 0xf1c2: 0xf1c2, + 0xf1c3: 0xf1c3, + 0xf1c4: 0xf1c4, + 0xf1c5: 0xf1c5, + 0xf1c6: 0xf1c6, + 0xf1c7: 0xf1c7, + 0xf1c8: 0xf1c8, + 0xf1c9: 0xf1c9, + 0xf1ca: 0xf1ca, + 0xf1cb: 0xf1cb, + 0xf1cc: 0xf1cc, + 0xf1cd: 0xf1cd, + 0xf1ce: 0xf1ce, + 0xf1cf: 0xf1cf, + 0xf1d0: 0xf1d0, + 0xf1d1: 0xf1d1, + 0xf1d2: 0xf1d2, + 0xf1d3: 0xf1d3, + 0xf1d4: 0xf1d4, + 0xf1d5: 0xf1d5, + 0xf1d6: 0xf1d6, + 0xf1d7: 0xf1d7, + 0xf1d8: 0xf1d8, + 0xf1d9: 0xf1d9, + 0xf1da: 0xf1da, + 0xf1db: 0xf1db, + 0xf1dc: 0xf1dc, + 0xf1dd: 0xf1dd, + 0xf1de: 0xf1de, + 0xf1df: 0xf1df, + 0xf1e0: 0xf1e0, + 0xf1e1: 0xf1e1, + 0xf1e2: 0xf1e2, + 0xf1e3: 0xf1e3, + 0xf1e4: 0xf1e4, + 0xf1e5: 0xf1e5, + 0xf1e6: 0xf1e6, + 0xf1e7: 0xf1e7, + 0xf1e8: 0xf1e8, + 0xf1e9: 0xf1e9, + 0xf1ea: 0xf1ea, + 0xf1eb: 0xf1eb, + 0xf1ec: 0xf1ec, + 0xf1ed: 0xf1ed, + 0xf1ee: 0xf1ee, + 0xf1ef: 0xf1ef, + 0xf1f0: 0xf1f0, + 0xf1f1: 0xf1f1, + 0xf1f2: 0xf1f2, + 0xf1f3: 0xf1f3, + 0xf1f4: 0xf1f4, + 0xf1f5: 0xf1f5, + 0xf1f6: 0xf1f6, + 0xf1f7: 0xf1f7, + 0xf1f8: 0xf1f8, + 0xf1f9: 0xf1f9, + 0xf1fa: 0xf1fa, + 0xf1fb: 0xf1fb, + 0xf1fc: 0xf1fc, + 0xf1fd: 0xf1fd, + 0xf1fe: 0xf1fe, + 0xf1ff: 0xf1ff, + 0xf200: 0xf200, + 0xf201: 0xf201, + 0xf202: 0xf202, + 0xf203: 0xf203, + 0xf204: 0xf204, + 0xf205: 0xf205, + 0xf206: 0xf206, + 0xf207: 0xf207, + 0xf208: 0xf208, + 0xf209: 0xf209, + 0xf20a: 0xf20a, + 0xf20b: 0xf20b, + 0xf20c: 0xf20c, + 0xf20d: 0xf20d, + 0xf20e: 0xf20e, + 0xf20f: 0xf20f, + 0xf210: 0xf210, + 0xf211: 0xf211, + 0xf212: 0xf212, + 0xf213: 0xf213, + 0xf214: 0xf214, + 0xf215: 0xf215, + 0xf216: 0xf216, + 0xf217: 0xf217, + 0xf218: 0xf218, + 0xf219: 0xf219, + 0xf21a: 0xf21a, + 0xf21b: 0xf21b, + 0xf21c: 0xf21c, + 0xf21d: 0xf21d, + 0xf21e: 0xf21e, + 0xf21f: 0xf21f, + 0xf220: 0xf220, + 0xf221: 0xf221, + 0xf222: 0xf222, + 0xf223: 0xf223, + 0xf224: 0xf224, + 0xf225: 0xf225, + 0xf226: 0xf226, + 0xf227: 0xf227, + 0xf228: 0xf228, + 0xf229: 0xf229, + 0xf22a: 0xf22a, + 0xf22b: 0xf22b, + 0xf22c: 0xf22c, + 0xf22d: 0xf22d, + 0xf22e: 0xf22e, + 0xf22f: 0xf22f, + 0xf230: 0xf230, + 0xf231: 0xf231, + 0xf232: 0xf232, + 0xf233: 0xf233, + 0xf234: 0xf234, + 0xf235: 0xf235, + 0xf236: 0xf236, + 0xf237: 0xf237, + 0xf238: 0xf238, + 0xf239: 0xf239, + 0xf23a: 0xf23a, + 0xf23b: 0xf23b, + 0xf23c: 0xf23c, + 0xf23d: 0xf23d, + 0xf23e: 0xf23e, + 0xf23f: 0xf23f, + 0xf240: 0xf240, + 0xf241: 0xf241, + 0xf242: 0xf242, + 0xf243: 0xf243, + 0xf244: 0xf244, + 0xf245: 0xf245, + 0xf246: 0xf246, + 0xf247: 0xf247, + 0xf248: 0xf248, + 0xf249: 0xf249, + 0xf24a: 0xf24a, + 0xf24b: 0xf24b, + 0xf24c: 0xf24c, + 0xf24d: 0xf24d, + 0xf24e: 0xf24e, + 0xf24f: 0xf24f, + 0xf250: 0xf250, + 0xf251: 0xf251, + 0xf252: 0xf252, + 0xf253: 0xf253, + 0xf254: 0xf254, + 0xf255: 0xf255, + 0xf256: 0xf256, + 0xf257: 0xf257, + 0xf258: 0xf258, + 0xf259: 0xf259, + 0xf25a: 0xf25a, + 0xf25b: 0xf25b, + 0xf25c: 0xf25c, + 0xf25d: 0xf25d, + 0xf25e: 0xf25e, + 0xf25f: 0xf25f, + 0xf260: 0xf260, + 0xf261: 0xf261, + 0xf262: 0xf262, + 0xf263: 0xf263, + 0xf264: 0xf264, + 0xf265: 0xf265, + 0xf266: 0xf266, + 0xf267: 0xf267, + 0xf268: 0xf268, + 0xf269: 0xf269, + 0xf26a: 0xf26a, + 0xf26b: 0xf26b, + 0xf26c: 0xf26c, + 0xf26d: 0xf26d, + 0xf26e: 0xf26e, + 0xf26f: 0xf26f, + 0xf270: 0xf270, + 0xf271: 0xf271, + 0xf272: 0xf272, + 0xf273: 0xf273, + 0xf274: 0xf274, + 0xf275: 0xf275, + 0xf276: 0xf276, + 0xf277: 0xf277, + 0xf278: 0xf278, + 0xf279: 0xf279, + 0xf27a: 0xf27a, + 0xf27b: 0xf27b, + 0xf27c: 0xf27c, + 0xf27d: 0xf27d, + 0xf27e: 0xf27e, + 0xf27f: 0xf27f, + 0xf280: 0xf280, + 0xf281: 0xf281, + 0xf282: 0xf282, + 0xf283: 0xf283, + 0xf284: 0xf284, + 0xf285: 0xf285, + 0xf286: 0xf286, + 0xf287: 0xf287, + 0xf288: 0xf288, + 0xf289: 0xf289, + 0xf28a: 0xf28a, + 0xf28b: 0xf28b, + 0xf28c: 0xf28c, + 0xf28d: 0xf28d, + 0xf28e: 0xf28e, + 0xf28f: 0xf28f, + 0xf290: 0xf290, + 0xf291: 0xf291, + 0xf292: 0xf292, + 0xf293: 0xf293, + 0xf294: 0xf294, + 0xf295: 0xf295, + 0xf296: 0xf296, + 0xf297: 0xf297, + 0xf298: 0xf298, + 0xf299: 0xf299, + 0xf29a: 0xf29a, + 0xf29b: 0xf29b, + 0xf29c: 0xf29c, + 0xf29d: 0xf29d, + 0xf29e: 0xf29e, + 0xf29f: 0xf29f, + 0xf2a0: 0xf2a0, + 0xf2a1: 0xf2a1, + 0xf2a2: 0xf2a2, + 0xf2a3: 0xf2a3, + 0xf2a4: 0xf2a4, + 0xf2a5: 0xf2a5, + 0xf2a6: 0xf2a6, + 0xf2a7: 0xf2a7, + 0xf2a8: 0xf2a8, + 0xf2a9: 0xf2a9, + 0xf2aa: 0xf2aa, + 0xf2ab: 0xf2ab, + 0xf2ac: 0xf2ac, + 0xf2ad: 0xf2ad, + 0xf2ae: 0xf2ae, + 0xf2af: 0xf2af, + 0xf2b0: 0xf2b0, + 0xf2b1: 0xf2b1, + 0xf2b2: 0xf2b2, + 0xf2b3: 0xf2b3, + 0xf2b4: 0xf2b4, + 0xf2b5: 0xf2b5, + 0xf2b6: 0xf2b6, + 0xf2b7: 0xf2b7, + 0xf2b8: 0xf2b8, + 0xf2b9: 0xf2b9, + 0xf2ba: 0xf2ba, + 0xf2bb: 0xf2bb, + 0xf2bc: 0xf2bc, + 0xf2bd: 0xf2bd, + 0xf2be: 0xf2be, + 0xf2bf: 0xf2bf, + 0xf2c0: 0xf2c0, + 0xf2c1: 0xf2c1, + 0xf2c2: 0xf2c2, + 0xf2c3: 0xf2c3, + 0xf2c4: 0xf2c4, + 0xf2c5: 0xf2c5, + 0xf2c6: 0xf2c6, + 0xf2c7: 0xf2c7, + 0xf2c8: 0xf2c8, + 0xf2c9: 0xf2c9, + 0xf2ca: 0xf2ca, + 0xf2cb: 0xf2cb, + 0xf2cc: 0xf2cc, + 0xf2cd: 0xf2cd, + 0xf2ce: 0xf2ce, + 0xf2cf: 0xf2cf, + 0xf2d0: 0xf2d0, + 0xf2d1: 0xf2d1, + 0xf2d2: 0xf2d2, + 0xf2d3: 0xf2d3, + 0xf2d4: 0xf2d4, + 0xf2d5: 0xf2d5, + 0xf2d6: 0xf2d6, + 0xf2d7: 0xf2d7, + 0xf2d8: 0xf2d8, + 0xf2d9: 0xf2d9, + 0xf2da: 0xf2da, + 0xf2db: 0xf2db, + 0xf2dc: 0xf2dc, + 0xf2dd: 0xf2dd, + 0xf2de: 0xf2de, + 0xf2df: 0xf2df, + 0xf2e0: 0xf2e0, + 0xf2e1: 0xf2e1, + 0xf2e2: 0xf2e2, + 0xf2e3: 0xf2e3, + 0xf2e4: 0xf2e4, + 0xf2e5: 0xf2e5, + 0xf2e6: 0xf2e6, + 0xf2e7: 0xf2e7, + 0xf2e8: 0xf2e8, + 0xf2e9: 0xf2e9, + 0xf2ea: 0xf2ea, + 0xf2eb: 0xf2eb, + 0xf2ec: 0xf2ec, + 0xf2ed: 0xf2ed, + 0xf2ee: 0xf2ee, + 0xf2ef: 0xf2ef, + 0xf2f0: 0xf2f0, + 0xf2f1: 0xf2f1, + 0xf2f2: 0xf2f2, + 0xf2f3: 0xf2f3, + 0xf2f4: 0xf2f4, + 0xf2f5: 0xf2f5, + 0xf2f6: 0xf2f6, + 0xf2f7: 0xf2f7, + 0xf2f8: 0xf2f8, + 0xf2f9: 0xf2f9, + 0xf2fa: 0xf2fa, + 0xf2fb: 0xf2fb, + 0xf2fc: 0xf2fc, + 0xf2fd: 0xf2fd, + 0xf2fe: 0xf2fe, + 0xf2ff: 0xf2ff, + }, + "Font Awesome Extension": { + 0xe000: 0xe200, + 0xe001: 0xe201, + 0xe002: 0xe202, + 0xe003: 0xe203, + 0xe004: 0xe204, + 0xe005: 0xe205, + 0xe006: 0xe206, + 0xe007: 0xe207, + 0xe008: 0xe208, + 0xe009: 0xe209, + 0xe00a: 0xe20a, + 0xe00b: 0xe20b, + 0xe00c: 0xe20c, + 0xe00d: 0xe20d, + 0xe00e: 0xe20e, + 0xe00f: 0xe20f, + 0xe010: 0xe210, + 0xe011: 0xe211, + 0xe012: 0xe212, + 0xe013: 0xe213, + 0xe014: 0xe214, + 0xe015: 0xe215, + 0xe016: 0xe216, + 0xe017: 0xe217, + 0xe018: 0xe218, + 0xe019: 0xe219, + 0xe01a: 0xe21a, + 0xe01b: 0xe21b, + 0xe01c: 0xe21c, + 0xe01d: 0xe21d, + 0xe01e: 0xe21e, + 0xe01f: 0xe21f, + 0xe020: 0xe220, + 0xe021: 0xe221, + 0xe022: 0xe222, + 0xe023: 0xe223, + 0xe024: 0xe224, + 0xe025: 0xe225, + 0xe026: 0xe226, + 0xe027: 0xe227, + 0xe028: 0xe228, + 0xe029: 0xe229, + 0xe02a: 0xe22a, + 0xe02b: 0xe22b, + 0xe02c: 0xe22c, + 0xe02d: 0xe22d, + 0xe02e: 0xe22e, + 0xe02f: 0xe22f, + 0xe030: 0xe230, + 0xe031: 0xe231, + 0xe032: 0xe232, + 0xe033: 0xe233, + 0xe034: 0xe234, + 0xe035: 0xe235, + 0xe036: 0xe236, + 0xe037: 0xe237, + 0xe038: 0xe238, + 0xe039: 0xe239, + 0xe03a: 0xe23a, + 0xe03b: 0xe23b, + 0xe03c: 0xe23c, + 0xe03d: 0xe23d, + 0xe03e: 0xe23e, + 0xe03f: 0xe23f, + 0xe040: 0xe240, + 0xe041: 0xe241, + 0xe042: 0xe242, + 0xe043: 0xe243, + 0xe044: 0xe244, + 0xe045: 0xe245, + 0xe046: 0xe246, + 0xe047: 0xe247, + 0xe048: 0xe248, + 0xe049: 0xe249, + 0xe04a: 0xe24a, + 0xe04b: 0xe24b, + 0xe04c: 0xe24c, + 0xe04d: 0xe24d, + 0xe04e: 0xe24e, + 0xe04f: 0xe24f, + 0xe050: 0xe250, + 0xe051: 0xe251, + 0xe052: 0xe252, + 0xe053: 0xe253, + 0xe054: 0xe254, + 0xe055: 0xe255, + 0xe056: 0xe256, + 0xe057: 0xe257, + 0xe058: 0xe258, + 0xe059: 0xe259, + 0xe05a: 0xe25a, + 0xe05b: 0xe25b, + 0xe05c: 0xe25c, + 0xe05d: 0xe25d, + 0xe05e: 0xe25e, + 0xe05f: 0xe25f, + 0xe060: 0xe260, + 0xe061: 0xe261, + 0xe062: 0xe262, + 0xe063: 0xe263, + 0xe064: 0xe264, + 0xe065: 0xe265, + 0xe066: 0xe266, + 0xe067: 0xe267, + 0xe068: 0xe268, + 0xe069: 0xe269, + 0xe06a: 0xe26a, + 0xe06b: 0xe26b, + 0xe06c: 0xe26c, + 0xe06d: 0xe26d, + 0xe06e: 0xe26e, + 0xe06f: 0xe26f, + 0xe070: 0xe270, + 0xe071: 0xe271, + 0xe072: 0xe272, + 0xe073: 0xe273, + 0xe074: 0xe274, + 0xe075: 0xe275, + 0xe076: 0xe276, + 0xe077: 0xe277, + 0xe078: 0xe278, + 0xe079: 0xe279, + 0xe07a: 0xe27a, + 0xe07b: 0xe27b, + 0xe07c: 0xe27c, + 0xe07d: 0xe27d, + 0xe07e: 0xe27e, + 0xe07f: 0xe27f, + 0xe080: 0xe280, + 0xe081: 0xe281, + 0xe082: 0xe282, + 0xe083: 0xe283, + 0xe084: 0xe284, + 0xe085: 0xe285, + 0xe086: 0xe286, + 0xe087: 0xe287, + 0xe088: 0xe288, + 0xe089: 0xe289, + 0xe08a: 0xe28a, + 0xe08b: 0xe28b, + 0xe08c: 0xe28c, + 0xe08d: 0xe28d, + 0xe08e: 0xe28e, + 0xe08f: 0xe28f, + 0xe090: 0xe290, + 0xe091: 0xe291, + 0xe092: 0xe292, + 0xe093: 0xe293, + 0xe094: 0xe294, + 0xe095: 0xe295, + 0xe096: 0xe296, + 0xe097: 0xe297, + 0xe098: 0xe298, + 0xe099: 0xe299, + 0xe09a: 0xe29a, + 0xe09b: 0xe29b, + 0xe09c: 0xe29c, + 0xe09d: 0xe29d, + 0xe09e: 0xe29e, + 0xe09f: 0xe29f, + 0xe0a0: 0xe2a0, + 0xe0a1: 0xe2a1, + 0xe0a2: 0xe2a2, + 0xe0a3: 0xe2a3, + 0xe0a4: 0xe2a4, + 0xe0a5: 0xe2a5, + 0xe0a6: 0xe2a6, + 0xe0a7: 0xe2a7, + 0xe0a8: 0xe2a8, + 0xe0a9: 0xe2a9, + }, + "Power Symbols": { + 0x23fb: 0x23fb, + 0x23fc: 0x23fc, + 0x23fd: 0x23fd, + 0x23fe: 0x23fe, + 0x2b58: 0x2b58, + }, + "Material": { + 0xf0001: 0xf0001, + 0xf0002: 0xf0002, + 0xf0003: 0xf0003, + 0xf0004: 0xf0004, + 0xf0005: 0xf0005, + 0xf0006: 0xf0006, + 0xf0007: 0xf0007, + 0xf0008: 0xf0008, + 0xf0009: 0xf0009, + 0xf000a: 0xf000a, + 0xf000b: 0xf000b, + 0xf000c: 0xf000c, + 0xf000d: 0xf000d, + 0xf000e: 0xf000e, + 0xf000f: 0xf000f, + 0xf0010: 0xf0010, + 0xf0011: 0xf0011, + 0xf0012: 0xf0012, + 0xf0013: 0xf0013, + 0xf0014: 0xf0014, + 0xf0015: 0xf0015, + 0xf0016: 0xf0016, + 0xf0017: 0xf0017, + 0xf0018: 0xf0018, + 0xf0019: 0xf0019, + 0xf001a: 0xf001a, + 0xf001b: 0xf001b, + 0xf001c: 0xf001c, + 0xf001d: 0xf001d, + 0xf001e: 0xf001e, + 0xf001f: 0xf001f, + 0xf0020: 0xf0020, + 0xf0021: 0xf0021, + 0xf0022: 0xf0022, + 0xf0023: 0xf0023, + 0xf0024: 0xf0024, + 0xf0025: 0xf0025, + 0xf0026: 0xf0026, + 0xf0027: 0xf0027, + 0xf0028: 0xf0028, + 0xf0029: 0xf0029, + 0xf002a: 0xf002a, + 0xf002b: 0xf002b, + 0xf002c: 0xf002c, + 0xf002d: 0xf002d, + 0xf002e: 0xf002e, + 0xf002f: 0xf002f, + 0xf0030: 0xf0030, + 0xf0031: 0xf0031, + 0xf0032: 0xf0032, + 0xf0033: 0xf0033, + 0xf0034: 0xf0034, + 0xf0035: 0xf0035, + 0xf0036: 0xf0036, + 0xf0037: 0xf0037, + 0xf0038: 0xf0038, + 0xf0039: 0xf0039, + 0xf003a: 0xf003a, + 0xf003b: 0xf003b, + 0xf003c: 0xf003c, + 0xf003d: 0xf003d, + 0xf003e: 0xf003e, + 0xf003f: 0xf003f, + 0xf0040: 0xf0040, + 0xf0041: 0xf0041, + 0xf0042: 0xf0042, + 0xf0043: 0xf0043, + 0xf0044: 0xf0044, + 0xf0045: 0xf0045, + 0xf0046: 0xf0046, + 0xf0047: 0xf0047, + 0xf0048: 0xf0048, + 0xf0049: 0xf0049, + 0xf004a: 0xf004a, + 0xf004b: 0xf004b, + 0xf004c: 0xf004c, + 0xf004d: 0xf004d, + 0xf004e: 0xf004e, + 0xf004f: 0xf004f, + 0xf0050: 0xf0050, + 0xf0051: 0xf0051, + 0xf0052: 0xf0052, + 0xf0053: 0xf0053, + 0xf0054: 0xf0054, + 0xf0055: 0xf0055, + 0xf0056: 0xf0056, + 0xf0057: 0xf0057, + 0xf0058: 0xf0058, + 0xf0059: 0xf0059, + 0xf005a: 0xf005a, + 0xf005b: 0xf005b, + 0xf005c: 0xf005c, + 0xf005d: 0xf005d, + 0xf005e: 0xf005e, + 0xf005f: 0xf005f, + 0xf0060: 0xf0060, + 0xf0061: 0xf0061, + 0xf0062: 0xf0062, + 0xf0063: 0xf0063, + 0xf0064: 0xf0064, + 0xf0065: 0xf0065, + 0xf0066: 0xf0066, + 0xf0067: 0xf0067, + 0xf0068: 0xf0068, + 0xf0069: 0xf0069, + 0xf006a: 0xf006a, + 0xf006b: 0xf006b, + 0xf006c: 0xf006c, + 0xf006d: 0xf006d, + 0xf006e: 0xf006e, + 0xf006f: 0xf006f, + 0xf0070: 0xf0070, + 0xf0071: 0xf0071, + 0xf0072: 0xf0072, + 0xf0073: 0xf0073, + 0xf0074: 0xf0074, + 0xf0075: 0xf0075, + 0xf0076: 0xf0076, + 0xf0077: 0xf0077, + 0xf0078: 0xf0078, + 0xf0079: 0xf0079, + 0xf007a: 0xf007a, + 0xf007b: 0xf007b, + 0xf007c: 0xf007c, + 0xf007d: 0xf007d, + 0xf007e: 0xf007e, + 0xf007f: 0xf007f, + 0xf0080: 0xf0080, + 0xf0081: 0xf0081, + 0xf0082: 0xf0082, + 0xf0083: 0xf0083, + 0xf0084: 0xf0084, + 0xf0085: 0xf0085, + 0xf0086: 0xf0086, + 0xf0087: 0xf0087, + 0xf0088: 0xf0088, + 0xf0089: 0xf0089, + 0xf008a: 0xf008a, + 0xf008b: 0xf008b, + 0xf008c: 0xf008c, + 0xf008d: 0xf008d, + 0xf008e: 0xf008e, + 0xf008f: 0xf008f, + 0xf0090: 0xf0090, + 0xf0091: 0xf0091, + 0xf0092: 0xf0092, + 0xf0093: 0xf0093, + 0xf0094: 0xf0094, + 0xf0095: 0xf0095, + 0xf0096: 0xf0096, + 0xf0097: 0xf0097, + 0xf0098: 0xf0098, + 0xf0099: 0xf0099, + 0xf009a: 0xf009a, + 0xf009b: 0xf009b, + 0xf009c: 0xf009c, + 0xf009d: 0xf009d, + 0xf009e: 0xf009e, + 0xf009f: 0xf009f, + 0xf00a0: 0xf00a0, + 0xf00a1: 0xf00a1, + 0xf00a2: 0xf00a2, + 0xf00a3: 0xf00a3, + 0xf00a4: 0xf00a4, + 0xf00a5: 0xf00a5, + 0xf00a6: 0xf00a6, + 0xf00a7: 0xf00a7, + 0xf00a8: 0xf00a8, + 0xf00a9: 0xf00a9, + 0xf00aa: 0xf00aa, + 0xf00ab: 0xf00ab, + 0xf00ac: 0xf00ac, + 0xf00ad: 0xf00ad, + 0xf00ae: 0xf00ae, + 0xf00af: 0xf00af, + 0xf00b0: 0xf00b0, + 0xf00b1: 0xf00b1, + 0xf00b2: 0xf00b2, + 0xf00b3: 0xf00b3, + 0xf00b4: 0xf00b4, + 0xf00b5: 0xf00b5, + 0xf00b6: 0xf00b6, + 0xf00b7: 0xf00b7, + 0xf00b8: 0xf00b8, + 0xf00b9: 0xf00b9, + 0xf00ba: 0xf00ba, + 0xf00bb: 0xf00bb, + 0xf00bc: 0xf00bc, + 0xf00bd: 0xf00bd, + 0xf00be: 0xf00be, + 0xf00bf: 0xf00bf, + 0xf00c0: 0xf00c0, + 0xf00c1: 0xf00c1, + 0xf00c2: 0xf00c2, + 0xf00c3: 0xf00c3, + 0xf00c4: 0xf00c4, + 0xf00c5: 0xf00c5, + 0xf00c6: 0xf00c6, + 0xf00c7: 0xf00c7, + 0xf00c8: 0xf00c8, + 0xf00c9: 0xf00c9, + 0xf00ca: 0xf00ca, + 0xf00cb: 0xf00cb, + 0xf00cc: 0xf00cc, + 0xf00cd: 0xf00cd, + 0xf00ce: 0xf00ce, + 0xf00cf: 0xf00cf, + 0xf00d0: 0xf00d0, + 0xf00d1: 0xf00d1, + 0xf00d2: 0xf00d2, + 0xf00d3: 0xf00d3, + 0xf00d4: 0xf00d4, + 0xf00d5: 0xf00d5, + 0xf00d6: 0xf00d6, + 0xf00d7: 0xf00d7, + 0xf00d8: 0xf00d8, + 0xf00d9: 0xf00d9, + 0xf00da: 0xf00da, + 0xf00db: 0xf00db, + 0xf00dc: 0xf00dc, + 0xf00dd: 0xf00dd, + 0xf00de: 0xf00de, + 0xf00df: 0xf00df, + 0xf00e0: 0xf00e0, + 0xf00e1: 0xf00e1, + 0xf00e2: 0xf00e2, + 0xf00e3: 0xf00e3, + 0xf00e4: 0xf00e4, + 0xf00e5: 0xf00e5, + 0xf00e6: 0xf00e6, + 0xf00e7: 0xf00e7, + 0xf00e8: 0xf00e8, + 0xf00e9: 0xf00e9, + 0xf00ea: 0xf00ea, + 0xf00eb: 0xf00eb, + 0xf00ec: 0xf00ec, + 0xf00ed: 0xf00ed, + 0xf00ee: 0xf00ee, + 0xf00ef: 0xf00ef, + 0xf00f0: 0xf00f0, + 0xf00f1: 0xf00f1, + 0xf00f2: 0xf00f2, + 0xf00f3: 0xf00f3, + 0xf00f4: 0xf00f4, + 0xf00f5: 0xf00f5, + 0xf00f6: 0xf00f6, + 0xf00f7: 0xf00f7, + 0xf00f8: 0xf00f8, + 0xf00f9: 0xf00f9, + 0xf00fa: 0xf00fa, + 0xf00fb: 0xf00fb, + 0xf00fc: 0xf00fc, + 0xf00fd: 0xf00fd, + 0xf00fe: 0xf00fe, + 0xf00ff: 0xf00ff, + 0xf0100: 0xf0100, + 0xf0101: 0xf0101, + 0xf0102: 0xf0102, + 0xf0103: 0xf0103, + 0xf0104: 0xf0104, + 0xf0105: 0xf0105, + 0xf0106: 0xf0106, + 0xf0107: 0xf0107, + 0xf0108: 0xf0108, + 0xf0109: 0xf0109, + 0xf010a: 0xf010a, + 0xf010b: 0xf010b, + 0xf010c: 0xf010c, + 0xf010d: 0xf010d, + 0xf010e: 0xf010e, + 0xf010f: 0xf010f, + 0xf0110: 0xf0110, + 0xf0111: 0xf0111, + 0xf0112: 0xf0112, + 0xf0113: 0xf0113, + 0xf0114: 0xf0114, + 0xf0115: 0xf0115, + 0xf0116: 0xf0116, + 0xf0117: 0xf0117, + 0xf0118: 0xf0118, + 0xf0119: 0xf0119, + 0xf011a: 0xf011a, + 0xf011b: 0xf011b, + 0xf011c: 0xf011c, + 0xf011d: 0xf011d, + 0xf011e: 0xf011e, + 0xf011f: 0xf011f, + 0xf0120: 0xf0120, + 0xf0121: 0xf0121, + 0xf0122: 0xf0122, + 0xf0123: 0xf0123, + 0xf0124: 0xf0124, + 0xf0125: 0xf0125, + 0xf0126: 0xf0126, + 0xf0127: 0xf0127, + 0xf0128: 0xf0128, + 0xf0129: 0xf0129, + 0xf012a: 0xf012a, + 0xf012b: 0xf012b, + 0xf012c: 0xf012c, + 0xf012d: 0xf012d, + 0xf012e: 0xf012e, + 0xf012f: 0xf012f, + 0xf0130: 0xf0130, + 0xf0131: 0xf0131, + 0xf0132: 0xf0132, + 0xf0133: 0xf0133, + 0xf0134: 0xf0134, + 0xf0135: 0xf0135, + 0xf0136: 0xf0136, + 0xf0137: 0xf0137, + 0xf0138: 0xf0138, + 0xf0139: 0xf0139, + 0xf013a: 0xf013a, + 0xf013b: 0xf013b, + 0xf013c: 0xf013c, + 0xf013d: 0xf013d, + 0xf013e: 0xf013e, + 0xf013f: 0xf013f, + 0xf0140: 0xf0140, + 0xf0141: 0xf0141, + 0xf0142: 0xf0142, + 0xf0143: 0xf0143, + 0xf0144: 0xf0144, + 0xf0145: 0xf0145, + 0xf0146: 0xf0146, + 0xf0147: 0xf0147, + 0xf0148: 0xf0148, + 0xf0149: 0xf0149, + 0xf014a: 0xf014a, + 0xf014b: 0xf014b, + 0xf014c: 0xf014c, + 0xf014d: 0xf014d, + 0xf014e: 0xf014e, + 0xf014f: 0xf014f, + 0xf0150: 0xf0150, + 0xf0151: 0xf0151, + 0xf0152: 0xf0152, + 0xf0153: 0xf0153, + 0xf0154: 0xf0154, + 0xf0155: 0xf0155, + 0xf0156: 0xf0156, + 0xf0157: 0xf0157, + 0xf0158: 0xf0158, + 0xf0159: 0xf0159, + 0xf015a: 0xf015a, + 0xf015b: 0xf015b, + 0xf015c: 0xf015c, + 0xf015d: 0xf015d, + 0xf015e: 0xf015e, + 0xf015f: 0xf015f, + 0xf0160: 0xf0160, + 0xf0161: 0xf0161, + 0xf0162: 0xf0162, + 0xf0163: 0xf0163, + 0xf0164: 0xf0164, + 0xf0165: 0xf0165, + 0xf0166: 0xf0166, + 0xf0167: 0xf0167, + 0xf0168: 0xf0168, + 0xf0169: 0xf0169, + 0xf016a: 0xf016a, + 0xf016b: 0xf016b, + 0xf016c: 0xf016c, + 0xf016d: 0xf016d, + 0xf016e: 0xf016e, + 0xf016f: 0xf016f, + 0xf0170: 0xf0170, + 0xf0171: 0xf0171, + 0xf0172: 0xf0172, + 0xf0173: 0xf0173, + 0xf0174: 0xf0174, + 0xf0175: 0xf0175, + 0xf0176: 0xf0176, + 0xf0177: 0xf0177, + 0xf0178: 0xf0178, + 0xf0179: 0xf0179, + 0xf017a: 0xf017a, + 0xf017b: 0xf017b, + 0xf017c: 0xf017c, + 0xf017d: 0xf017d, + 0xf017e: 0xf017e, + 0xf017f: 0xf017f, + 0xf0180: 0xf0180, + 0xf0181: 0xf0181, + 0xf0182: 0xf0182, + 0xf0183: 0xf0183, + 0xf0184: 0xf0184, + 0xf0185: 0xf0185, + 0xf0186: 0xf0186, + 0xf0187: 0xf0187, + 0xf0188: 0xf0188, + 0xf0189: 0xf0189, + 0xf018a: 0xf018a, + 0xf018b: 0xf018b, + 0xf018c: 0xf018c, + 0xf018d: 0xf018d, + 0xf018e: 0xf018e, + 0xf018f: 0xf018f, + 0xf0190: 0xf0190, + 0xf0191: 0xf0191, + 0xf0192: 0xf0192, + 0xf0193: 0xf0193, + 0xf0194: 0xf0194, + 0xf0195: 0xf0195, + 0xf0196: 0xf0196, + 0xf0197: 0xf0197, + 0xf0198: 0xf0198, + 0xf0199: 0xf0199, + 0xf019a: 0xf019a, + 0xf019b: 0xf019b, + 0xf019c: 0xf019c, + 0xf019d: 0xf019d, + 0xf019e: 0xf019e, + 0xf019f: 0xf019f, + 0xf01a0: 0xf01a0, + 0xf01a1: 0xf01a1, + 0xf01a2: 0xf01a2, + 0xf01a3: 0xf01a3, + 0xf01a4: 0xf01a4, + 0xf01a5: 0xf01a5, + 0xf01a6: 0xf01a6, + 0xf01a7: 0xf01a7, + 0xf01a8: 0xf01a8, + 0xf01a9: 0xf01a9, + 0xf01aa: 0xf01aa, + 0xf01ab: 0xf01ab, + 0xf01ac: 0xf01ac, + 0xf01ad: 0xf01ad, + 0xf01ae: 0xf01ae, + 0xf01af: 0xf01af, + 0xf01b0: 0xf01b0, + 0xf01b1: 0xf01b1, + 0xf01b2: 0xf01b2, + 0xf01b3: 0xf01b3, + 0xf01b4: 0xf01b4, + 0xf01b5: 0xf01b5, + 0xf01b6: 0xf01b6, + 0xf01b7: 0xf01b7, + 0xf01b8: 0xf01b8, + 0xf01b9: 0xf01b9, + 0xf01ba: 0xf01ba, + 0xf01bb: 0xf01bb, + 0xf01bc: 0xf01bc, + 0xf01bd: 0xf01bd, + 0xf01be: 0xf01be, + 0xf01bf: 0xf01bf, + 0xf01c0: 0xf01c0, + 0xf01c1: 0xf01c1, + 0xf01c2: 0xf01c2, + 0xf01c3: 0xf01c3, + 0xf01c4: 0xf01c4, + 0xf01c5: 0xf01c5, + 0xf01c6: 0xf01c6, + 0xf01c7: 0xf01c7, + 0xf01c8: 0xf01c8, + 0xf01c9: 0xf01c9, + 0xf01ca: 0xf01ca, + 0xf01cb: 0xf01cb, + 0xf01cc: 0xf01cc, + 0xf01cd: 0xf01cd, + 0xf01ce: 0xf01ce, + 0xf01cf: 0xf01cf, + 0xf01d0: 0xf01d0, + 0xf01d1: 0xf01d1, + 0xf01d2: 0xf01d2, + 0xf01d3: 0xf01d3, + 0xf01d4: 0xf01d4, + 0xf01d5: 0xf01d5, + 0xf01d6: 0xf01d6, + 0xf01d7: 0xf01d7, + 0xf01d8: 0xf01d8, + 0xf01d9: 0xf01d9, + 0xf01da: 0xf01da, + 0xf01db: 0xf01db, + 0xf01dc: 0xf01dc, + 0xf01dd: 0xf01dd, + 0xf01de: 0xf01de, + 0xf01df: 0xf01df, + 0xf01e0: 0xf01e0, + 0xf01e1: 0xf01e1, + 0xf01e2: 0xf01e2, + 0xf01e3: 0xf01e3, + 0xf01e4: 0xf01e4, + 0xf01e5: 0xf01e5, + 0xf01e6: 0xf01e6, + 0xf01e7: 0xf01e7, + 0xf01e8: 0xf01e8, + 0xf01e9: 0xf01e9, + 0xf01ea: 0xf01ea, + 0xf01eb: 0xf01eb, + 0xf01ec: 0xf01ec, + 0xf01ed: 0xf01ed, + 0xf01ee: 0xf01ee, + 0xf01ef: 0xf01ef, + 0xf01f0: 0xf01f0, + 0xf01f1: 0xf01f1, + 0xf01f2: 0xf01f2, + 0xf01f3: 0xf01f3, + 0xf01f4: 0xf01f4, + 0xf01f5: 0xf01f5, + 0xf01f6: 0xf01f6, + 0xf01f7: 0xf01f7, + 0xf01f8: 0xf01f8, + 0xf01f9: 0xf01f9, + 0xf01fa: 0xf01fa, + 0xf01fb: 0xf01fb, + 0xf01fc: 0xf01fc, + 0xf01fd: 0xf01fd, + 0xf01fe: 0xf01fe, + 0xf01ff: 0xf01ff, + 0xf0200: 0xf0200, + 0xf0201: 0xf0201, + 0xf0202: 0xf0202, + 0xf0203: 0xf0203, + 0xf0204: 0xf0204, + 0xf0205: 0xf0205, + 0xf0206: 0xf0206, + 0xf0207: 0xf0207, + 0xf0208: 0xf0208, + 0xf0209: 0xf0209, + 0xf020a: 0xf020a, + 0xf020b: 0xf020b, + 0xf020c: 0xf020c, + 0xf020d: 0xf020d, + 0xf020e: 0xf020e, + 0xf020f: 0xf020f, + 0xf0210: 0xf0210, + 0xf0211: 0xf0211, + 0xf0212: 0xf0212, + 0xf0213: 0xf0213, + 0xf0214: 0xf0214, + 0xf0215: 0xf0215, + 0xf0216: 0xf0216, + 0xf0217: 0xf0217, + 0xf0218: 0xf0218, + 0xf0219: 0xf0219, + 0xf021a: 0xf021a, + 0xf021b: 0xf021b, + 0xf021c: 0xf021c, + 0xf021d: 0xf021d, + 0xf021e: 0xf021e, + 0xf021f: 0xf021f, + 0xf0220: 0xf0220, + 0xf0221: 0xf0221, + 0xf0222: 0xf0222, + 0xf0223: 0xf0223, + 0xf0224: 0xf0224, + 0xf0225: 0xf0225, + 0xf0226: 0xf0226, + 0xf0227: 0xf0227, + 0xf0228: 0xf0228, + 0xf0229: 0xf0229, + 0xf022a: 0xf022a, + 0xf022b: 0xf022b, + 0xf022c: 0xf022c, + 0xf022d: 0xf022d, + 0xf022e: 0xf022e, + 0xf022f: 0xf022f, + 0xf0230: 0xf0230, + 0xf0231: 0xf0231, + 0xf0232: 0xf0232, + 0xf0233: 0xf0233, + 0xf0234: 0xf0234, + 0xf0235: 0xf0235, + 0xf0236: 0xf0236, + 0xf0237: 0xf0237, + 0xf0238: 0xf0238, + 0xf0239: 0xf0239, + 0xf023a: 0xf023a, + 0xf023b: 0xf023b, + 0xf023c: 0xf023c, + 0xf023d: 0xf023d, + 0xf023e: 0xf023e, + 0xf023f: 0xf023f, + 0xf0240: 0xf0240, + 0xf0241: 0xf0241, + 0xf0242: 0xf0242, + 0xf0243: 0xf0243, + 0xf0244: 0xf0244, + 0xf0245: 0xf0245, + 0xf0246: 0xf0246, + 0xf0247: 0xf0247, + 0xf0248: 0xf0248, + 0xf0249: 0xf0249, + 0xf024a: 0xf024a, + 0xf024b: 0xf024b, + 0xf024c: 0xf024c, + 0xf024d: 0xf024d, + 0xf024e: 0xf024e, + 0xf024f: 0xf024f, + 0xf0250: 0xf0250, + 0xf0251: 0xf0251, + 0xf0252: 0xf0252, + 0xf0253: 0xf0253, + 0xf0254: 0xf0254, + 0xf0255: 0xf0255, + 0xf0256: 0xf0256, + 0xf0257: 0xf0257, + 0xf0258: 0xf0258, + 0xf0259: 0xf0259, + 0xf025a: 0xf025a, + 0xf025b: 0xf025b, + 0xf025c: 0xf025c, + 0xf025d: 0xf025d, + 0xf025e: 0xf025e, + 0xf025f: 0xf025f, + 0xf0260: 0xf0260, + 0xf0261: 0xf0261, + 0xf0262: 0xf0262, + 0xf0263: 0xf0263, + 0xf0264: 0xf0264, + 0xf0265: 0xf0265, + 0xf0266: 0xf0266, + 0xf0267: 0xf0267, + 0xf0268: 0xf0268, + 0xf0269: 0xf0269, + 0xf026a: 0xf026a, + 0xf026b: 0xf026b, + 0xf026c: 0xf026c, + 0xf026d: 0xf026d, + 0xf026e: 0xf026e, + 0xf026f: 0xf026f, + 0xf0270: 0xf0270, + 0xf0271: 0xf0271, + 0xf0272: 0xf0272, + 0xf0273: 0xf0273, + 0xf0274: 0xf0274, + 0xf0275: 0xf0275, + 0xf0276: 0xf0276, + 0xf0277: 0xf0277, + 0xf0278: 0xf0278, + 0xf0279: 0xf0279, + 0xf027a: 0xf027a, + 0xf027b: 0xf027b, + 0xf027c: 0xf027c, + 0xf027d: 0xf027d, + 0xf027e: 0xf027e, + 0xf027f: 0xf027f, + 0xf0280: 0xf0280, + 0xf0281: 0xf0281, + 0xf0282: 0xf0282, + 0xf0283: 0xf0283, + 0xf0284: 0xf0284, + 0xf0285: 0xf0285, + 0xf0286: 0xf0286, + 0xf0287: 0xf0287, + 0xf0288: 0xf0288, + 0xf0289: 0xf0289, + 0xf028a: 0xf028a, + 0xf028b: 0xf028b, + 0xf028c: 0xf028c, + 0xf028d: 0xf028d, + 0xf028e: 0xf028e, + 0xf028f: 0xf028f, + 0xf0290: 0xf0290, + 0xf0291: 0xf0291, + 0xf0292: 0xf0292, + 0xf0293: 0xf0293, + 0xf0294: 0xf0294, + 0xf0295: 0xf0295, + 0xf0296: 0xf0296, + 0xf0297: 0xf0297, + 0xf0298: 0xf0298, + 0xf0299: 0xf0299, + 0xf029a: 0xf029a, + 0xf029b: 0xf029b, + 0xf029c: 0xf029c, + 0xf029d: 0xf029d, + 0xf029e: 0xf029e, + 0xf029f: 0xf029f, + 0xf02a0: 0xf02a0, + 0xf02a1: 0xf02a1, + 0xf02a2: 0xf02a2, + 0xf02a3: 0xf02a3, + 0xf02a4: 0xf02a4, + 0xf02a5: 0xf02a5, + 0xf02a6: 0xf02a6, + 0xf02a7: 0xf02a7, + 0xf02a8: 0xf02a8, + 0xf02a9: 0xf02a9, + 0xf02aa: 0xf02aa, + 0xf02ab: 0xf02ab, + 0xf02ac: 0xf02ac, + 0xf02ad: 0xf02ad, + 0xf02ae: 0xf02ae, + 0xf02af: 0xf02af, + 0xf02b0: 0xf02b0, + 0xf02b1: 0xf02b1, + 0xf02b2: 0xf02b2, + 0xf02b3: 0xf02b3, + 0xf02b4: 0xf02b4, + 0xf02b5: 0xf02b5, + 0xf02b6: 0xf02b6, + 0xf02b7: 0xf02b7, + 0xf02b8: 0xf02b8, + 0xf02b9: 0xf02b9, + 0xf02ba: 0xf02ba, + 0xf02bb: 0xf02bb, + 0xf02bc: 0xf02bc, + 0xf02bd: 0xf02bd, + 0xf02be: 0xf02be, + 0xf02bf: 0xf02bf, + 0xf02c0: 0xf02c0, + 0xf02c1: 0xf02c1, + 0xf02c2: 0xf02c2, + 0xf02c3: 0xf02c3, + 0xf02c4: 0xf02c4, + 0xf02c5: 0xf02c5, + 0xf02c6: 0xf02c6, + 0xf02c7: 0xf02c7, + 0xf02c8: 0xf02c8, + 0xf02c9: 0xf02c9, + 0xf02ca: 0xf02ca, + 0xf02cb: 0xf02cb, + 0xf02cc: 0xf02cc, + 0xf02cd: 0xf02cd, + 0xf02ce: 0xf02ce, + 0xf02cf: 0xf02cf, + 0xf02d0: 0xf02d0, + 0xf02d1: 0xf02d1, + 0xf02d2: 0xf02d2, + 0xf02d3: 0xf02d3, + 0xf02d4: 0xf02d4, + 0xf02d5: 0xf02d5, + 0xf02d6: 0xf02d6, + 0xf02d7: 0xf02d7, + 0xf02d8: 0xf02d8, + 0xf02d9: 0xf02d9, + 0xf02da: 0xf02da, + 0xf02db: 0xf02db, + 0xf02dc: 0xf02dc, + 0xf02dd: 0xf02dd, + 0xf02de: 0xf02de, + 0xf02df: 0xf02df, + 0xf02e0: 0xf02e0, + 0xf02e1: 0xf02e1, + 0xf02e2: 0xf02e2, + 0xf02e3: 0xf02e3, + 0xf02e4: 0xf02e4, + 0xf02e5: 0xf02e5, + 0xf02e6: 0xf02e6, + 0xf02e7: 0xf02e7, + 0xf02e8: 0xf02e8, + 0xf02e9: 0xf02e9, + 0xf02ea: 0xf02ea, + 0xf02eb: 0xf02eb, + 0xf02ec: 0xf02ec, + 0xf02ed: 0xf02ed, + 0xf02ee: 0xf02ee, + 0xf02ef: 0xf02ef, + 0xf02f0: 0xf02f0, + 0xf02f1: 0xf02f1, + 0xf02f2: 0xf02f2, + 0xf02f3: 0xf02f3, + 0xf02f4: 0xf02f4, + 0xf02f5: 0xf02f5, + 0xf02f6: 0xf02f6, + 0xf02f7: 0xf02f7, + 0xf02f8: 0xf02f8, + 0xf02f9: 0xf02f9, + 0xf02fa: 0xf02fa, + 0xf02fb: 0xf02fb, + 0xf02fc: 0xf02fc, + 0xf02fd: 0xf02fd, + 0xf02fe: 0xf02fe, + 0xf02ff: 0xf02ff, + 0xf0300: 0xf0300, + 0xf0301: 0xf0301, + 0xf0302: 0xf0302, + 0xf0303: 0xf0303, + 0xf0304: 0xf0304, + 0xf0305: 0xf0305, + 0xf0306: 0xf0306, + 0xf0307: 0xf0307, + 0xf0308: 0xf0308, + 0xf0309: 0xf0309, + 0xf030a: 0xf030a, + 0xf030b: 0xf030b, + 0xf030c: 0xf030c, + 0xf030d: 0xf030d, + 0xf030e: 0xf030e, + 0xf030f: 0xf030f, + 0xf0310: 0xf0310, + 0xf0311: 0xf0311, + 0xf0312: 0xf0312, + 0xf0313: 0xf0313, + 0xf0314: 0xf0314, + 0xf0315: 0xf0315, + 0xf0316: 0xf0316, + 0xf0317: 0xf0317, + 0xf0318: 0xf0318, + 0xf0319: 0xf0319, + 0xf031a: 0xf031a, + 0xf031b: 0xf031b, + 0xf031c: 0xf031c, + 0xf031d: 0xf031d, + 0xf031e: 0xf031e, + 0xf031f: 0xf031f, + 0xf0320: 0xf0320, + 0xf0321: 0xf0321, + 0xf0322: 0xf0322, + 0xf0323: 0xf0323, + 0xf0324: 0xf0324, + 0xf0325: 0xf0325, + 0xf0326: 0xf0326, + 0xf0327: 0xf0327, + 0xf0328: 0xf0328, + 0xf0329: 0xf0329, + 0xf032a: 0xf032a, + 0xf032b: 0xf032b, + 0xf032c: 0xf032c, + 0xf032d: 0xf032d, + 0xf032e: 0xf032e, + 0xf032f: 0xf032f, + 0xf0330: 0xf0330, + 0xf0331: 0xf0331, + 0xf0332: 0xf0332, + 0xf0333: 0xf0333, + 0xf0334: 0xf0334, + 0xf0335: 0xf0335, + 0xf0336: 0xf0336, + 0xf0337: 0xf0337, + 0xf0338: 0xf0338, + 0xf0339: 0xf0339, + 0xf033a: 0xf033a, + 0xf033b: 0xf033b, + 0xf033c: 0xf033c, + 0xf033d: 0xf033d, + 0xf033e: 0xf033e, + 0xf033f: 0xf033f, + 0xf0340: 0xf0340, + 0xf0341: 0xf0341, + 0xf0342: 0xf0342, + 0xf0343: 0xf0343, + 0xf0344: 0xf0344, + 0xf0345: 0xf0345, + 0xf0346: 0xf0346, + 0xf0347: 0xf0347, + 0xf0348: 0xf0348, + 0xf0349: 0xf0349, + 0xf034a: 0xf034a, + 0xf034b: 0xf034b, + 0xf034c: 0xf034c, + 0xf034d: 0xf034d, + 0xf034e: 0xf034e, + 0xf034f: 0xf034f, + 0xf0350: 0xf0350, + 0xf0351: 0xf0351, + 0xf0352: 0xf0352, + 0xf0353: 0xf0353, + 0xf0354: 0xf0354, + 0xf0355: 0xf0355, + 0xf0356: 0xf0356, + 0xf0357: 0xf0357, + 0xf0358: 0xf0358, + 0xf0359: 0xf0359, + 0xf035a: 0xf035a, + 0xf035b: 0xf035b, + 0xf035c: 0xf035c, + 0xf035d: 0xf035d, + 0xf035e: 0xf035e, + 0xf035f: 0xf035f, + 0xf0360: 0xf0360, + 0xf0361: 0xf0361, + 0xf0362: 0xf0362, + 0xf0363: 0xf0363, + 0xf0364: 0xf0364, + 0xf0365: 0xf0365, + 0xf0366: 0xf0366, + 0xf0367: 0xf0367, + 0xf0368: 0xf0368, + 0xf0369: 0xf0369, + 0xf036a: 0xf036a, + 0xf036b: 0xf036b, + 0xf036c: 0xf036c, + 0xf036d: 0xf036d, + 0xf036e: 0xf036e, + 0xf036f: 0xf036f, + 0xf0370: 0xf0370, + 0xf0371: 0xf0371, + 0xf0372: 0xf0372, + 0xf0373: 0xf0373, + 0xf0374: 0xf0374, + 0xf0375: 0xf0375, + 0xf0376: 0xf0376, + 0xf0377: 0xf0377, + 0xf0378: 0xf0378, + 0xf0379: 0xf0379, + 0xf037a: 0xf037a, + 0xf037b: 0xf037b, + 0xf037c: 0xf037c, + 0xf037d: 0xf037d, + 0xf037e: 0xf037e, + 0xf037f: 0xf037f, + 0xf0380: 0xf0380, + 0xf0381: 0xf0381, + 0xf0382: 0xf0382, + 0xf0383: 0xf0383, + 0xf0384: 0xf0384, + 0xf0385: 0xf0385, + 0xf0386: 0xf0386, + 0xf0387: 0xf0387, + 0xf0388: 0xf0388, + 0xf0389: 0xf0389, + 0xf038a: 0xf038a, + 0xf038b: 0xf038b, + 0xf038c: 0xf038c, + 0xf038d: 0xf038d, + 0xf038e: 0xf038e, + 0xf038f: 0xf038f, + 0xf0390: 0xf0390, + 0xf0391: 0xf0391, + 0xf0392: 0xf0392, + 0xf0393: 0xf0393, + 0xf0394: 0xf0394, + 0xf0395: 0xf0395, + 0xf0396: 0xf0396, + 0xf0397: 0xf0397, + 0xf0398: 0xf0398, + 0xf0399: 0xf0399, + 0xf039a: 0xf039a, + 0xf039b: 0xf039b, + 0xf039c: 0xf039c, + 0xf039d: 0xf039d, + 0xf039e: 0xf039e, + 0xf039f: 0xf039f, + 0xf03a0: 0xf03a0, + 0xf03a1: 0xf03a1, + 0xf03a2: 0xf03a2, + 0xf03a3: 0xf03a3, + 0xf03a4: 0xf03a4, + 0xf03a5: 0xf03a5, + 0xf03a6: 0xf03a6, + 0xf03a7: 0xf03a7, + 0xf03a8: 0xf03a8, + 0xf03a9: 0xf03a9, + 0xf03aa: 0xf03aa, + 0xf03ab: 0xf03ab, + 0xf03ac: 0xf03ac, + 0xf03ad: 0xf03ad, + 0xf03ae: 0xf03ae, + 0xf03af: 0xf03af, + 0xf03b0: 0xf03b0, + 0xf03b1: 0xf03b1, + 0xf03b2: 0xf03b2, + 0xf03b3: 0xf03b3, + 0xf03b4: 0xf03b4, + 0xf03b5: 0xf03b5, + 0xf03b6: 0xf03b6, + 0xf03b7: 0xf03b7, + 0xf03b8: 0xf03b8, + 0xf03b9: 0xf03b9, + 0xf03ba: 0xf03ba, + 0xf03bb: 0xf03bb, + 0xf03bc: 0xf03bc, + 0xf03bd: 0xf03bd, + 0xf03be: 0xf03be, + 0xf03bf: 0xf03bf, + 0xf03c0: 0xf03c0, + 0xf03c1: 0xf03c1, + 0xf03c2: 0xf03c2, + 0xf03c3: 0xf03c3, + 0xf03c4: 0xf03c4, + 0xf03c5: 0xf03c5, + 0xf03c6: 0xf03c6, + 0xf03c7: 0xf03c7, + 0xf03c8: 0xf03c8, + 0xf03c9: 0xf03c9, + 0xf03ca: 0xf03ca, + 0xf03cb: 0xf03cb, + 0xf03cc: 0xf03cc, + 0xf03cd: 0xf03cd, + 0xf03ce: 0xf03ce, + 0xf03cf: 0xf03cf, + 0xf03d0: 0xf03d0, + 0xf03d1: 0xf03d1, + 0xf03d2: 0xf03d2, + 0xf03d3: 0xf03d3, + 0xf03d4: 0xf03d4, + 0xf03d5: 0xf03d5, + 0xf03d6: 0xf03d6, + 0xf03d7: 0xf03d7, + 0xf03d8: 0xf03d8, + 0xf03d9: 0xf03d9, + 0xf03da: 0xf03da, + 0xf03db: 0xf03db, + 0xf03dc: 0xf03dc, + 0xf03dd: 0xf03dd, + 0xf03de: 0xf03de, + 0xf03df: 0xf03df, + 0xf03e0: 0xf03e0, + 0xf03e1: 0xf03e1, + 0xf03e2: 0xf03e2, + 0xf03e3: 0xf03e3, + 0xf03e4: 0xf03e4, + 0xf03e5: 0xf03e5, + 0xf03e6: 0xf03e6, + 0xf03e7: 0xf03e7, + 0xf03e8: 0xf03e8, + 0xf03e9: 0xf03e9, + 0xf03ea: 0xf03ea, + 0xf03eb: 0xf03eb, + 0xf03ec: 0xf03ec, + 0xf03ed: 0xf03ed, + 0xf03ee: 0xf03ee, + 0xf03ef: 0xf03ef, + 0xf03f0: 0xf03f0, + 0xf03f1: 0xf03f1, + 0xf03f2: 0xf03f2, + 0xf03f3: 0xf03f3, + 0xf03f4: 0xf03f4, + 0xf03f5: 0xf03f5, + 0xf03f6: 0xf03f6, + 0xf03f7: 0xf03f7, + 0xf03f8: 0xf03f8, + 0xf03f9: 0xf03f9, + 0xf03fa: 0xf03fa, + 0xf03fb: 0xf03fb, + 0xf03fc: 0xf03fc, + 0xf03fd: 0xf03fd, + 0xf03fe: 0xf03fe, + 0xf03ff: 0xf03ff, + 0xf0400: 0xf0400, + 0xf0401: 0xf0401, + 0xf0402: 0xf0402, + 0xf0403: 0xf0403, + 0xf0404: 0xf0404, + 0xf0405: 0xf0405, + 0xf0406: 0xf0406, + 0xf0407: 0xf0407, + 0xf0408: 0xf0408, + 0xf0409: 0xf0409, + 0xf040a: 0xf040a, + 0xf040b: 0xf040b, + 0xf040c: 0xf040c, + 0xf040d: 0xf040d, + 0xf040e: 0xf040e, + 0xf040f: 0xf040f, + 0xf0410: 0xf0410, + 0xf0411: 0xf0411, + 0xf0412: 0xf0412, + 0xf0413: 0xf0413, + 0xf0414: 0xf0414, + 0xf0415: 0xf0415, + 0xf0416: 0xf0416, + 0xf0417: 0xf0417, + 0xf0418: 0xf0418, + 0xf0419: 0xf0419, + 0xf041a: 0xf041a, + 0xf041b: 0xf041b, + 0xf041c: 0xf041c, + 0xf041d: 0xf041d, + 0xf041e: 0xf041e, + 0xf041f: 0xf041f, + 0xf0420: 0xf0420, + 0xf0421: 0xf0421, + 0xf0422: 0xf0422, + 0xf0423: 0xf0423, + 0xf0424: 0xf0424, + 0xf0425: 0xf0425, + 0xf0426: 0xf0426, + 0xf0427: 0xf0427, + 0xf0428: 0xf0428, + 0xf0429: 0xf0429, + 0xf042a: 0xf042a, + 0xf042b: 0xf042b, + 0xf042c: 0xf042c, + 0xf042d: 0xf042d, + 0xf042e: 0xf042e, + 0xf042f: 0xf042f, + 0xf0430: 0xf0430, + 0xf0431: 0xf0431, + 0xf0432: 0xf0432, + 0xf0433: 0xf0433, + 0xf0434: 0xf0434, + 0xf0435: 0xf0435, + 0xf0436: 0xf0436, + 0xf0437: 0xf0437, + 0xf0438: 0xf0438, + 0xf0439: 0xf0439, + 0xf043a: 0xf043a, + 0xf043b: 0xf043b, + 0xf043c: 0xf043c, + 0xf043d: 0xf043d, + 0xf043e: 0xf043e, + 0xf043f: 0xf043f, + 0xf0440: 0xf0440, + 0xf0441: 0xf0441, + 0xf0442: 0xf0442, + 0xf0443: 0xf0443, + 0xf0444: 0xf0444, + 0xf0445: 0xf0445, + 0xf0446: 0xf0446, + 0xf0447: 0xf0447, + 0xf0448: 0xf0448, + 0xf0449: 0xf0449, + 0xf044a: 0xf044a, + 0xf044b: 0xf044b, + 0xf044c: 0xf044c, + 0xf044d: 0xf044d, + 0xf044e: 0xf044e, + 0xf044f: 0xf044f, + 0xf0450: 0xf0450, + 0xf0451: 0xf0451, + 0xf0452: 0xf0452, + 0xf0453: 0xf0453, + 0xf0454: 0xf0454, + 0xf0455: 0xf0455, + 0xf0456: 0xf0456, + 0xf0457: 0xf0457, + 0xf0458: 0xf0458, + 0xf0459: 0xf0459, + 0xf045a: 0xf045a, + 0xf045b: 0xf045b, + 0xf045c: 0xf045c, + 0xf045d: 0xf045d, + 0xf045e: 0xf045e, + 0xf045f: 0xf045f, + 0xf0460: 0xf0460, + 0xf0461: 0xf0461, + 0xf0462: 0xf0462, + 0xf0463: 0xf0463, + 0xf0464: 0xf0464, + 0xf0465: 0xf0465, + 0xf0466: 0xf0466, + 0xf0467: 0xf0467, + 0xf0468: 0xf0468, + 0xf0469: 0xf0469, + 0xf046a: 0xf046a, + 0xf046b: 0xf046b, + 0xf046c: 0xf046c, + 0xf046d: 0xf046d, + 0xf046e: 0xf046e, + 0xf046f: 0xf046f, + 0xf0470: 0xf0470, + 0xf0471: 0xf0471, + 0xf0472: 0xf0472, + 0xf0473: 0xf0473, + 0xf0474: 0xf0474, + 0xf0475: 0xf0475, + 0xf0476: 0xf0476, + 0xf0477: 0xf0477, + 0xf0478: 0xf0478, + 0xf0479: 0xf0479, + 0xf047a: 0xf047a, + 0xf047b: 0xf047b, + 0xf047c: 0xf047c, + 0xf047d: 0xf047d, + 0xf047e: 0xf047e, + 0xf047f: 0xf047f, + 0xf0480: 0xf0480, + 0xf0481: 0xf0481, + 0xf0482: 0xf0482, + 0xf0483: 0xf0483, + 0xf0484: 0xf0484, + 0xf0485: 0xf0485, + 0xf0486: 0xf0486, + 0xf0487: 0xf0487, + 0xf0488: 0xf0488, + 0xf0489: 0xf0489, + 0xf048a: 0xf048a, + 0xf048b: 0xf048b, + 0xf048c: 0xf048c, + 0xf048d: 0xf048d, + 0xf048e: 0xf048e, + 0xf048f: 0xf048f, + 0xf0490: 0xf0490, + 0xf0491: 0xf0491, + 0xf0492: 0xf0492, + 0xf0493: 0xf0493, + 0xf0494: 0xf0494, + 0xf0495: 0xf0495, + 0xf0496: 0xf0496, + 0xf0497: 0xf0497, + 0xf0498: 0xf0498, + 0xf0499: 0xf0499, + 0xf049a: 0xf049a, + 0xf049b: 0xf049b, + 0xf049c: 0xf049c, + 0xf049d: 0xf049d, + 0xf049e: 0xf049e, + 0xf049f: 0xf049f, + 0xf04a0: 0xf04a0, + 0xf04a1: 0xf04a1, + 0xf04a2: 0xf04a2, + 0xf04a3: 0xf04a3, + 0xf04a4: 0xf04a4, + 0xf04a5: 0xf04a5, + 0xf04a6: 0xf04a6, + 0xf04a7: 0xf04a7, + 0xf04a8: 0xf04a8, + 0xf04a9: 0xf04a9, + 0xf04aa: 0xf04aa, + 0xf04ab: 0xf04ab, + 0xf04ac: 0xf04ac, + 0xf04ad: 0xf04ad, + 0xf04ae: 0xf04ae, + 0xf04af: 0xf04af, + 0xf04b0: 0xf04b0, + 0xf04b1: 0xf04b1, + 0xf04b2: 0xf04b2, + 0xf04b3: 0xf04b3, + 0xf04b4: 0xf04b4, + 0xf04b5: 0xf04b5, + 0xf04b6: 0xf04b6, + 0xf04b7: 0xf04b7, + 0xf04b8: 0xf04b8, + 0xf04b9: 0xf04b9, + 0xf04ba: 0xf04ba, + 0xf04bb: 0xf04bb, + 0xf04bc: 0xf04bc, + 0xf04bd: 0xf04bd, + 0xf04be: 0xf04be, + 0xf04bf: 0xf04bf, + 0xf04c0: 0xf04c0, + 0xf04c1: 0xf04c1, + 0xf04c2: 0xf04c2, + 0xf04c3: 0xf04c3, + 0xf04c4: 0xf04c4, + 0xf04c5: 0xf04c5, + 0xf04c6: 0xf04c6, + 0xf04c7: 0xf04c7, + 0xf04c8: 0xf04c8, + 0xf04c9: 0xf04c9, + 0xf04ca: 0xf04ca, + 0xf04cb: 0xf04cb, + 0xf04cc: 0xf04cc, + 0xf04cd: 0xf04cd, + 0xf04ce: 0xf04ce, + 0xf04cf: 0xf04cf, + 0xf04d0: 0xf04d0, + 0xf04d1: 0xf04d1, + 0xf04d2: 0xf04d2, + 0xf04d3: 0xf04d3, + 0xf04d4: 0xf04d4, + 0xf04d5: 0xf04d5, + 0xf04d6: 0xf04d6, + 0xf04d7: 0xf04d7, + 0xf04d8: 0xf04d8, + 0xf04d9: 0xf04d9, + 0xf04da: 0xf04da, + 0xf04db: 0xf04db, + 0xf04dc: 0xf04dc, + 0xf04dd: 0xf04dd, + 0xf04de: 0xf04de, + 0xf04df: 0xf04df, + 0xf04e0: 0xf04e0, + 0xf04e1: 0xf04e1, + 0xf04e2: 0xf04e2, + 0xf04e3: 0xf04e3, + 0xf04e4: 0xf04e4, + 0xf04e5: 0xf04e5, + 0xf04e6: 0xf04e6, + 0xf04e7: 0xf04e7, + 0xf04e8: 0xf04e8, + 0xf04e9: 0xf04e9, + 0xf04ea: 0xf04ea, + 0xf04eb: 0xf04eb, + 0xf04ec: 0xf04ec, + 0xf04ed: 0xf04ed, + 0xf04ee: 0xf04ee, + 0xf04ef: 0xf04ef, + 0xf04f0: 0xf04f0, + 0xf04f1: 0xf04f1, + 0xf04f2: 0xf04f2, + 0xf04f3: 0xf04f3, + 0xf04f4: 0xf04f4, + 0xf04f5: 0xf04f5, + 0xf04f6: 0xf04f6, + 0xf04f7: 0xf04f7, + 0xf04f8: 0xf04f8, + 0xf04f9: 0xf04f9, + 0xf04fa: 0xf04fa, + 0xf04fb: 0xf04fb, + 0xf04fc: 0xf04fc, + 0xf04fd: 0xf04fd, + 0xf04fe: 0xf04fe, + 0xf04ff: 0xf04ff, + 0xf0500: 0xf0500, + 0xf0501: 0xf0501, + 0xf0502: 0xf0502, + 0xf0503: 0xf0503, + 0xf0504: 0xf0504, + 0xf0505: 0xf0505, + 0xf0506: 0xf0506, + 0xf0507: 0xf0507, + 0xf0508: 0xf0508, + 0xf0509: 0xf0509, + 0xf050a: 0xf050a, + 0xf050b: 0xf050b, + 0xf050c: 0xf050c, + 0xf050d: 0xf050d, + 0xf050e: 0xf050e, + 0xf050f: 0xf050f, + 0xf0510: 0xf0510, + 0xf0511: 0xf0511, + 0xf0512: 0xf0512, + 0xf0513: 0xf0513, + 0xf0514: 0xf0514, + 0xf0515: 0xf0515, + 0xf0516: 0xf0516, + 0xf0517: 0xf0517, + 0xf0518: 0xf0518, + 0xf0519: 0xf0519, + 0xf051a: 0xf051a, + 0xf051b: 0xf051b, + 0xf051c: 0xf051c, + 0xf051d: 0xf051d, + 0xf051e: 0xf051e, + 0xf051f: 0xf051f, + 0xf0520: 0xf0520, + 0xf0521: 0xf0521, + 0xf0522: 0xf0522, + 0xf0523: 0xf0523, + 0xf0524: 0xf0524, + 0xf0525: 0xf0525, + 0xf0526: 0xf0526, + 0xf0527: 0xf0527, + 0xf0528: 0xf0528, + 0xf0529: 0xf0529, + 0xf052a: 0xf052a, + 0xf052b: 0xf052b, + 0xf052c: 0xf052c, + 0xf052d: 0xf052d, + 0xf052e: 0xf052e, + 0xf052f: 0xf052f, + 0xf0530: 0xf0530, + 0xf0531: 0xf0531, + 0xf0532: 0xf0532, + 0xf0533: 0xf0533, + 0xf0534: 0xf0534, + 0xf0535: 0xf0535, + 0xf0536: 0xf0536, + 0xf0537: 0xf0537, + 0xf0538: 0xf0538, + 0xf0539: 0xf0539, + 0xf053a: 0xf053a, + 0xf053b: 0xf053b, + 0xf053c: 0xf053c, + 0xf053d: 0xf053d, + 0xf053e: 0xf053e, + 0xf053f: 0xf053f, + 0xf0540: 0xf0540, + 0xf0541: 0xf0541, + 0xf0542: 0xf0542, + 0xf0543: 0xf0543, + 0xf0544: 0xf0544, + 0xf0545: 0xf0545, + 0xf0546: 0xf0546, + 0xf0547: 0xf0547, + 0xf0548: 0xf0548, + 0xf0549: 0xf0549, + 0xf054a: 0xf054a, + 0xf054b: 0xf054b, + 0xf054c: 0xf054c, + 0xf054d: 0xf054d, + 0xf054e: 0xf054e, + 0xf054f: 0xf054f, + 0xf0550: 0xf0550, + 0xf0551: 0xf0551, + 0xf0552: 0xf0552, + 0xf0553: 0xf0553, + 0xf0554: 0xf0554, + 0xf0555: 0xf0555, + 0xf0556: 0xf0556, + 0xf0557: 0xf0557, + 0xf0558: 0xf0558, + 0xf0559: 0xf0559, + 0xf055a: 0xf055a, + 0xf055b: 0xf055b, + 0xf055c: 0xf055c, + 0xf055d: 0xf055d, + 0xf055e: 0xf055e, + 0xf055f: 0xf055f, + 0xf0560: 0xf0560, + 0xf0561: 0xf0561, + 0xf0562: 0xf0562, + 0xf0563: 0xf0563, + 0xf0564: 0xf0564, + 0xf0565: 0xf0565, + 0xf0566: 0xf0566, + 0xf0567: 0xf0567, + 0xf0568: 0xf0568, + 0xf0569: 0xf0569, + 0xf056a: 0xf056a, + 0xf056b: 0xf056b, + 0xf056c: 0xf056c, + 0xf056d: 0xf056d, + 0xf056e: 0xf056e, + 0xf056f: 0xf056f, + 0xf0570: 0xf0570, + 0xf0571: 0xf0571, + 0xf0572: 0xf0572, + 0xf0573: 0xf0573, + 0xf0574: 0xf0574, + 0xf0575: 0xf0575, + 0xf0576: 0xf0576, + 0xf0577: 0xf0577, + 0xf0578: 0xf0578, + 0xf0579: 0xf0579, + 0xf057a: 0xf057a, + 0xf057b: 0xf057b, + 0xf057c: 0xf057c, + 0xf057d: 0xf057d, + 0xf057e: 0xf057e, + 0xf057f: 0xf057f, + 0xf0580: 0xf0580, + 0xf0581: 0xf0581, + 0xf0582: 0xf0582, + 0xf0583: 0xf0583, + 0xf0584: 0xf0584, + 0xf0585: 0xf0585, + 0xf0586: 0xf0586, + 0xf0587: 0xf0587, + 0xf0588: 0xf0588, + 0xf0589: 0xf0589, + 0xf058a: 0xf058a, + 0xf058b: 0xf058b, + 0xf058c: 0xf058c, + 0xf058d: 0xf058d, + 0xf058e: 0xf058e, + 0xf058f: 0xf058f, + 0xf0590: 0xf0590, + 0xf0591: 0xf0591, + 0xf0592: 0xf0592, + 0xf0593: 0xf0593, + 0xf0594: 0xf0594, + 0xf0595: 0xf0595, + 0xf0596: 0xf0596, + 0xf0597: 0xf0597, + 0xf0598: 0xf0598, + 0xf0599: 0xf0599, + 0xf059a: 0xf059a, + 0xf059b: 0xf059b, + 0xf059c: 0xf059c, + 0xf059d: 0xf059d, + 0xf059e: 0xf059e, + 0xf059f: 0xf059f, + 0xf05a0: 0xf05a0, + 0xf05a1: 0xf05a1, + 0xf05a2: 0xf05a2, + 0xf05a3: 0xf05a3, + 0xf05a4: 0xf05a4, + 0xf05a5: 0xf05a5, + 0xf05a6: 0xf05a6, + 0xf05a7: 0xf05a7, + 0xf05a8: 0xf05a8, + 0xf05a9: 0xf05a9, + 0xf05aa: 0xf05aa, + 0xf05ab: 0xf05ab, + 0xf05ac: 0xf05ac, + 0xf05ad: 0xf05ad, + 0xf05ae: 0xf05ae, + 0xf05af: 0xf05af, + 0xf05b0: 0xf05b0, + 0xf05b1: 0xf05b1, + 0xf05b2: 0xf05b2, + 0xf05b3: 0xf05b3, + 0xf05b4: 0xf05b4, + 0xf05b5: 0xf05b5, + 0xf05b6: 0xf05b6, + 0xf05b7: 0xf05b7, + 0xf05b8: 0xf05b8, + 0xf05b9: 0xf05b9, + 0xf05ba: 0xf05ba, + 0xf05bb: 0xf05bb, + 0xf05bc: 0xf05bc, + 0xf05bd: 0xf05bd, + 0xf05be: 0xf05be, + 0xf05bf: 0xf05bf, + 0xf05c0: 0xf05c0, + 0xf05c1: 0xf05c1, + 0xf05c2: 0xf05c2, + 0xf05c3: 0xf05c3, + 0xf05c4: 0xf05c4, + 0xf05c5: 0xf05c5, + 0xf05c6: 0xf05c6, + 0xf05c7: 0xf05c7, + 0xf05c8: 0xf05c8, + 0xf05c9: 0xf05c9, + 0xf05ca: 0xf05ca, + 0xf05cb: 0xf05cb, + 0xf05cc: 0xf05cc, + 0xf05cd: 0xf05cd, + 0xf05ce: 0xf05ce, + 0xf05cf: 0xf05cf, + 0xf05d0: 0xf05d0, + 0xf05d1: 0xf05d1, + 0xf05d2: 0xf05d2, + 0xf05d3: 0xf05d3, + 0xf05d4: 0xf05d4, + 0xf05d5: 0xf05d5, + 0xf05d6: 0xf05d6, + 0xf05d7: 0xf05d7, + 0xf05d8: 0xf05d8, + 0xf05d9: 0xf05d9, + 0xf05da: 0xf05da, + 0xf05db: 0xf05db, + 0xf05dc: 0xf05dc, + 0xf05dd: 0xf05dd, + 0xf05de: 0xf05de, + 0xf05df: 0xf05df, + 0xf05e0: 0xf05e0, + 0xf05e1: 0xf05e1, + 0xf05e2: 0xf05e2, + 0xf05e3: 0xf05e3, + 0xf05e4: 0xf05e4, + 0xf05e5: 0xf05e5, + 0xf05e6: 0xf05e6, + 0xf05e7: 0xf05e7, + 0xf05e8: 0xf05e8, + 0xf05e9: 0xf05e9, + 0xf05ea: 0xf05ea, + 0xf05eb: 0xf05eb, + 0xf05ec: 0xf05ec, + 0xf05ed: 0xf05ed, + 0xf05ee: 0xf05ee, + 0xf05ef: 0xf05ef, + 0xf05f0: 0xf05f0, + 0xf05f1: 0xf05f1, + 0xf05f2: 0xf05f2, + 0xf05f3: 0xf05f3, + 0xf05f4: 0xf05f4, + 0xf05f5: 0xf05f5, + 0xf05f6: 0xf05f6, + 0xf05f7: 0xf05f7, + 0xf05f8: 0xf05f8, + 0xf05f9: 0xf05f9, + 0xf05fa: 0xf05fa, + 0xf05fb: 0xf05fb, + 0xf05fc: 0xf05fc, + 0xf05fd: 0xf05fd, + 0xf05fe: 0xf05fe, + 0xf05ff: 0xf05ff, + 0xf0600: 0xf0600, + 0xf0601: 0xf0601, + 0xf0602: 0xf0602, + 0xf0603: 0xf0603, + 0xf0604: 0xf0604, + 0xf0605: 0xf0605, + 0xf0606: 0xf0606, + 0xf0607: 0xf0607, + 0xf0608: 0xf0608, + 0xf0609: 0xf0609, + 0xf060a: 0xf060a, + 0xf060b: 0xf060b, + 0xf060c: 0xf060c, + 0xf060d: 0xf060d, + 0xf060e: 0xf060e, + 0xf060f: 0xf060f, + 0xf0610: 0xf0610, + 0xf0611: 0xf0611, + 0xf0612: 0xf0612, + 0xf0613: 0xf0613, + 0xf0614: 0xf0614, + 0xf0615: 0xf0615, + 0xf0616: 0xf0616, + 0xf0617: 0xf0617, + 0xf0618: 0xf0618, + 0xf0619: 0xf0619, + 0xf061a: 0xf061a, + 0xf061b: 0xf061b, + 0xf061c: 0xf061c, + 0xf061d: 0xf061d, + 0xf061e: 0xf061e, + 0xf061f: 0xf061f, + 0xf0620: 0xf0620, + 0xf0621: 0xf0621, + 0xf0622: 0xf0622, + 0xf0623: 0xf0623, + 0xf0624: 0xf0624, + 0xf0625: 0xf0625, + 0xf0626: 0xf0626, + 0xf0627: 0xf0627, + 0xf0628: 0xf0628, + 0xf0629: 0xf0629, + 0xf062a: 0xf062a, + 0xf062b: 0xf062b, + 0xf062c: 0xf062c, + 0xf062d: 0xf062d, + 0xf062e: 0xf062e, + 0xf062f: 0xf062f, + 0xf0630: 0xf0630, + 0xf0631: 0xf0631, + 0xf0632: 0xf0632, + 0xf0633: 0xf0633, + 0xf0634: 0xf0634, + 0xf0635: 0xf0635, + 0xf0636: 0xf0636, + 0xf0637: 0xf0637, + 0xf0638: 0xf0638, + 0xf0639: 0xf0639, + 0xf063a: 0xf063a, + 0xf063b: 0xf063b, + 0xf063c: 0xf063c, + 0xf063d: 0xf063d, + 0xf063e: 0xf063e, + 0xf063f: 0xf063f, + 0xf0640: 0xf0640, + 0xf0641: 0xf0641, + 0xf0642: 0xf0642, + 0xf0643: 0xf0643, + 0xf0644: 0xf0644, + 0xf0645: 0xf0645, + 0xf0646: 0xf0646, + 0xf0647: 0xf0647, + 0xf0648: 0xf0648, + 0xf0649: 0xf0649, + 0xf064a: 0xf064a, + 0xf064b: 0xf064b, + 0xf064c: 0xf064c, + 0xf064d: 0xf064d, + 0xf064e: 0xf064e, + 0xf064f: 0xf064f, + 0xf0650: 0xf0650, + 0xf0651: 0xf0651, + 0xf0652: 0xf0652, + 0xf0653: 0xf0653, + 0xf0654: 0xf0654, + 0xf0655: 0xf0655, + 0xf0656: 0xf0656, + 0xf0657: 0xf0657, + 0xf0658: 0xf0658, + 0xf0659: 0xf0659, + 0xf065a: 0xf065a, + 0xf065b: 0xf065b, + 0xf065c: 0xf065c, + 0xf065d: 0xf065d, + 0xf065e: 0xf065e, + 0xf065f: 0xf065f, + 0xf0660: 0xf0660, + 0xf0661: 0xf0661, + 0xf0662: 0xf0662, + 0xf0663: 0xf0663, + 0xf0664: 0xf0664, + 0xf0665: 0xf0665, + 0xf0666: 0xf0666, + 0xf0667: 0xf0667, + 0xf0668: 0xf0668, + 0xf0669: 0xf0669, + 0xf066a: 0xf066a, + 0xf066b: 0xf066b, + 0xf066c: 0xf066c, + 0xf066d: 0xf066d, + 0xf066e: 0xf066e, + 0xf066f: 0xf066f, + 0xf0670: 0xf0670, + 0xf0671: 0xf0671, + 0xf0672: 0xf0672, + 0xf0673: 0xf0673, + 0xf0674: 0xf0674, + 0xf0675: 0xf0675, + 0xf0676: 0xf0676, + 0xf0677: 0xf0677, + 0xf0678: 0xf0678, + 0xf0679: 0xf0679, + 0xf067a: 0xf067a, + 0xf067b: 0xf067b, + 0xf067c: 0xf067c, + 0xf067d: 0xf067d, + 0xf067e: 0xf067e, + 0xf067f: 0xf067f, + 0xf0680: 0xf0680, + 0xf0681: 0xf0681, + 0xf0682: 0xf0682, + 0xf0683: 0xf0683, + 0xf0684: 0xf0684, + 0xf0685: 0xf0685, + 0xf0686: 0xf0686, + 0xf0687: 0xf0687, + 0xf0688: 0xf0688, + 0xf0689: 0xf0689, + 0xf068a: 0xf068a, + 0xf068b: 0xf068b, + 0xf068c: 0xf068c, + 0xf068d: 0xf068d, + 0xf068e: 0xf068e, + 0xf068f: 0xf068f, + 0xf0690: 0xf0690, + 0xf0691: 0xf0691, + 0xf0692: 0xf0692, + 0xf0693: 0xf0693, + 0xf0694: 0xf0694, + 0xf0695: 0xf0695, + 0xf0696: 0xf0696, + 0xf0697: 0xf0697, + 0xf0698: 0xf0698, + 0xf0699: 0xf0699, + 0xf069a: 0xf069a, + 0xf069b: 0xf069b, + 0xf069c: 0xf069c, + 0xf069d: 0xf069d, + 0xf069e: 0xf069e, + 0xf069f: 0xf069f, + 0xf06a0: 0xf06a0, + 0xf06a1: 0xf06a1, + 0xf06a2: 0xf06a2, + 0xf06a3: 0xf06a3, + 0xf06a4: 0xf06a4, + 0xf06a5: 0xf06a5, + 0xf06a6: 0xf06a6, + 0xf06a7: 0xf06a7, + 0xf06a8: 0xf06a8, + 0xf06a9: 0xf06a9, + 0xf06aa: 0xf06aa, + 0xf06ab: 0xf06ab, + 0xf06ac: 0xf06ac, + 0xf06ad: 0xf06ad, + 0xf06ae: 0xf06ae, + 0xf06af: 0xf06af, + 0xf06b0: 0xf06b0, + 0xf06b1: 0xf06b1, + 0xf06b2: 0xf06b2, + 0xf06b3: 0xf06b3, + 0xf06b4: 0xf06b4, + 0xf06b5: 0xf06b5, + 0xf06b6: 0xf06b6, + 0xf06b7: 0xf06b7, + 0xf06b8: 0xf06b8, + 0xf06b9: 0xf06b9, + 0xf06ba: 0xf06ba, + 0xf06bb: 0xf06bb, + 0xf06bc: 0xf06bc, + 0xf06bd: 0xf06bd, + 0xf06be: 0xf06be, + 0xf06bf: 0xf06bf, + 0xf06c0: 0xf06c0, + 0xf06c1: 0xf06c1, + 0xf06c2: 0xf06c2, + 0xf06c3: 0xf06c3, + 0xf06c4: 0xf06c4, + 0xf06c5: 0xf06c5, + 0xf06c6: 0xf06c6, + 0xf06c7: 0xf06c7, + 0xf06c8: 0xf06c8, + 0xf06c9: 0xf06c9, + 0xf06ca: 0xf06ca, + 0xf06cb: 0xf06cb, + 0xf06cc: 0xf06cc, + 0xf06cd: 0xf06cd, + 0xf06ce: 0xf06ce, + 0xf06cf: 0xf06cf, + 0xf06d0: 0xf06d0, + 0xf06d1: 0xf06d1, + 0xf06d2: 0xf06d2, + 0xf06d3: 0xf06d3, + 0xf06d4: 0xf06d4, + 0xf06d5: 0xf06d5, + 0xf06d6: 0xf06d6, + 0xf06d7: 0xf06d7, + 0xf06d8: 0xf06d8, + 0xf06d9: 0xf06d9, + 0xf06da: 0xf06da, + 0xf06db: 0xf06db, + 0xf06dc: 0xf06dc, + 0xf06dd: 0xf06dd, + 0xf06de: 0xf06de, + 0xf06df: 0xf06df, + 0xf06e0: 0xf06e0, + 0xf06e1: 0xf06e1, + 0xf06e2: 0xf06e2, + 0xf06e3: 0xf06e3, + 0xf06e4: 0xf06e4, + 0xf06e5: 0xf06e5, + 0xf06e6: 0xf06e6, + 0xf06e7: 0xf06e7, + 0xf06e8: 0xf06e8, + 0xf06e9: 0xf06e9, + 0xf06ea: 0xf06ea, + 0xf06eb: 0xf06eb, + 0xf06ec: 0xf06ec, + 0xf06ed: 0xf06ed, + 0xf06ee: 0xf06ee, + 0xf06ef: 0xf06ef, + 0xf06f0: 0xf06f0, + 0xf06f1: 0xf06f1, + 0xf06f2: 0xf06f2, + 0xf06f3: 0xf06f3, + 0xf06f4: 0xf06f4, + 0xf06f5: 0xf06f5, + 0xf06f6: 0xf06f6, + 0xf06f7: 0xf06f7, + 0xf06f8: 0xf06f8, + 0xf06f9: 0xf06f9, + 0xf06fa: 0xf06fa, + 0xf06fb: 0xf06fb, + 0xf06fc: 0xf06fc, + 0xf06fd: 0xf06fd, + 0xf06fe: 0xf06fe, + 0xf06ff: 0xf06ff, + 0xf0700: 0xf0700, + 0xf0701: 0xf0701, + 0xf0702: 0xf0702, + 0xf0703: 0xf0703, + 0xf0704: 0xf0704, + 0xf0705: 0xf0705, + 0xf0706: 0xf0706, + 0xf0707: 0xf0707, + 0xf0708: 0xf0708, + 0xf0709: 0xf0709, + 0xf070a: 0xf070a, + 0xf070b: 0xf070b, + 0xf070c: 0xf070c, + 0xf070d: 0xf070d, + 0xf070e: 0xf070e, + 0xf070f: 0xf070f, + 0xf0710: 0xf0710, + 0xf0711: 0xf0711, + 0xf0712: 0xf0712, + 0xf0713: 0xf0713, + 0xf0714: 0xf0714, + 0xf0715: 0xf0715, + 0xf0716: 0xf0716, + 0xf0717: 0xf0717, + 0xf0718: 0xf0718, + 0xf0719: 0xf0719, + 0xf071a: 0xf071a, + 0xf071b: 0xf071b, + 0xf071c: 0xf071c, + 0xf071d: 0xf071d, + 0xf071e: 0xf071e, + 0xf071f: 0xf071f, + 0xf0720: 0xf0720, + 0xf0721: 0xf0721, + 0xf0722: 0xf0722, + 0xf0723: 0xf0723, + 0xf0724: 0xf0724, + 0xf0725: 0xf0725, + 0xf0726: 0xf0726, + 0xf0727: 0xf0727, + 0xf0728: 0xf0728, + 0xf0729: 0xf0729, + 0xf072a: 0xf072a, + 0xf072b: 0xf072b, + 0xf072c: 0xf072c, + 0xf072d: 0xf072d, + 0xf072e: 0xf072e, + 0xf072f: 0xf072f, + 0xf0730: 0xf0730, + 0xf0731: 0xf0731, + 0xf0732: 0xf0732, + 0xf0733: 0xf0733, + 0xf0734: 0xf0734, + 0xf0735: 0xf0735, + 0xf0736: 0xf0736, + 0xf0737: 0xf0737, + 0xf0738: 0xf0738, + 0xf0739: 0xf0739, + 0xf073a: 0xf073a, + 0xf073b: 0xf073b, + 0xf073c: 0xf073c, + 0xf073d: 0xf073d, + 0xf073e: 0xf073e, + 0xf073f: 0xf073f, + 0xf0740: 0xf0740, + 0xf0741: 0xf0741, + 0xf0742: 0xf0742, + 0xf0743: 0xf0743, + 0xf0744: 0xf0744, + 0xf0745: 0xf0745, + 0xf0746: 0xf0746, + 0xf0747: 0xf0747, + 0xf0748: 0xf0748, + 0xf0749: 0xf0749, + 0xf074a: 0xf074a, + 0xf074b: 0xf074b, + 0xf074c: 0xf074c, + 0xf074d: 0xf074d, + 0xf074e: 0xf074e, + 0xf074f: 0xf074f, + 0xf0750: 0xf0750, + 0xf0751: 0xf0751, + 0xf0752: 0xf0752, + 0xf0753: 0xf0753, + 0xf0754: 0xf0754, + 0xf0755: 0xf0755, + 0xf0756: 0xf0756, + 0xf0757: 0xf0757, + 0xf0758: 0xf0758, + 0xf0759: 0xf0759, + 0xf075a: 0xf075a, + 0xf075b: 0xf075b, + 0xf075c: 0xf075c, + 0xf075d: 0xf075d, + 0xf075e: 0xf075e, + 0xf075f: 0xf075f, + 0xf0760: 0xf0760, + 0xf0761: 0xf0761, + 0xf0762: 0xf0762, + 0xf0763: 0xf0763, + 0xf0764: 0xf0764, + 0xf0765: 0xf0765, + 0xf0766: 0xf0766, + 0xf0767: 0xf0767, + 0xf0768: 0xf0768, + 0xf0769: 0xf0769, + 0xf076a: 0xf076a, + 0xf076b: 0xf076b, + 0xf076c: 0xf076c, + 0xf076d: 0xf076d, + 0xf076e: 0xf076e, + 0xf076f: 0xf076f, + 0xf0770: 0xf0770, + 0xf0771: 0xf0771, + 0xf0772: 0xf0772, + 0xf0773: 0xf0773, + 0xf0774: 0xf0774, + 0xf0775: 0xf0775, + 0xf0776: 0xf0776, + 0xf0777: 0xf0777, + 0xf0778: 0xf0778, + 0xf0779: 0xf0779, + 0xf077a: 0xf077a, + 0xf077b: 0xf077b, + 0xf077c: 0xf077c, + 0xf077d: 0xf077d, + 0xf077e: 0xf077e, + 0xf077f: 0xf077f, + 0xf0780: 0xf0780, + 0xf0781: 0xf0781, + 0xf0782: 0xf0782, + 0xf0783: 0xf0783, + 0xf0784: 0xf0784, + 0xf0785: 0xf0785, + 0xf0786: 0xf0786, + 0xf0787: 0xf0787, + 0xf0788: 0xf0788, + 0xf0789: 0xf0789, + 0xf078a: 0xf078a, + 0xf078b: 0xf078b, + 0xf078c: 0xf078c, + 0xf078d: 0xf078d, + 0xf078e: 0xf078e, + 0xf078f: 0xf078f, + 0xf0790: 0xf0790, + 0xf0791: 0xf0791, + 0xf0792: 0xf0792, + 0xf0793: 0xf0793, + 0xf0794: 0xf0794, + 0xf0795: 0xf0795, + 0xf0796: 0xf0796, + 0xf0797: 0xf0797, + 0xf0798: 0xf0798, + 0xf0799: 0xf0799, + 0xf079a: 0xf079a, + 0xf079b: 0xf079b, + 0xf079c: 0xf079c, + 0xf079d: 0xf079d, + 0xf079e: 0xf079e, + 0xf079f: 0xf079f, + 0xf07a0: 0xf07a0, + 0xf07a1: 0xf07a1, + 0xf07a2: 0xf07a2, + 0xf07a3: 0xf07a3, + 0xf07a4: 0xf07a4, + 0xf07a5: 0xf07a5, + 0xf07a6: 0xf07a6, + 0xf07a7: 0xf07a7, + 0xf07a8: 0xf07a8, + 0xf07a9: 0xf07a9, + 0xf07aa: 0xf07aa, + 0xf07ab: 0xf07ab, + 0xf07ac: 0xf07ac, + 0xf07ad: 0xf07ad, + 0xf07ae: 0xf07ae, + 0xf07af: 0xf07af, + 0xf07b0: 0xf07b0, + 0xf07b1: 0xf07b1, + 0xf07b2: 0xf07b2, + 0xf07b3: 0xf07b3, + 0xf07b4: 0xf07b4, + 0xf07b5: 0xf07b5, + 0xf07b6: 0xf07b6, + 0xf07b7: 0xf07b7, + 0xf07b8: 0xf07b8, + 0xf07b9: 0xf07b9, + 0xf07ba: 0xf07ba, + 0xf07bb: 0xf07bb, + 0xf07bc: 0xf07bc, + 0xf07bd: 0xf07bd, + 0xf07be: 0xf07be, + 0xf07bf: 0xf07bf, + 0xf07c0: 0xf07c0, + 0xf07c1: 0xf07c1, + 0xf07c2: 0xf07c2, + 0xf07c3: 0xf07c3, + 0xf07c4: 0xf07c4, + 0xf07c5: 0xf07c5, + 0xf07c6: 0xf07c6, + 0xf07c7: 0xf07c7, + 0xf07c8: 0xf07c8, + 0xf07c9: 0xf07c9, + 0xf07ca: 0xf07ca, + 0xf07cb: 0xf07cb, + 0xf07cc: 0xf07cc, + 0xf07cd: 0xf07cd, + 0xf07ce: 0xf07ce, + 0xf07cf: 0xf07cf, + 0xf07d0: 0xf07d0, + 0xf07d1: 0xf07d1, + 0xf07d2: 0xf07d2, + 0xf07d3: 0xf07d3, + 0xf07d4: 0xf07d4, + 0xf07d5: 0xf07d5, + 0xf07d6: 0xf07d6, + 0xf07d7: 0xf07d7, + 0xf07d8: 0xf07d8, + 0xf07d9: 0xf07d9, + 0xf07da: 0xf07da, + 0xf07db: 0xf07db, + 0xf07dc: 0xf07dc, + 0xf07dd: 0xf07dd, + 0xf07de: 0xf07de, + 0xf07df: 0xf07df, + 0xf07e0: 0xf07e0, + 0xf07e1: 0xf07e1, + 0xf07e2: 0xf07e2, + 0xf07e3: 0xf07e3, + 0xf07e4: 0xf07e4, + 0xf07e5: 0xf07e5, + 0xf07e6: 0xf07e6, + 0xf07e7: 0xf07e7, + 0xf07e8: 0xf07e8, + 0xf07e9: 0xf07e9, + 0xf07ea: 0xf07ea, + 0xf07eb: 0xf07eb, + 0xf07ec: 0xf07ec, + 0xf07ed: 0xf07ed, + 0xf07ee: 0xf07ee, + 0xf07ef: 0xf07ef, + 0xf07f0: 0xf07f0, + 0xf07f1: 0xf07f1, + 0xf07f2: 0xf07f2, + 0xf07f3: 0xf07f3, + 0xf07f4: 0xf07f4, + 0xf07f5: 0xf07f5, + 0xf07f6: 0xf07f6, + 0xf07f7: 0xf07f7, + 0xf07f8: 0xf07f8, + 0xf07f9: 0xf07f9, + 0xf07fa: 0xf07fa, + 0xf07fb: 0xf07fb, + 0xf07fc: 0xf07fc, + 0xf07fd: 0xf07fd, + 0xf07fe: 0xf07fe, + 0xf07ff: 0xf07ff, + 0xf0800: 0xf0800, + 0xf0801: 0xf0801, + 0xf0802: 0xf0802, + 0xf0803: 0xf0803, + 0xf0804: 0xf0804, + 0xf0805: 0xf0805, + 0xf0806: 0xf0806, + 0xf0807: 0xf0807, + 0xf0808: 0xf0808, + 0xf0809: 0xf0809, + 0xf080a: 0xf080a, + 0xf080b: 0xf080b, + 0xf080c: 0xf080c, + 0xf080d: 0xf080d, + 0xf080e: 0xf080e, + 0xf080f: 0xf080f, + 0xf0810: 0xf0810, + 0xf0811: 0xf0811, + 0xf0812: 0xf0812, + 0xf0813: 0xf0813, + 0xf0814: 0xf0814, + 0xf0815: 0xf0815, + 0xf0816: 0xf0816, + 0xf0817: 0xf0817, + 0xf0818: 0xf0818, + 0xf0819: 0xf0819, + 0xf081a: 0xf081a, + 0xf081b: 0xf081b, + 0xf081c: 0xf081c, + 0xf081d: 0xf081d, + 0xf081e: 0xf081e, + 0xf081f: 0xf081f, + 0xf0820: 0xf0820, + 0xf0821: 0xf0821, + 0xf0822: 0xf0822, + 0xf0823: 0xf0823, + 0xf0824: 0xf0824, + 0xf0825: 0xf0825, + 0xf0826: 0xf0826, + 0xf0827: 0xf0827, + 0xf0828: 0xf0828, + 0xf0829: 0xf0829, + 0xf082a: 0xf082a, + 0xf082b: 0xf082b, + 0xf082c: 0xf082c, + 0xf082d: 0xf082d, + 0xf082e: 0xf082e, + 0xf082f: 0xf082f, + 0xf0830: 0xf0830, + 0xf0831: 0xf0831, + 0xf0832: 0xf0832, + 0xf0833: 0xf0833, + 0xf0834: 0xf0834, + 0xf0835: 0xf0835, + 0xf0836: 0xf0836, + 0xf0837: 0xf0837, + 0xf0838: 0xf0838, + 0xf0839: 0xf0839, + 0xf083a: 0xf083a, + 0xf083b: 0xf083b, + 0xf083c: 0xf083c, + 0xf083d: 0xf083d, + 0xf083e: 0xf083e, + 0xf083f: 0xf083f, + 0xf0840: 0xf0840, + 0xf0841: 0xf0841, + 0xf0842: 0xf0842, + 0xf0843: 0xf0843, + 0xf0844: 0xf0844, + 0xf0845: 0xf0845, + 0xf0846: 0xf0846, + 0xf0847: 0xf0847, + 0xf0848: 0xf0848, + 0xf0849: 0xf0849, + 0xf084a: 0xf084a, + 0xf084b: 0xf084b, + 0xf084c: 0xf084c, + 0xf084d: 0xf084d, + 0xf084e: 0xf084e, + 0xf084f: 0xf084f, + 0xf0850: 0xf0850, + 0xf0851: 0xf0851, + 0xf0852: 0xf0852, + 0xf0853: 0xf0853, + 0xf0854: 0xf0854, + 0xf0855: 0xf0855, + 0xf0856: 0xf0856, + 0xf0857: 0xf0857, + 0xf0858: 0xf0858, + 0xf0859: 0xf0859, + 0xf085a: 0xf085a, + 0xf085b: 0xf085b, + 0xf085c: 0xf085c, + 0xf085d: 0xf085d, + 0xf085e: 0xf085e, + 0xf085f: 0xf085f, + 0xf0860: 0xf0860, + 0xf0861: 0xf0861, + 0xf0862: 0xf0862, + 0xf0863: 0xf0863, + 0xf0864: 0xf0864, + 0xf0865: 0xf0865, + 0xf0866: 0xf0866, + 0xf0867: 0xf0867, + 0xf0868: 0xf0868, + 0xf0869: 0xf0869, + 0xf086a: 0xf086a, + 0xf086b: 0xf086b, + 0xf086c: 0xf086c, + 0xf086d: 0xf086d, + 0xf086e: 0xf086e, + 0xf086f: 0xf086f, + 0xf0870: 0xf0870, + 0xf0871: 0xf0871, + 0xf0872: 0xf0872, + 0xf0873: 0xf0873, + 0xf0874: 0xf0874, + 0xf0875: 0xf0875, + 0xf0876: 0xf0876, + 0xf0877: 0xf0877, + 0xf0878: 0xf0878, + 0xf0879: 0xf0879, + 0xf087a: 0xf087a, + 0xf087b: 0xf087b, + 0xf087c: 0xf087c, + 0xf087d: 0xf087d, + 0xf087e: 0xf087e, + 0xf087f: 0xf087f, + 0xf0880: 0xf0880, + 0xf0881: 0xf0881, + 0xf0882: 0xf0882, + 0xf0883: 0xf0883, + 0xf0884: 0xf0884, + 0xf0885: 0xf0885, + 0xf0886: 0xf0886, + 0xf0887: 0xf0887, + 0xf0888: 0xf0888, + 0xf0889: 0xf0889, + 0xf088a: 0xf088a, + 0xf088b: 0xf088b, + 0xf088c: 0xf088c, + 0xf088d: 0xf088d, + 0xf088e: 0xf088e, + 0xf088f: 0xf088f, + 0xf0890: 0xf0890, + 0xf0891: 0xf0891, + 0xf0892: 0xf0892, + 0xf0893: 0xf0893, + 0xf0894: 0xf0894, + 0xf0895: 0xf0895, + 0xf0896: 0xf0896, + 0xf0897: 0xf0897, + 0xf0898: 0xf0898, + 0xf0899: 0xf0899, + 0xf089a: 0xf089a, + 0xf089b: 0xf089b, + 0xf089c: 0xf089c, + 0xf089d: 0xf089d, + 0xf089e: 0xf089e, + 0xf089f: 0xf089f, + 0xf08a0: 0xf08a0, + 0xf08a1: 0xf08a1, + 0xf08a2: 0xf08a2, + 0xf08a3: 0xf08a3, + 0xf08a4: 0xf08a4, + 0xf08a5: 0xf08a5, + 0xf08a6: 0xf08a6, + 0xf08a7: 0xf08a7, + 0xf08a8: 0xf08a8, + 0xf08a9: 0xf08a9, + 0xf08aa: 0xf08aa, + 0xf08ab: 0xf08ab, + 0xf08ac: 0xf08ac, + 0xf08ad: 0xf08ad, + 0xf08ae: 0xf08ae, + 0xf08af: 0xf08af, + 0xf08b0: 0xf08b0, + 0xf08b1: 0xf08b1, + 0xf08b2: 0xf08b2, + 0xf08b3: 0xf08b3, + 0xf08b4: 0xf08b4, + 0xf08b5: 0xf08b5, + 0xf08b6: 0xf08b6, + 0xf08b7: 0xf08b7, + 0xf08b8: 0xf08b8, + 0xf08b9: 0xf08b9, + 0xf08ba: 0xf08ba, + 0xf08bb: 0xf08bb, + 0xf08bc: 0xf08bc, + 0xf08bd: 0xf08bd, + 0xf08be: 0xf08be, + 0xf08bf: 0xf08bf, + 0xf08c0: 0xf08c0, + 0xf08c1: 0xf08c1, + 0xf08c2: 0xf08c2, + 0xf08c3: 0xf08c3, + 0xf08c4: 0xf08c4, + 0xf08c5: 0xf08c5, + 0xf08c6: 0xf08c6, + 0xf08c7: 0xf08c7, + 0xf08c8: 0xf08c8, + 0xf08c9: 0xf08c9, + 0xf08ca: 0xf08ca, + 0xf08cb: 0xf08cb, + 0xf08cc: 0xf08cc, + 0xf08cd: 0xf08cd, + 0xf08ce: 0xf08ce, + 0xf08cf: 0xf08cf, + 0xf08d0: 0xf08d0, + 0xf08d1: 0xf08d1, + 0xf08d2: 0xf08d2, + 0xf08d3: 0xf08d3, + 0xf08d4: 0xf08d4, + 0xf08d5: 0xf08d5, + 0xf08d6: 0xf08d6, + 0xf08d7: 0xf08d7, + 0xf08d8: 0xf08d8, + 0xf08d9: 0xf08d9, + 0xf08da: 0xf08da, + 0xf08db: 0xf08db, + 0xf08dc: 0xf08dc, + 0xf08dd: 0xf08dd, + 0xf08de: 0xf08de, + 0xf08df: 0xf08df, + 0xf08e0: 0xf08e0, + 0xf08e1: 0xf08e1, + 0xf08e2: 0xf08e2, + 0xf08e3: 0xf08e3, + 0xf08e4: 0xf08e4, + 0xf08e5: 0xf08e5, + 0xf08e6: 0xf08e6, + 0xf08e7: 0xf08e7, + 0xf08e8: 0xf08e8, + 0xf08e9: 0xf08e9, + 0xf08ea: 0xf08ea, + 0xf08eb: 0xf08eb, + 0xf08ec: 0xf08ec, + 0xf08ed: 0xf08ed, + 0xf08ee: 0xf08ee, + 0xf08ef: 0xf08ef, + 0xf08f0: 0xf08f0, + 0xf08f1: 0xf08f1, + 0xf08f2: 0xf08f2, + 0xf08f3: 0xf08f3, + 0xf08f4: 0xf08f4, + 0xf08f5: 0xf08f5, + 0xf08f6: 0xf08f6, + 0xf08f7: 0xf08f7, + 0xf08f8: 0xf08f8, + 0xf08f9: 0xf08f9, + 0xf08fa: 0xf08fa, + 0xf08fb: 0xf08fb, + 0xf08fc: 0xf08fc, + 0xf08fd: 0xf08fd, + 0xf08fe: 0xf08fe, + 0xf08ff: 0xf08ff, + 0xf0900: 0xf0900, + 0xf0901: 0xf0901, + 0xf0902: 0xf0902, + 0xf0903: 0xf0903, + 0xf0904: 0xf0904, + 0xf0905: 0xf0905, + 0xf0906: 0xf0906, + 0xf0907: 0xf0907, + 0xf0908: 0xf0908, + 0xf0909: 0xf0909, + 0xf090a: 0xf090a, + 0xf090b: 0xf090b, + 0xf090c: 0xf090c, + 0xf090d: 0xf090d, + 0xf090e: 0xf090e, + 0xf090f: 0xf090f, + 0xf0910: 0xf0910, + 0xf0911: 0xf0911, + 0xf0912: 0xf0912, + 0xf0913: 0xf0913, + 0xf0914: 0xf0914, + 0xf0915: 0xf0915, + 0xf0916: 0xf0916, + 0xf0917: 0xf0917, + 0xf0918: 0xf0918, + 0xf0919: 0xf0919, + 0xf091a: 0xf091a, + 0xf091b: 0xf091b, + 0xf091c: 0xf091c, + 0xf091d: 0xf091d, + 0xf091e: 0xf091e, + 0xf091f: 0xf091f, + 0xf0920: 0xf0920, + 0xf0921: 0xf0921, + 0xf0922: 0xf0922, + 0xf0923: 0xf0923, + 0xf0924: 0xf0924, + 0xf0925: 0xf0925, + 0xf0926: 0xf0926, + 0xf0927: 0xf0927, + 0xf0928: 0xf0928, + 0xf0929: 0xf0929, + 0xf092a: 0xf092a, + 0xf092b: 0xf092b, + 0xf092c: 0xf092c, + 0xf092d: 0xf092d, + 0xf092e: 0xf092e, + 0xf092f: 0xf092f, + 0xf0930: 0xf0930, + 0xf0931: 0xf0931, + 0xf0932: 0xf0932, + 0xf0933: 0xf0933, + 0xf0934: 0xf0934, + 0xf0935: 0xf0935, + 0xf0936: 0xf0936, + 0xf0937: 0xf0937, + 0xf0938: 0xf0938, + 0xf0939: 0xf0939, + 0xf093a: 0xf093a, + 0xf093b: 0xf093b, + 0xf093c: 0xf093c, + 0xf093d: 0xf093d, + 0xf093e: 0xf093e, + 0xf093f: 0xf093f, + 0xf0940: 0xf0940, + 0xf0941: 0xf0941, + 0xf0942: 0xf0942, + 0xf0943: 0xf0943, + 0xf0944: 0xf0944, + 0xf0945: 0xf0945, + 0xf0946: 0xf0946, + 0xf0947: 0xf0947, + 0xf0948: 0xf0948, + 0xf0949: 0xf0949, + 0xf094a: 0xf094a, + 0xf094b: 0xf094b, + 0xf094c: 0xf094c, + 0xf094d: 0xf094d, + 0xf094e: 0xf094e, + 0xf094f: 0xf094f, + 0xf0950: 0xf0950, + 0xf0951: 0xf0951, + 0xf0952: 0xf0952, + 0xf0953: 0xf0953, + 0xf0954: 0xf0954, + 0xf0955: 0xf0955, + 0xf0956: 0xf0956, + 0xf0957: 0xf0957, + 0xf0958: 0xf0958, + 0xf0959: 0xf0959, + 0xf095a: 0xf095a, + 0xf095b: 0xf095b, + 0xf095c: 0xf095c, + 0xf095d: 0xf095d, + 0xf095e: 0xf095e, + 0xf095f: 0xf095f, + 0xf0960: 0xf0960, + 0xf0961: 0xf0961, + 0xf0962: 0xf0962, + 0xf0963: 0xf0963, + 0xf0964: 0xf0964, + 0xf0965: 0xf0965, + 0xf0966: 0xf0966, + 0xf0967: 0xf0967, + 0xf0968: 0xf0968, + 0xf0969: 0xf0969, + 0xf096a: 0xf096a, + 0xf096b: 0xf096b, + 0xf096c: 0xf096c, + 0xf096d: 0xf096d, + 0xf096e: 0xf096e, + 0xf096f: 0xf096f, + 0xf0970: 0xf0970, + 0xf0971: 0xf0971, + 0xf0972: 0xf0972, + 0xf0973: 0xf0973, + 0xf0974: 0xf0974, + 0xf0975: 0xf0975, + 0xf0976: 0xf0976, + 0xf0977: 0xf0977, + 0xf0978: 0xf0978, + 0xf0979: 0xf0979, + 0xf097a: 0xf097a, + 0xf097b: 0xf097b, + 0xf097c: 0xf097c, + 0xf097d: 0xf097d, + 0xf097e: 0xf097e, + 0xf097f: 0xf097f, + 0xf0980: 0xf0980, + 0xf0981: 0xf0981, + 0xf0982: 0xf0982, + 0xf0983: 0xf0983, + 0xf0984: 0xf0984, + 0xf0985: 0xf0985, + 0xf0986: 0xf0986, + 0xf0987: 0xf0987, + 0xf0988: 0xf0988, + 0xf0989: 0xf0989, + 0xf098a: 0xf098a, + 0xf098b: 0xf098b, + 0xf098c: 0xf098c, + 0xf098d: 0xf098d, + 0xf098e: 0xf098e, + 0xf098f: 0xf098f, + 0xf0990: 0xf0990, + 0xf0991: 0xf0991, + 0xf0992: 0xf0992, + 0xf0993: 0xf0993, + 0xf0994: 0xf0994, + 0xf0995: 0xf0995, + 0xf0996: 0xf0996, + 0xf0997: 0xf0997, + 0xf0998: 0xf0998, + 0xf0999: 0xf0999, + 0xf099a: 0xf099a, + 0xf099b: 0xf099b, + 0xf099c: 0xf099c, + 0xf099d: 0xf099d, + 0xf099e: 0xf099e, + 0xf099f: 0xf099f, + 0xf09a0: 0xf09a0, + 0xf09a1: 0xf09a1, + 0xf09a2: 0xf09a2, + 0xf09a3: 0xf09a3, + 0xf09a4: 0xf09a4, + 0xf09a5: 0xf09a5, + 0xf09a6: 0xf09a6, + 0xf09a7: 0xf09a7, + 0xf09a8: 0xf09a8, + 0xf09a9: 0xf09a9, + 0xf09aa: 0xf09aa, + 0xf09ab: 0xf09ab, + 0xf09ac: 0xf09ac, + 0xf09ad: 0xf09ad, + 0xf09ae: 0xf09ae, + 0xf09af: 0xf09af, + 0xf09b0: 0xf09b0, + 0xf09b1: 0xf09b1, + 0xf09b2: 0xf09b2, + 0xf09b3: 0xf09b3, + 0xf09b4: 0xf09b4, + 0xf09b5: 0xf09b5, + 0xf09b6: 0xf09b6, + 0xf09b7: 0xf09b7, + 0xf09b8: 0xf09b8, + 0xf09b9: 0xf09b9, + 0xf09ba: 0xf09ba, + 0xf09bb: 0xf09bb, + 0xf09bc: 0xf09bc, + 0xf09bd: 0xf09bd, + 0xf09be: 0xf09be, + 0xf09bf: 0xf09bf, + 0xf09c0: 0xf09c0, + 0xf09c1: 0xf09c1, + 0xf09c2: 0xf09c2, + 0xf09c3: 0xf09c3, + 0xf09c4: 0xf09c4, + 0xf09c5: 0xf09c5, + 0xf09c6: 0xf09c6, + 0xf09c7: 0xf09c7, + 0xf09c8: 0xf09c8, + 0xf09c9: 0xf09c9, + 0xf09ca: 0xf09ca, + 0xf09cb: 0xf09cb, + 0xf09cc: 0xf09cc, + 0xf09cd: 0xf09cd, + 0xf09ce: 0xf09ce, + 0xf09cf: 0xf09cf, + 0xf09d0: 0xf09d0, + 0xf09d1: 0xf09d1, + 0xf09d2: 0xf09d2, + 0xf09d3: 0xf09d3, + 0xf09d4: 0xf09d4, + 0xf09d5: 0xf09d5, + 0xf09d6: 0xf09d6, + 0xf09d7: 0xf09d7, + 0xf09d8: 0xf09d8, + 0xf09d9: 0xf09d9, + 0xf09da: 0xf09da, + 0xf09db: 0xf09db, + 0xf09dc: 0xf09dc, + 0xf09dd: 0xf09dd, + 0xf09de: 0xf09de, + 0xf09df: 0xf09df, + 0xf09e0: 0xf09e0, + 0xf09e1: 0xf09e1, + 0xf09e2: 0xf09e2, + 0xf09e3: 0xf09e3, + 0xf09e4: 0xf09e4, + 0xf09e5: 0xf09e5, + 0xf09e6: 0xf09e6, + 0xf09e7: 0xf09e7, + 0xf09e8: 0xf09e8, + 0xf09e9: 0xf09e9, + 0xf09ea: 0xf09ea, + 0xf09eb: 0xf09eb, + 0xf09ec: 0xf09ec, + 0xf09ed: 0xf09ed, + 0xf09ee: 0xf09ee, + 0xf09ef: 0xf09ef, + 0xf09f0: 0xf09f0, + 0xf09f1: 0xf09f1, + 0xf09f2: 0xf09f2, + 0xf09f3: 0xf09f3, + 0xf09f4: 0xf09f4, + 0xf09f5: 0xf09f5, + 0xf09f6: 0xf09f6, + 0xf09f7: 0xf09f7, + 0xf09f8: 0xf09f8, + 0xf09f9: 0xf09f9, + 0xf09fa: 0xf09fa, + 0xf09fb: 0xf09fb, + 0xf09fc: 0xf09fc, + 0xf09fd: 0xf09fd, + 0xf09fe: 0xf09fe, + 0xf09ff: 0xf09ff, + 0xf0a00: 0xf0a00, + 0xf0a01: 0xf0a01, + 0xf0a02: 0xf0a02, + 0xf0a03: 0xf0a03, + 0xf0a04: 0xf0a04, + 0xf0a05: 0xf0a05, + 0xf0a06: 0xf0a06, + 0xf0a07: 0xf0a07, + 0xf0a08: 0xf0a08, + 0xf0a09: 0xf0a09, + 0xf0a0a: 0xf0a0a, + 0xf0a0b: 0xf0a0b, + 0xf0a0c: 0xf0a0c, + 0xf0a0d: 0xf0a0d, + 0xf0a0e: 0xf0a0e, + 0xf0a0f: 0xf0a0f, + 0xf0a10: 0xf0a10, + 0xf0a11: 0xf0a11, + 0xf0a12: 0xf0a12, + 0xf0a13: 0xf0a13, + 0xf0a14: 0xf0a14, + 0xf0a15: 0xf0a15, + 0xf0a16: 0xf0a16, + 0xf0a17: 0xf0a17, + 0xf0a18: 0xf0a18, + 0xf0a19: 0xf0a19, + 0xf0a1a: 0xf0a1a, + 0xf0a1b: 0xf0a1b, + 0xf0a1c: 0xf0a1c, + 0xf0a1d: 0xf0a1d, + 0xf0a1e: 0xf0a1e, + 0xf0a1f: 0xf0a1f, + 0xf0a20: 0xf0a20, + 0xf0a21: 0xf0a21, + 0xf0a22: 0xf0a22, + 0xf0a23: 0xf0a23, + 0xf0a24: 0xf0a24, + 0xf0a25: 0xf0a25, + 0xf0a26: 0xf0a26, + 0xf0a27: 0xf0a27, + 0xf0a28: 0xf0a28, + 0xf0a29: 0xf0a29, + 0xf0a2a: 0xf0a2a, + 0xf0a2b: 0xf0a2b, + 0xf0a2c: 0xf0a2c, + 0xf0a2d: 0xf0a2d, + 0xf0a2e: 0xf0a2e, + 0xf0a2f: 0xf0a2f, + 0xf0a30: 0xf0a30, + 0xf0a31: 0xf0a31, + 0xf0a32: 0xf0a32, + 0xf0a33: 0xf0a33, + 0xf0a34: 0xf0a34, + 0xf0a35: 0xf0a35, + 0xf0a36: 0xf0a36, + 0xf0a37: 0xf0a37, + 0xf0a38: 0xf0a38, + 0xf0a39: 0xf0a39, + 0xf0a3a: 0xf0a3a, + 0xf0a3b: 0xf0a3b, + 0xf0a3c: 0xf0a3c, + 0xf0a3d: 0xf0a3d, + 0xf0a3e: 0xf0a3e, + 0xf0a3f: 0xf0a3f, + 0xf0a40: 0xf0a40, + 0xf0a41: 0xf0a41, + 0xf0a42: 0xf0a42, + 0xf0a43: 0xf0a43, + 0xf0a44: 0xf0a44, + 0xf0a45: 0xf0a45, + 0xf0a46: 0xf0a46, + 0xf0a47: 0xf0a47, + 0xf0a48: 0xf0a48, + 0xf0a49: 0xf0a49, + 0xf0a4a: 0xf0a4a, + 0xf0a4b: 0xf0a4b, + 0xf0a4c: 0xf0a4c, + 0xf0a4d: 0xf0a4d, + 0xf0a4e: 0xf0a4e, + 0xf0a4f: 0xf0a4f, + 0xf0a50: 0xf0a50, + 0xf0a51: 0xf0a51, + 0xf0a52: 0xf0a52, + 0xf0a53: 0xf0a53, + 0xf0a54: 0xf0a54, + 0xf0a55: 0xf0a55, + 0xf0a56: 0xf0a56, + 0xf0a57: 0xf0a57, + 0xf0a58: 0xf0a58, + 0xf0a59: 0xf0a59, + 0xf0a5a: 0xf0a5a, + 0xf0a5b: 0xf0a5b, + 0xf0a5c: 0xf0a5c, + 0xf0a5d: 0xf0a5d, + 0xf0a5e: 0xf0a5e, + 0xf0a5f: 0xf0a5f, + 0xf0a60: 0xf0a60, + 0xf0a61: 0xf0a61, + 0xf0a62: 0xf0a62, + 0xf0a63: 0xf0a63, + 0xf0a64: 0xf0a64, + 0xf0a65: 0xf0a65, + 0xf0a66: 0xf0a66, + 0xf0a67: 0xf0a67, + 0xf0a68: 0xf0a68, + 0xf0a69: 0xf0a69, + 0xf0a6a: 0xf0a6a, + 0xf0a6b: 0xf0a6b, + 0xf0a6c: 0xf0a6c, + 0xf0a6d: 0xf0a6d, + 0xf0a6e: 0xf0a6e, + 0xf0a6f: 0xf0a6f, + 0xf0a70: 0xf0a70, + 0xf0a71: 0xf0a71, + 0xf0a72: 0xf0a72, + 0xf0a73: 0xf0a73, + 0xf0a74: 0xf0a74, + 0xf0a75: 0xf0a75, + 0xf0a76: 0xf0a76, + 0xf0a77: 0xf0a77, + 0xf0a78: 0xf0a78, + 0xf0a79: 0xf0a79, + 0xf0a7a: 0xf0a7a, + 0xf0a7b: 0xf0a7b, + 0xf0a7c: 0xf0a7c, + 0xf0a7d: 0xf0a7d, + 0xf0a7e: 0xf0a7e, + 0xf0a7f: 0xf0a7f, + 0xf0a80: 0xf0a80, + 0xf0a81: 0xf0a81, + 0xf0a82: 0xf0a82, + 0xf0a83: 0xf0a83, + 0xf0a84: 0xf0a84, + 0xf0a85: 0xf0a85, + 0xf0a86: 0xf0a86, + 0xf0a87: 0xf0a87, + 0xf0a88: 0xf0a88, + 0xf0a89: 0xf0a89, + 0xf0a8a: 0xf0a8a, + 0xf0a8b: 0xf0a8b, + 0xf0a8c: 0xf0a8c, + 0xf0a8d: 0xf0a8d, + 0xf0a8e: 0xf0a8e, + 0xf0a8f: 0xf0a8f, + 0xf0a90: 0xf0a90, + 0xf0a91: 0xf0a91, + 0xf0a92: 0xf0a92, + 0xf0a93: 0xf0a93, + 0xf0a94: 0xf0a94, + 0xf0a95: 0xf0a95, + 0xf0a96: 0xf0a96, + 0xf0a97: 0xf0a97, + 0xf0a98: 0xf0a98, + 0xf0a99: 0xf0a99, + 0xf0a9a: 0xf0a9a, + 0xf0a9b: 0xf0a9b, + 0xf0a9c: 0xf0a9c, + 0xf0a9d: 0xf0a9d, + 0xf0a9e: 0xf0a9e, + 0xf0a9f: 0xf0a9f, + 0xf0aa0: 0xf0aa0, + 0xf0aa1: 0xf0aa1, + 0xf0aa2: 0xf0aa2, + 0xf0aa3: 0xf0aa3, + 0xf0aa4: 0xf0aa4, + 0xf0aa5: 0xf0aa5, + 0xf0aa6: 0xf0aa6, + 0xf0aa7: 0xf0aa7, + 0xf0aa8: 0xf0aa8, + 0xf0aa9: 0xf0aa9, + 0xf0aaa: 0xf0aaa, + 0xf0aab: 0xf0aab, + 0xf0aac: 0xf0aac, + 0xf0aad: 0xf0aad, + 0xf0aae: 0xf0aae, + 0xf0aaf: 0xf0aaf, + 0xf0ab0: 0xf0ab0, + 0xf0ab1: 0xf0ab1, + 0xf0ab2: 0xf0ab2, + 0xf0ab3: 0xf0ab3, + 0xf0ab4: 0xf0ab4, + 0xf0ab5: 0xf0ab5, + 0xf0ab6: 0xf0ab6, + 0xf0ab7: 0xf0ab7, + 0xf0ab8: 0xf0ab8, + 0xf0ab9: 0xf0ab9, + 0xf0aba: 0xf0aba, + 0xf0abb: 0xf0abb, + 0xf0abc: 0xf0abc, + 0xf0abd: 0xf0abd, + 0xf0abe: 0xf0abe, + 0xf0abf: 0xf0abf, + 0xf0ac0: 0xf0ac0, + 0xf0ac1: 0xf0ac1, + 0xf0ac2: 0xf0ac2, + 0xf0ac3: 0xf0ac3, + 0xf0ac4: 0xf0ac4, + 0xf0ac5: 0xf0ac5, + 0xf0ac6: 0xf0ac6, + 0xf0ac7: 0xf0ac7, + 0xf0ac8: 0xf0ac8, + 0xf0ac9: 0xf0ac9, + 0xf0aca: 0xf0aca, + 0xf0acb: 0xf0acb, + 0xf0acc: 0xf0acc, + 0xf0acd: 0xf0acd, + 0xf0ace: 0xf0ace, + 0xf0acf: 0xf0acf, + 0xf0ad0: 0xf0ad0, + 0xf0ad1: 0xf0ad1, + 0xf0ad2: 0xf0ad2, + 0xf0ad3: 0xf0ad3, + 0xf0ad4: 0xf0ad4, + 0xf0ad5: 0xf0ad5, + 0xf0ad6: 0xf0ad6, + 0xf0ad7: 0xf0ad7, + 0xf0ad8: 0xf0ad8, + 0xf0ad9: 0xf0ad9, + 0xf0ada: 0xf0ada, + 0xf0adb: 0xf0adb, + 0xf0adc: 0xf0adc, + 0xf0add: 0xf0add, + 0xf0ade: 0xf0ade, + 0xf0adf: 0xf0adf, + 0xf0ae0: 0xf0ae0, + 0xf0ae1: 0xf0ae1, + 0xf0ae2: 0xf0ae2, + 0xf0ae3: 0xf0ae3, + 0xf0ae4: 0xf0ae4, + 0xf0ae5: 0xf0ae5, + 0xf0ae6: 0xf0ae6, + 0xf0ae7: 0xf0ae7, + 0xf0ae8: 0xf0ae8, + 0xf0ae9: 0xf0ae9, + 0xf0aea: 0xf0aea, + 0xf0aeb: 0xf0aeb, + 0xf0aec: 0xf0aec, + 0xf0aed: 0xf0aed, + 0xf0aee: 0xf0aee, + 0xf0aef: 0xf0aef, + 0xf0af0: 0xf0af0, + 0xf0af1: 0xf0af1, + 0xf0af2: 0xf0af2, + 0xf0af3: 0xf0af3, + 0xf0af4: 0xf0af4, + 0xf0af5: 0xf0af5, + 0xf0af6: 0xf0af6, + 0xf0af7: 0xf0af7, + 0xf0af8: 0xf0af8, + 0xf0af9: 0xf0af9, + 0xf0afa: 0xf0afa, + 0xf0afb: 0xf0afb, + 0xf0afc: 0xf0afc, + 0xf0afd: 0xf0afd, + 0xf0afe: 0xf0afe, + 0xf0aff: 0xf0aff, + 0xf0b00: 0xf0b00, + 0xf0b01: 0xf0b01, + 0xf0b02: 0xf0b02, + 0xf0b03: 0xf0b03, + 0xf0b04: 0xf0b04, + 0xf0b05: 0xf0b05, + 0xf0b06: 0xf0b06, + 0xf0b07: 0xf0b07, + 0xf0b08: 0xf0b08, + 0xf0b09: 0xf0b09, + 0xf0b0a: 0xf0b0a, + 0xf0b0b: 0xf0b0b, + 0xf0b0c: 0xf0b0c, + 0xf0b0d: 0xf0b0d, + 0xf0b0e: 0xf0b0e, + 0xf0b0f: 0xf0b0f, + 0xf0b10: 0xf0b10, + 0xf0b11: 0xf0b11, + 0xf0b12: 0xf0b12, + 0xf0b13: 0xf0b13, + 0xf0b14: 0xf0b14, + 0xf0b15: 0xf0b15, + 0xf0b16: 0xf0b16, + 0xf0b17: 0xf0b17, + 0xf0b18: 0xf0b18, + 0xf0b19: 0xf0b19, + 0xf0b1a: 0xf0b1a, + 0xf0b1b: 0xf0b1b, + 0xf0b1c: 0xf0b1c, + 0xf0b1d: 0xf0b1d, + 0xf0b1e: 0xf0b1e, + 0xf0b1f: 0xf0b1f, + 0xf0b20: 0xf0b20, + 0xf0b21: 0xf0b21, + 0xf0b22: 0xf0b22, + 0xf0b23: 0xf0b23, + 0xf0b24: 0xf0b24, + 0xf0b25: 0xf0b25, + 0xf0b26: 0xf0b26, + 0xf0b27: 0xf0b27, + 0xf0b28: 0xf0b28, + 0xf0b29: 0xf0b29, + 0xf0b2a: 0xf0b2a, + 0xf0b2b: 0xf0b2b, + 0xf0b2c: 0xf0b2c, + 0xf0b2d: 0xf0b2d, + 0xf0b2e: 0xf0b2e, + 0xf0b2f: 0xf0b2f, + 0xf0b30: 0xf0b30, + 0xf0b31: 0xf0b31, + 0xf0b32: 0xf0b32, + 0xf0b33: 0xf0b33, + 0xf0b34: 0xf0b34, + 0xf0b35: 0xf0b35, + 0xf0b36: 0xf0b36, + 0xf0b37: 0xf0b37, + 0xf0b38: 0xf0b38, + 0xf0b39: 0xf0b39, + 0xf0b3a: 0xf0b3a, + 0xf0b3b: 0xf0b3b, + 0xf0b3c: 0xf0b3c, + 0xf0b3d: 0xf0b3d, + 0xf0b3e: 0xf0b3e, + 0xf0b3f: 0xf0b3f, + 0xf0b40: 0xf0b40, + 0xf0b41: 0xf0b41, + 0xf0b42: 0xf0b42, + 0xf0b43: 0xf0b43, + 0xf0b44: 0xf0b44, + 0xf0b45: 0xf0b45, + 0xf0b46: 0xf0b46, + 0xf0b47: 0xf0b47, + 0xf0b48: 0xf0b48, + 0xf0b49: 0xf0b49, + 0xf0b4a: 0xf0b4a, + 0xf0b4b: 0xf0b4b, + 0xf0b4c: 0xf0b4c, + 0xf0b4d: 0xf0b4d, + 0xf0b4e: 0xf0b4e, + 0xf0b4f: 0xf0b4f, + 0xf0b50: 0xf0b50, + 0xf0b51: 0xf0b51, + 0xf0b52: 0xf0b52, + 0xf0b53: 0xf0b53, + 0xf0b54: 0xf0b54, + 0xf0b55: 0xf0b55, + 0xf0b56: 0xf0b56, + 0xf0b57: 0xf0b57, + 0xf0b58: 0xf0b58, + 0xf0b59: 0xf0b59, + 0xf0b5a: 0xf0b5a, + 0xf0b5b: 0xf0b5b, + 0xf0b5c: 0xf0b5c, + 0xf0b5d: 0xf0b5d, + 0xf0b5e: 0xf0b5e, + 0xf0b5f: 0xf0b5f, + 0xf0b60: 0xf0b60, + 0xf0b61: 0xf0b61, + 0xf0b62: 0xf0b62, + 0xf0b63: 0xf0b63, + 0xf0b64: 0xf0b64, + 0xf0b65: 0xf0b65, + 0xf0b66: 0xf0b66, + 0xf0b67: 0xf0b67, + 0xf0b68: 0xf0b68, + 0xf0b69: 0xf0b69, + 0xf0b6a: 0xf0b6a, + 0xf0b6b: 0xf0b6b, + 0xf0b6c: 0xf0b6c, + 0xf0b6d: 0xf0b6d, + 0xf0b6e: 0xf0b6e, + 0xf0b6f: 0xf0b6f, + 0xf0b70: 0xf0b70, + 0xf0b71: 0xf0b71, + 0xf0b72: 0xf0b72, + 0xf0b73: 0xf0b73, + 0xf0b74: 0xf0b74, + 0xf0b75: 0xf0b75, + 0xf0b76: 0xf0b76, + 0xf0b77: 0xf0b77, + 0xf0b78: 0xf0b78, + 0xf0b79: 0xf0b79, + 0xf0b7a: 0xf0b7a, + 0xf0b7b: 0xf0b7b, + 0xf0b7c: 0xf0b7c, + 0xf0b7d: 0xf0b7d, + 0xf0b7e: 0xf0b7e, + 0xf0b7f: 0xf0b7f, + 0xf0b80: 0xf0b80, + 0xf0b81: 0xf0b81, + 0xf0b82: 0xf0b82, + 0xf0b83: 0xf0b83, + 0xf0b84: 0xf0b84, + 0xf0b85: 0xf0b85, + 0xf0b86: 0xf0b86, + 0xf0b87: 0xf0b87, + 0xf0b88: 0xf0b88, + 0xf0b89: 0xf0b89, + 0xf0b8a: 0xf0b8a, + 0xf0b8b: 0xf0b8b, + 0xf0b8c: 0xf0b8c, + 0xf0b8d: 0xf0b8d, + 0xf0b8e: 0xf0b8e, + 0xf0b8f: 0xf0b8f, + 0xf0b90: 0xf0b90, + 0xf0b91: 0xf0b91, + 0xf0b92: 0xf0b92, + 0xf0b93: 0xf0b93, + 0xf0b94: 0xf0b94, + 0xf0b95: 0xf0b95, + 0xf0b96: 0xf0b96, + 0xf0b97: 0xf0b97, + 0xf0b98: 0xf0b98, + 0xf0b99: 0xf0b99, + 0xf0b9a: 0xf0b9a, + 0xf0b9b: 0xf0b9b, + 0xf0b9c: 0xf0b9c, + 0xf0b9d: 0xf0b9d, + 0xf0b9e: 0xf0b9e, + 0xf0b9f: 0xf0b9f, + 0xf0ba0: 0xf0ba0, + 0xf0ba1: 0xf0ba1, + 0xf0ba2: 0xf0ba2, + 0xf0ba3: 0xf0ba3, + 0xf0ba4: 0xf0ba4, + 0xf0ba5: 0xf0ba5, + 0xf0ba6: 0xf0ba6, + 0xf0ba7: 0xf0ba7, + 0xf0ba8: 0xf0ba8, + 0xf0ba9: 0xf0ba9, + 0xf0baa: 0xf0baa, + 0xf0bab: 0xf0bab, + 0xf0bac: 0xf0bac, + 0xf0bad: 0xf0bad, + 0xf0bae: 0xf0bae, + 0xf0baf: 0xf0baf, + 0xf0bb0: 0xf0bb0, + 0xf0bb1: 0xf0bb1, + 0xf0bb2: 0xf0bb2, + 0xf0bb3: 0xf0bb3, + 0xf0bb4: 0xf0bb4, + 0xf0bb5: 0xf0bb5, + 0xf0bb6: 0xf0bb6, + 0xf0bb7: 0xf0bb7, + 0xf0bb8: 0xf0bb8, + 0xf0bb9: 0xf0bb9, + 0xf0bba: 0xf0bba, + 0xf0bbb: 0xf0bbb, + 0xf0bbc: 0xf0bbc, + 0xf0bbd: 0xf0bbd, + 0xf0bbe: 0xf0bbe, + 0xf0bbf: 0xf0bbf, + 0xf0bc0: 0xf0bc0, + 0xf0bc1: 0xf0bc1, + 0xf0bc2: 0xf0bc2, + 0xf0bc3: 0xf0bc3, + 0xf0bc4: 0xf0bc4, + 0xf0bc5: 0xf0bc5, + 0xf0bc6: 0xf0bc6, + 0xf0bc7: 0xf0bc7, + 0xf0bc8: 0xf0bc8, + 0xf0bc9: 0xf0bc9, + 0xf0bca: 0xf0bca, + 0xf0bcb: 0xf0bcb, + 0xf0bcc: 0xf0bcc, + 0xf0bcd: 0xf0bcd, + 0xf0bce: 0xf0bce, + 0xf0bcf: 0xf0bcf, + 0xf0bd0: 0xf0bd0, + 0xf0bd1: 0xf0bd1, + 0xf0bd2: 0xf0bd2, + 0xf0bd3: 0xf0bd3, + 0xf0bd4: 0xf0bd4, + 0xf0bd5: 0xf0bd5, + 0xf0bd6: 0xf0bd6, + 0xf0bd7: 0xf0bd7, + 0xf0bd8: 0xf0bd8, + 0xf0bd9: 0xf0bd9, + 0xf0bda: 0xf0bda, + 0xf0bdb: 0xf0bdb, + 0xf0bdc: 0xf0bdc, + 0xf0bdd: 0xf0bdd, + 0xf0bde: 0xf0bde, + 0xf0bdf: 0xf0bdf, + 0xf0be0: 0xf0be0, + 0xf0be1: 0xf0be1, + 0xf0be2: 0xf0be2, + 0xf0be3: 0xf0be3, + 0xf0be4: 0xf0be4, + 0xf0be5: 0xf0be5, + 0xf0be6: 0xf0be6, + 0xf0be7: 0xf0be7, + 0xf0be8: 0xf0be8, + 0xf0be9: 0xf0be9, + 0xf0bea: 0xf0bea, + 0xf0beb: 0xf0beb, + 0xf0bec: 0xf0bec, + 0xf0bed: 0xf0bed, + 0xf0bee: 0xf0bee, + 0xf0bef: 0xf0bef, + 0xf0bf0: 0xf0bf0, + 0xf0bf1: 0xf0bf1, + 0xf0bf2: 0xf0bf2, + 0xf0bf3: 0xf0bf3, + 0xf0bf4: 0xf0bf4, + 0xf0bf5: 0xf0bf5, + 0xf0bf6: 0xf0bf6, + 0xf0bf7: 0xf0bf7, + 0xf0bf8: 0xf0bf8, + 0xf0bf9: 0xf0bf9, + 0xf0bfa: 0xf0bfa, + 0xf0bfb: 0xf0bfb, + 0xf0bfc: 0xf0bfc, + 0xf0bfd: 0xf0bfd, + 0xf0bfe: 0xf0bfe, + 0xf0bff: 0xf0bff, + 0xf0c00: 0xf0c00, + 0xf0c01: 0xf0c01, + 0xf0c02: 0xf0c02, + 0xf0c03: 0xf0c03, + 0xf0c04: 0xf0c04, + 0xf0c05: 0xf0c05, + 0xf0c06: 0xf0c06, + 0xf0c07: 0xf0c07, + 0xf0c08: 0xf0c08, + 0xf0c09: 0xf0c09, + 0xf0c0a: 0xf0c0a, + 0xf0c0b: 0xf0c0b, + 0xf0c0c: 0xf0c0c, + 0xf0c0d: 0xf0c0d, + 0xf0c0e: 0xf0c0e, + 0xf0c0f: 0xf0c0f, + 0xf0c10: 0xf0c10, + 0xf0c11: 0xf0c11, + 0xf0c12: 0xf0c12, + 0xf0c13: 0xf0c13, + 0xf0c14: 0xf0c14, + 0xf0c15: 0xf0c15, + 0xf0c16: 0xf0c16, + 0xf0c17: 0xf0c17, + 0xf0c18: 0xf0c18, + 0xf0c19: 0xf0c19, + 0xf0c1a: 0xf0c1a, + 0xf0c1b: 0xf0c1b, + 0xf0c1c: 0xf0c1c, + 0xf0c1d: 0xf0c1d, + 0xf0c1e: 0xf0c1e, + 0xf0c1f: 0xf0c1f, + 0xf0c20: 0xf0c20, + 0xf0c21: 0xf0c21, + 0xf0c22: 0xf0c22, + 0xf0c23: 0xf0c23, + 0xf0c24: 0xf0c24, + 0xf0c25: 0xf0c25, + 0xf0c26: 0xf0c26, + 0xf0c27: 0xf0c27, + 0xf0c28: 0xf0c28, + 0xf0c29: 0xf0c29, + 0xf0c2a: 0xf0c2a, + 0xf0c2b: 0xf0c2b, + 0xf0c2c: 0xf0c2c, + 0xf0c2d: 0xf0c2d, + 0xf0c2e: 0xf0c2e, + 0xf0c2f: 0xf0c2f, + 0xf0c30: 0xf0c30, + 0xf0c31: 0xf0c31, + 0xf0c32: 0xf0c32, + 0xf0c33: 0xf0c33, + 0xf0c34: 0xf0c34, + 0xf0c35: 0xf0c35, + 0xf0c36: 0xf0c36, + 0xf0c37: 0xf0c37, + 0xf0c38: 0xf0c38, + 0xf0c39: 0xf0c39, + 0xf0c3a: 0xf0c3a, + 0xf0c3b: 0xf0c3b, + 0xf0c3c: 0xf0c3c, + 0xf0c3d: 0xf0c3d, + 0xf0c3e: 0xf0c3e, + 0xf0c3f: 0xf0c3f, + 0xf0c40: 0xf0c40, + 0xf0c41: 0xf0c41, + 0xf0c42: 0xf0c42, + 0xf0c43: 0xf0c43, + 0xf0c44: 0xf0c44, + 0xf0c45: 0xf0c45, + 0xf0c46: 0xf0c46, + 0xf0c47: 0xf0c47, + 0xf0c48: 0xf0c48, + 0xf0c49: 0xf0c49, + 0xf0c4a: 0xf0c4a, + 0xf0c4b: 0xf0c4b, + 0xf0c4c: 0xf0c4c, + 0xf0c4d: 0xf0c4d, + 0xf0c4e: 0xf0c4e, + 0xf0c4f: 0xf0c4f, + 0xf0c50: 0xf0c50, + 0xf0c51: 0xf0c51, + 0xf0c52: 0xf0c52, + 0xf0c53: 0xf0c53, + 0xf0c54: 0xf0c54, + 0xf0c55: 0xf0c55, + 0xf0c56: 0xf0c56, + 0xf0c57: 0xf0c57, + 0xf0c58: 0xf0c58, + 0xf0c59: 0xf0c59, + 0xf0c5a: 0xf0c5a, + 0xf0c5b: 0xf0c5b, + 0xf0c5c: 0xf0c5c, + 0xf0c5d: 0xf0c5d, + 0xf0c5e: 0xf0c5e, + 0xf0c5f: 0xf0c5f, + 0xf0c60: 0xf0c60, + 0xf0c61: 0xf0c61, + 0xf0c62: 0xf0c62, + 0xf0c63: 0xf0c63, + 0xf0c64: 0xf0c64, + 0xf0c65: 0xf0c65, + 0xf0c66: 0xf0c66, + 0xf0c67: 0xf0c67, + 0xf0c68: 0xf0c68, + 0xf0c69: 0xf0c69, + 0xf0c6a: 0xf0c6a, + 0xf0c6b: 0xf0c6b, + 0xf0c6c: 0xf0c6c, + 0xf0c6d: 0xf0c6d, + 0xf0c6e: 0xf0c6e, + 0xf0c6f: 0xf0c6f, + 0xf0c70: 0xf0c70, + 0xf0c71: 0xf0c71, + 0xf0c72: 0xf0c72, + 0xf0c73: 0xf0c73, + 0xf0c74: 0xf0c74, + 0xf0c75: 0xf0c75, + 0xf0c76: 0xf0c76, + 0xf0c77: 0xf0c77, + 0xf0c78: 0xf0c78, + 0xf0c79: 0xf0c79, + 0xf0c7a: 0xf0c7a, + 0xf0c7b: 0xf0c7b, + 0xf0c7c: 0xf0c7c, + 0xf0c7d: 0xf0c7d, + 0xf0c7e: 0xf0c7e, + 0xf0c7f: 0xf0c7f, + 0xf0c80: 0xf0c80, + 0xf0c81: 0xf0c81, + 0xf0c82: 0xf0c82, + 0xf0c83: 0xf0c83, + 0xf0c84: 0xf0c84, + 0xf0c85: 0xf0c85, + 0xf0c86: 0xf0c86, + 0xf0c87: 0xf0c87, + 0xf0c88: 0xf0c88, + 0xf0c89: 0xf0c89, + 0xf0c8a: 0xf0c8a, + 0xf0c8b: 0xf0c8b, + 0xf0c8c: 0xf0c8c, + 0xf0c8d: 0xf0c8d, + 0xf0c8e: 0xf0c8e, + 0xf0c8f: 0xf0c8f, + 0xf0c90: 0xf0c90, + 0xf0c91: 0xf0c91, + 0xf0c92: 0xf0c92, + 0xf0c93: 0xf0c93, + 0xf0c94: 0xf0c94, + 0xf0c95: 0xf0c95, + 0xf0c96: 0xf0c96, + 0xf0c97: 0xf0c97, + 0xf0c98: 0xf0c98, + 0xf0c99: 0xf0c99, + 0xf0c9a: 0xf0c9a, + 0xf0c9b: 0xf0c9b, + 0xf0c9c: 0xf0c9c, + 0xf0c9d: 0xf0c9d, + 0xf0c9e: 0xf0c9e, + 0xf0c9f: 0xf0c9f, + 0xf0ca0: 0xf0ca0, + 0xf0ca1: 0xf0ca1, + 0xf0ca2: 0xf0ca2, + 0xf0ca3: 0xf0ca3, + 0xf0ca4: 0xf0ca4, + 0xf0ca5: 0xf0ca5, + 0xf0ca6: 0xf0ca6, + 0xf0ca7: 0xf0ca7, + 0xf0ca8: 0xf0ca8, + 0xf0ca9: 0xf0ca9, + 0xf0caa: 0xf0caa, + 0xf0cab: 0xf0cab, + 0xf0cac: 0xf0cac, + 0xf0cad: 0xf0cad, + 0xf0cae: 0xf0cae, + 0xf0caf: 0xf0caf, + 0xf0cb0: 0xf0cb0, + 0xf0cb1: 0xf0cb1, + 0xf0cb2: 0xf0cb2, + 0xf0cb3: 0xf0cb3, + 0xf0cb4: 0xf0cb4, + 0xf0cb5: 0xf0cb5, + 0xf0cb6: 0xf0cb6, + 0xf0cb7: 0xf0cb7, + 0xf0cb8: 0xf0cb8, + 0xf0cb9: 0xf0cb9, + 0xf0cba: 0xf0cba, + 0xf0cbb: 0xf0cbb, + 0xf0cbc: 0xf0cbc, + 0xf0cbd: 0xf0cbd, + 0xf0cbe: 0xf0cbe, + 0xf0cbf: 0xf0cbf, + 0xf0cc0: 0xf0cc0, + 0xf0cc1: 0xf0cc1, + 0xf0cc2: 0xf0cc2, + 0xf0cc3: 0xf0cc3, + 0xf0cc4: 0xf0cc4, + 0xf0cc5: 0xf0cc5, + 0xf0cc6: 0xf0cc6, + 0xf0cc7: 0xf0cc7, + 0xf0cc8: 0xf0cc8, + 0xf0cc9: 0xf0cc9, + 0xf0cca: 0xf0cca, + 0xf0ccb: 0xf0ccb, + 0xf0ccc: 0xf0ccc, + 0xf0ccd: 0xf0ccd, + 0xf0cce: 0xf0cce, + 0xf0ccf: 0xf0ccf, + 0xf0cd0: 0xf0cd0, + 0xf0cd1: 0xf0cd1, + 0xf0cd2: 0xf0cd2, + 0xf0cd3: 0xf0cd3, + 0xf0cd4: 0xf0cd4, + 0xf0cd5: 0xf0cd5, + 0xf0cd6: 0xf0cd6, + 0xf0cd7: 0xf0cd7, + 0xf0cd8: 0xf0cd8, + 0xf0cd9: 0xf0cd9, + 0xf0cda: 0xf0cda, + 0xf0cdb: 0xf0cdb, + 0xf0cdc: 0xf0cdc, + 0xf0cdd: 0xf0cdd, + 0xf0cde: 0xf0cde, + 0xf0cdf: 0xf0cdf, + 0xf0ce0: 0xf0ce0, + 0xf0ce1: 0xf0ce1, + 0xf0ce2: 0xf0ce2, + 0xf0ce3: 0xf0ce3, + 0xf0ce4: 0xf0ce4, + 0xf0ce5: 0xf0ce5, + 0xf0ce6: 0xf0ce6, + 0xf0ce7: 0xf0ce7, + 0xf0ce8: 0xf0ce8, + 0xf0ce9: 0xf0ce9, + 0xf0cea: 0xf0cea, + 0xf0ceb: 0xf0ceb, + 0xf0cec: 0xf0cec, + 0xf0ced: 0xf0ced, + 0xf0cee: 0xf0cee, + 0xf0cef: 0xf0cef, + 0xf0cf0: 0xf0cf0, + 0xf0cf1: 0xf0cf1, + 0xf0cf2: 0xf0cf2, + 0xf0cf3: 0xf0cf3, + 0xf0cf4: 0xf0cf4, + 0xf0cf5: 0xf0cf5, + 0xf0cf6: 0xf0cf6, + 0xf0cf7: 0xf0cf7, + 0xf0cf8: 0xf0cf8, + 0xf0cf9: 0xf0cf9, + 0xf0cfa: 0xf0cfa, + 0xf0cfb: 0xf0cfb, + 0xf0cfc: 0xf0cfc, + 0xf0cfd: 0xf0cfd, + 0xf0cfe: 0xf0cfe, + 0xf0cff: 0xf0cff, + 0xf0d00: 0xf0d00, + 0xf0d01: 0xf0d01, + 0xf0d02: 0xf0d02, + 0xf0d03: 0xf0d03, + 0xf0d04: 0xf0d04, + 0xf0d05: 0xf0d05, + 0xf0d06: 0xf0d06, + 0xf0d07: 0xf0d07, + 0xf0d08: 0xf0d08, + 0xf0d09: 0xf0d09, + 0xf0d0a: 0xf0d0a, + 0xf0d0b: 0xf0d0b, + 0xf0d0c: 0xf0d0c, + 0xf0d0d: 0xf0d0d, + 0xf0d0e: 0xf0d0e, + 0xf0d0f: 0xf0d0f, + 0xf0d10: 0xf0d10, + 0xf0d11: 0xf0d11, + 0xf0d12: 0xf0d12, + 0xf0d13: 0xf0d13, + 0xf0d14: 0xf0d14, + 0xf0d15: 0xf0d15, + 0xf0d16: 0xf0d16, + 0xf0d17: 0xf0d17, + 0xf0d18: 0xf0d18, + 0xf0d19: 0xf0d19, + 0xf0d1a: 0xf0d1a, + 0xf0d1b: 0xf0d1b, + 0xf0d1c: 0xf0d1c, + 0xf0d1d: 0xf0d1d, + 0xf0d1e: 0xf0d1e, + 0xf0d1f: 0xf0d1f, + 0xf0d20: 0xf0d20, + 0xf0d21: 0xf0d21, + 0xf0d22: 0xf0d22, + 0xf0d23: 0xf0d23, + 0xf0d24: 0xf0d24, + 0xf0d25: 0xf0d25, + 0xf0d26: 0xf0d26, + 0xf0d27: 0xf0d27, + 0xf0d28: 0xf0d28, + 0xf0d29: 0xf0d29, + 0xf0d2a: 0xf0d2a, + 0xf0d2b: 0xf0d2b, + 0xf0d2c: 0xf0d2c, + 0xf0d2d: 0xf0d2d, + 0xf0d2e: 0xf0d2e, + 0xf0d2f: 0xf0d2f, + 0xf0d30: 0xf0d30, + 0xf0d31: 0xf0d31, + 0xf0d32: 0xf0d32, + 0xf0d33: 0xf0d33, + 0xf0d34: 0xf0d34, + 0xf0d35: 0xf0d35, + 0xf0d36: 0xf0d36, + 0xf0d37: 0xf0d37, + 0xf0d38: 0xf0d38, + 0xf0d39: 0xf0d39, + 0xf0d3a: 0xf0d3a, + 0xf0d3b: 0xf0d3b, + 0xf0d3c: 0xf0d3c, + 0xf0d3d: 0xf0d3d, + 0xf0d3e: 0xf0d3e, + 0xf0d3f: 0xf0d3f, + 0xf0d40: 0xf0d40, + 0xf0d41: 0xf0d41, + 0xf0d42: 0xf0d42, + 0xf0d43: 0xf0d43, + 0xf0d44: 0xf0d44, + 0xf0d45: 0xf0d45, + 0xf0d46: 0xf0d46, + 0xf0d47: 0xf0d47, + 0xf0d48: 0xf0d48, + 0xf0d49: 0xf0d49, + 0xf0d4a: 0xf0d4a, + 0xf0d4b: 0xf0d4b, + 0xf0d4c: 0xf0d4c, + 0xf0d4d: 0xf0d4d, + 0xf0d4e: 0xf0d4e, + 0xf0d4f: 0xf0d4f, + 0xf0d50: 0xf0d50, + 0xf0d51: 0xf0d51, + 0xf0d52: 0xf0d52, + 0xf0d53: 0xf0d53, + 0xf0d54: 0xf0d54, + 0xf0d55: 0xf0d55, + 0xf0d56: 0xf0d56, + 0xf0d57: 0xf0d57, + 0xf0d58: 0xf0d58, + 0xf0d59: 0xf0d59, + 0xf0d5a: 0xf0d5a, + 0xf0d5b: 0xf0d5b, + 0xf0d5c: 0xf0d5c, + 0xf0d5d: 0xf0d5d, + 0xf0d5e: 0xf0d5e, + 0xf0d5f: 0xf0d5f, + 0xf0d60: 0xf0d60, + 0xf0d61: 0xf0d61, + 0xf0d62: 0xf0d62, + 0xf0d63: 0xf0d63, + 0xf0d64: 0xf0d64, + 0xf0d65: 0xf0d65, + 0xf0d66: 0xf0d66, + 0xf0d67: 0xf0d67, + 0xf0d68: 0xf0d68, + 0xf0d69: 0xf0d69, + 0xf0d6a: 0xf0d6a, + 0xf0d6b: 0xf0d6b, + 0xf0d6c: 0xf0d6c, + 0xf0d6d: 0xf0d6d, + 0xf0d6e: 0xf0d6e, + 0xf0d6f: 0xf0d6f, + 0xf0d70: 0xf0d70, + 0xf0d71: 0xf0d71, + 0xf0d72: 0xf0d72, + 0xf0d73: 0xf0d73, + 0xf0d74: 0xf0d74, + 0xf0d75: 0xf0d75, + 0xf0d76: 0xf0d76, + 0xf0d77: 0xf0d77, + 0xf0d78: 0xf0d78, + 0xf0d79: 0xf0d79, + 0xf0d7a: 0xf0d7a, + 0xf0d7b: 0xf0d7b, + 0xf0d7c: 0xf0d7c, + 0xf0d7d: 0xf0d7d, + 0xf0d7e: 0xf0d7e, + 0xf0d7f: 0xf0d7f, + 0xf0d80: 0xf0d80, + 0xf0d81: 0xf0d81, + 0xf0d82: 0xf0d82, + 0xf0d83: 0xf0d83, + 0xf0d84: 0xf0d84, + 0xf0d85: 0xf0d85, + 0xf0d86: 0xf0d86, + 0xf0d87: 0xf0d87, + 0xf0d88: 0xf0d88, + 0xf0d89: 0xf0d89, + 0xf0d8a: 0xf0d8a, + 0xf0d8b: 0xf0d8b, + 0xf0d8c: 0xf0d8c, + 0xf0d8d: 0xf0d8d, + 0xf0d8e: 0xf0d8e, + 0xf0d8f: 0xf0d8f, + 0xf0d90: 0xf0d90, + 0xf0d91: 0xf0d91, + 0xf0d92: 0xf0d92, + 0xf0d93: 0xf0d93, + 0xf0d94: 0xf0d94, + 0xf0d95: 0xf0d95, + 0xf0d96: 0xf0d96, + 0xf0d97: 0xf0d97, + 0xf0d98: 0xf0d98, + 0xf0d99: 0xf0d99, + 0xf0d9a: 0xf0d9a, + 0xf0d9b: 0xf0d9b, + 0xf0d9c: 0xf0d9c, + 0xf0d9d: 0xf0d9d, + 0xf0d9e: 0xf0d9e, + 0xf0d9f: 0xf0d9f, + 0xf0da0: 0xf0da0, + 0xf0da1: 0xf0da1, + 0xf0da2: 0xf0da2, + 0xf0da3: 0xf0da3, + 0xf0da4: 0xf0da4, + 0xf0da5: 0xf0da5, + 0xf0da6: 0xf0da6, + 0xf0da7: 0xf0da7, + 0xf0da8: 0xf0da8, + 0xf0da9: 0xf0da9, + 0xf0daa: 0xf0daa, + 0xf0dab: 0xf0dab, + 0xf0dac: 0xf0dac, + 0xf0dad: 0xf0dad, + 0xf0dae: 0xf0dae, + 0xf0daf: 0xf0daf, + 0xf0db0: 0xf0db0, + 0xf0db1: 0xf0db1, + 0xf0db2: 0xf0db2, + 0xf0db3: 0xf0db3, + 0xf0db4: 0xf0db4, + 0xf0db5: 0xf0db5, + 0xf0db6: 0xf0db6, + 0xf0db7: 0xf0db7, + 0xf0db8: 0xf0db8, + 0xf0db9: 0xf0db9, + 0xf0dba: 0xf0dba, + 0xf0dbb: 0xf0dbb, + 0xf0dbc: 0xf0dbc, + 0xf0dbd: 0xf0dbd, + 0xf0dbe: 0xf0dbe, + 0xf0dbf: 0xf0dbf, + 0xf0dc0: 0xf0dc0, + 0xf0dc1: 0xf0dc1, + 0xf0dc2: 0xf0dc2, + 0xf0dc3: 0xf0dc3, + 0xf0dc4: 0xf0dc4, + 0xf0dc5: 0xf0dc5, + 0xf0dc6: 0xf0dc6, + 0xf0dc7: 0xf0dc7, + 0xf0dc8: 0xf0dc8, + 0xf0dc9: 0xf0dc9, + 0xf0dca: 0xf0dca, + 0xf0dcb: 0xf0dcb, + 0xf0dcc: 0xf0dcc, + 0xf0dcd: 0xf0dcd, + 0xf0dce: 0xf0dce, + 0xf0dcf: 0xf0dcf, + 0xf0dd0: 0xf0dd0, + 0xf0dd1: 0xf0dd1, + 0xf0dd2: 0xf0dd2, + 0xf0dd3: 0xf0dd3, + 0xf0dd4: 0xf0dd4, + 0xf0dd5: 0xf0dd5, + 0xf0dd6: 0xf0dd6, + 0xf0dd7: 0xf0dd7, + 0xf0dd8: 0xf0dd8, + 0xf0dd9: 0xf0dd9, + 0xf0dda: 0xf0dda, + 0xf0ddb: 0xf0ddb, + 0xf0ddc: 0xf0ddc, + 0xf0ddd: 0xf0ddd, + 0xf0dde: 0xf0dde, + 0xf0ddf: 0xf0ddf, + 0xf0de0: 0xf0de0, + 0xf0de1: 0xf0de1, + 0xf0de2: 0xf0de2, + 0xf0de3: 0xf0de3, + 0xf0de4: 0xf0de4, + 0xf0de5: 0xf0de5, + 0xf0de6: 0xf0de6, + 0xf0de7: 0xf0de7, + 0xf0de8: 0xf0de8, + 0xf0de9: 0xf0de9, + 0xf0dea: 0xf0dea, + 0xf0deb: 0xf0deb, + 0xf0dec: 0xf0dec, + 0xf0ded: 0xf0ded, + 0xf0dee: 0xf0dee, + 0xf0def: 0xf0def, + 0xf0df0: 0xf0df0, + 0xf0df1: 0xf0df1, + 0xf0df2: 0xf0df2, + 0xf0df3: 0xf0df3, + 0xf0df4: 0xf0df4, + 0xf0df5: 0xf0df5, + 0xf0df6: 0xf0df6, + 0xf0df7: 0xf0df7, + 0xf0df8: 0xf0df8, + 0xf0df9: 0xf0df9, + 0xf0dfa: 0xf0dfa, + 0xf0dfb: 0xf0dfb, + 0xf0dfc: 0xf0dfc, + 0xf0dfd: 0xf0dfd, + 0xf0dfe: 0xf0dfe, + 0xf0dff: 0xf0dff, + 0xf0e00: 0xf0e00, + 0xf0e01: 0xf0e01, + 0xf0e02: 0xf0e02, + 0xf0e03: 0xf0e03, + 0xf0e04: 0xf0e04, + 0xf0e05: 0xf0e05, + 0xf0e06: 0xf0e06, + 0xf0e07: 0xf0e07, + 0xf0e08: 0xf0e08, + 0xf0e09: 0xf0e09, + 0xf0e0a: 0xf0e0a, + 0xf0e0b: 0xf0e0b, + 0xf0e0c: 0xf0e0c, + 0xf0e0d: 0xf0e0d, + 0xf0e0e: 0xf0e0e, + 0xf0e0f: 0xf0e0f, + 0xf0e10: 0xf0e10, + 0xf0e11: 0xf0e11, + 0xf0e12: 0xf0e12, + 0xf0e13: 0xf0e13, + 0xf0e14: 0xf0e14, + 0xf0e15: 0xf0e15, + 0xf0e16: 0xf0e16, + 0xf0e17: 0xf0e17, + 0xf0e18: 0xf0e18, + 0xf0e19: 0xf0e19, + 0xf0e1a: 0xf0e1a, + 0xf0e1b: 0xf0e1b, + 0xf0e1c: 0xf0e1c, + 0xf0e1d: 0xf0e1d, + 0xf0e1e: 0xf0e1e, + 0xf0e1f: 0xf0e1f, + 0xf0e20: 0xf0e20, + 0xf0e21: 0xf0e21, + 0xf0e22: 0xf0e22, + 0xf0e23: 0xf0e23, + 0xf0e24: 0xf0e24, + 0xf0e25: 0xf0e25, + 0xf0e26: 0xf0e26, + 0xf0e27: 0xf0e27, + 0xf0e28: 0xf0e28, + 0xf0e29: 0xf0e29, + 0xf0e2a: 0xf0e2a, + 0xf0e2b: 0xf0e2b, + 0xf0e2c: 0xf0e2c, + 0xf0e2d: 0xf0e2d, + 0xf0e2e: 0xf0e2e, + 0xf0e2f: 0xf0e2f, + 0xf0e30: 0xf0e30, + 0xf0e31: 0xf0e31, + 0xf0e32: 0xf0e32, + 0xf0e33: 0xf0e33, + 0xf0e34: 0xf0e34, + 0xf0e35: 0xf0e35, + 0xf0e36: 0xf0e36, + 0xf0e37: 0xf0e37, + 0xf0e38: 0xf0e38, + 0xf0e39: 0xf0e39, + 0xf0e3a: 0xf0e3a, + 0xf0e3b: 0xf0e3b, + 0xf0e3c: 0xf0e3c, + 0xf0e3d: 0xf0e3d, + 0xf0e3e: 0xf0e3e, + 0xf0e3f: 0xf0e3f, + 0xf0e40: 0xf0e40, + 0xf0e41: 0xf0e41, + 0xf0e42: 0xf0e42, + 0xf0e43: 0xf0e43, + 0xf0e44: 0xf0e44, + 0xf0e45: 0xf0e45, + 0xf0e46: 0xf0e46, + 0xf0e47: 0xf0e47, + 0xf0e48: 0xf0e48, + 0xf0e49: 0xf0e49, + 0xf0e4a: 0xf0e4a, + 0xf0e4b: 0xf0e4b, + 0xf0e4c: 0xf0e4c, + 0xf0e4d: 0xf0e4d, + 0xf0e4e: 0xf0e4e, + 0xf0e4f: 0xf0e4f, + 0xf0e50: 0xf0e50, + 0xf0e51: 0xf0e51, + 0xf0e52: 0xf0e52, + 0xf0e53: 0xf0e53, + 0xf0e54: 0xf0e54, + 0xf0e55: 0xf0e55, + 0xf0e56: 0xf0e56, + 0xf0e57: 0xf0e57, + 0xf0e58: 0xf0e58, + 0xf0e59: 0xf0e59, + 0xf0e5a: 0xf0e5a, + 0xf0e5b: 0xf0e5b, + 0xf0e5c: 0xf0e5c, + 0xf0e5d: 0xf0e5d, + 0xf0e5e: 0xf0e5e, + 0xf0e5f: 0xf0e5f, + 0xf0e60: 0xf0e60, + 0xf0e61: 0xf0e61, + 0xf0e62: 0xf0e62, + 0xf0e63: 0xf0e63, + 0xf0e64: 0xf0e64, + 0xf0e65: 0xf0e65, + 0xf0e66: 0xf0e66, + 0xf0e67: 0xf0e67, + 0xf0e68: 0xf0e68, + 0xf0e69: 0xf0e69, + 0xf0e6a: 0xf0e6a, + 0xf0e6b: 0xf0e6b, + 0xf0e6c: 0xf0e6c, + 0xf0e6d: 0xf0e6d, + 0xf0e6e: 0xf0e6e, + 0xf0e6f: 0xf0e6f, + 0xf0e70: 0xf0e70, + 0xf0e71: 0xf0e71, + 0xf0e72: 0xf0e72, + 0xf0e73: 0xf0e73, + 0xf0e74: 0xf0e74, + 0xf0e75: 0xf0e75, + 0xf0e76: 0xf0e76, + 0xf0e77: 0xf0e77, + 0xf0e78: 0xf0e78, + 0xf0e79: 0xf0e79, + 0xf0e7a: 0xf0e7a, + 0xf0e7b: 0xf0e7b, + 0xf0e7c: 0xf0e7c, + 0xf0e7d: 0xf0e7d, + 0xf0e7e: 0xf0e7e, + 0xf0e7f: 0xf0e7f, + 0xf0e80: 0xf0e80, + 0xf0e81: 0xf0e81, + 0xf0e82: 0xf0e82, + 0xf0e83: 0xf0e83, + 0xf0e84: 0xf0e84, + 0xf0e85: 0xf0e85, + 0xf0e86: 0xf0e86, + 0xf0e87: 0xf0e87, + 0xf0e88: 0xf0e88, + 0xf0e89: 0xf0e89, + 0xf0e8a: 0xf0e8a, + 0xf0e8b: 0xf0e8b, + 0xf0e8c: 0xf0e8c, + 0xf0e8d: 0xf0e8d, + 0xf0e8e: 0xf0e8e, + 0xf0e8f: 0xf0e8f, + 0xf0e90: 0xf0e90, + 0xf0e91: 0xf0e91, + 0xf0e92: 0xf0e92, + 0xf0e93: 0xf0e93, + 0xf0e94: 0xf0e94, + 0xf0e95: 0xf0e95, + 0xf0e96: 0xf0e96, + 0xf0e97: 0xf0e97, + 0xf0e98: 0xf0e98, + 0xf0e99: 0xf0e99, + 0xf0e9a: 0xf0e9a, + 0xf0e9b: 0xf0e9b, + 0xf0e9c: 0xf0e9c, + 0xf0e9d: 0xf0e9d, + 0xf0e9e: 0xf0e9e, + 0xf0e9f: 0xf0e9f, + 0xf0ea0: 0xf0ea0, + 0xf0ea1: 0xf0ea1, + 0xf0ea2: 0xf0ea2, + 0xf0ea3: 0xf0ea3, + 0xf0ea4: 0xf0ea4, + 0xf0ea5: 0xf0ea5, + 0xf0ea6: 0xf0ea6, + 0xf0ea7: 0xf0ea7, + 0xf0ea8: 0xf0ea8, + 0xf0ea9: 0xf0ea9, + 0xf0eaa: 0xf0eaa, + 0xf0eab: 0xf0eab, + 0xf0eac: 0xf0eac, + 0xf0ead: 0xf0ead, + 0xf0eae: 0xf0eae, + 0xf0eaf: 0xf0eaf, + 0xf0eb0: 0xf0eb0, + 0xf0eb1: 0xf0eb1, + 0xf0eb2: 0xf0eb2, + 0xf0eb3: 0xf0eb3, + 0xf0eb4: 0xf0eb4, + 0xf0eb5: 0xf0eb5, + 0xf0eb6: 0xf0eb6, + 0xf0eb7: 0xf0eb7, + 0xf0eb8: 0xf0eb8, + 0xf0eb9: 0xf0eb9, + 0xf0eba: 0xf0eba, + 0xf0ebb: 0xf0ebb, + 0xf0ebc: 0xf0ebc, + 0xf0ebd: 0xf0ebd, + 0xf0ebe: 0xf0ebe, + 0xf0ebf: 0xf0ebf, + 0xf0ec0: 0xf0ec0, + 0xf0ec1: 0xf0ec1, + 0xf0ec2: 0xf0ec2, + 0xf0ec3: 0xf0ec3, + 0xf0ec4: 0xf0ec4, + 0xf0ec5: 0xf0ec5, + 0xf0ec6: 0xf0ec6, + 0xf0ec7: 0xf0ec7, + 0xf0ec8: 0xf0ec8, + 0xf0ec9: 0xf0ec9, + 0xf0eca: 0xf0eca, + 0xf0ecb: 0xf0ecb, + 0xf0ecc: 0xf0ecc, + 0xf0ecd: 0xf0ecd, + 0xf0ece: 0xf0ece, + 0xf0ecf: 0xf0ecf, + 0xf0ed0: 0xf0ed0, + 0xf0ed1: 0xf0ed1, + 0xf0ed2: 0xf0ed2, + 0xf0ed3: 0xf0ed3, + 0xf0ed4: 0xf0ed4, + 0xf0ed5: 0xf0ed5, + 0xf0ed6: 0xf0ed6, + 0xf0ed7: 0xf0ed7, + 0xf0ed8: 0xf0ed8, + 0xf0ed9: 0xf0ed9, + 0xf0eda: 0xf0eda, + 0xf0edb: 0xf0edb, + 0xf0edc: 0xf0edc, + 0xf0edd: 0xf0edd, + 0xf0ede: 0xf0ede, + 0xf0edf: 0xf0edf, + 0xf0ee0: 0xf0ee0, + 0xf0ee1: 0xf0ee1, + 0xf0ee2: 0xf0ee2, + 0xf0ee3: 0xf0ee3, + 0xf0ee4: 0xf0ee4, + 0xf0ee5: 0xf0ee5, + 0xf0ee6: 0xf0ee6, + 0xf0ee7: 0xf0ee7, + 0xf0ee8: 0xf0ee8, + 0xf0ee9: 0xf0ee9, + 0xf0eea: 0xf0eea, + 0xf0eeb: 0xf0eeb, + 0xf0eec: 0xf0eec, + 0xf0eed: 0xf0eed, + 0xf0eee: 0xf0eee, + 0xf0eef: 0xf0eef, + 0xf0ef0: 0xf0ef0, + 0xf0ef1: 0xf0ef1, + 0xf0ef2: 0xf0ef2, + 0xf0ef3: 0xf0ef3, + 0xf0ef4: 0xf0ef4, + 0xf0ef5: 0xf0ef5, + 0xf0ef6: 0xf0ef6, + 0xf0ef7: 0xf0ef7, + 0xf0ef8: 0xf0ef8, + 0xf0ef9: 0xf0ef9, + 0xf0efa: 0xf0efa, + 0xf0efb: 0xf0efb, + 0xf0efc: 0xf0efc, + 0xf0efd: 0xf0efd, + 0xf0efe: 0xf0efe, + 0xf0eff: 0xf0eff, + 0xf0f00: 0xf0f00, + 0xf0f01: 0xf0f01, + 0xf0f02: 0xf0f02, + 0xf0f03: 0xf0f03, + 0xf0f04: 0xf0f04, + 0xf0f05: 0xf0f05, + 0xf0f06: 0xf0f06, + 0xf0f07: 0xf0f07, + 0xf0f08: 0xf0f08, + 0xf0f09: 0xf0f09, + 0xf0f0a: 0xf0f0a, + 0xf0f0b: 0xf0f0b, + 0xf0f0c: 0xf0f0c, + 0xf0f0d: 0xf0f0d, + 0xf0f0e: 0xf0f0e, + 0xf0f0f: 0xf0f0f, + 0xf0f10: 0xf0f10, + 0xf0f11: 0xf0f11, + 0xf0f12: 0xf0f12, + 0xf0f13: 0xf0f13, + 0xf0f14: 0xf0f14, + 0xf0f15: 0xf0f15, + 0xf0f16: 0xf0f16, + 0xf0f17: 0xf0f17, + 0xf0f18: 0xf0f18, + 0xf0f19: 0xf0f19, + 0xf0f1a: 0xf0f1a, + 0xf0f1b: 0xf0f1b, + 0xf0f1c: 0xf0f1c, + 0xf0f1d: 0xf0f1d, + 0xf0f1e: 0xf0f1e, + 0xf0f1f: 0xf0f1f, + 0xf0f20: 0xf0f20, + 0xf0f21: 0xf0f21, + 0xf0f22: 0xf0f22, + 0xf0f23: 0xf0f23, + 0xf0f24: 0xf0f24, + 0xf0f25: 0xf0f25, + 0xf0f26: 0xf0f26, + 0xf0f27: 0xf0f27, + 0xf0f28: 0xf0f28, + 0xf0f29: 0xf0f29, + 0xf0f2a: 0xf0f2a, + 0xf0f2b: 0xf0f2b, + 0xf0f2c: 0xf0f2c, + 0xf0f2d: 0xf0f2d, + 0xf0f2e: 0xf0f2e, + 0xf0f2f: 0xf0f2f, + 0xf0f30: 0xf0f30, + 0xf0f31: 0xf0f31, + 0xf0f32: 0xf0f32, + 0xf0f33: 0xf0f33, + 0xf0f34: 0xf0f34, + 0xf0f35: 0xf0f35, + 0xf0f36: 0xf0f36, + 0xf0f37: 0xf0f37, + 0xf0f38: 0xf0f38, + 0xf0f39: 0xf0f39, + 0xf0f3a: 0xf0f3a, + 0xf0f3b: 0xf0f3b, + 0xf0f3c: 0xf0f3c, + 0xf0f3d: 0xf0f3d, + 0xf0f3e: 0xf0f3e, + 0xf0f3f: 0xf0f3f, + 0xf0f40: 0xf0f40, + 0xf0f41: 0xf0f41, + 0xf0f42: 0xf0f42, + 0xf0f43: 0xf0f43, + 0xf0f44: 0xf0f44, + 0xf0f45: 0xf0f45, + 0xf0f46: 0xf0f46, + 0xf0f47: 0xf0f47, + 0xf0f48: 0xf0f48, + 0xf0f49: 0xf0f49, + 0xf0f4a: 0xf0f4a, + 0xf0f4b: 0xf0f4b, + 0xf0f4c: 0xf0f4c, + 0xf0f4d: 0xf0f4d, + 0xf0f4e: 0xf0f4e, + 0xf0f4f: 0xf0f4f, + 0xf0f50: 0xf0f50, + 0xf0f51: 0xf0f51, + 0xf0f52: 0xf0f52, + 0xf0f53: 0xf0f53, + 0xf0f54: 0xf0f54, + 0xf0f55: 0xf0f55, + 0xf0f56: 0xf0f56, + 0xf0f57: 0xf0f57, + 0xf0f58: 0xf0f58, + 0xf0f59: 0xf0f59, + 0xf0f5a: 0xf0f5a, + 0xf0f5b: 0xf0f5b, + 0xf0f5c: 0xf0f5c, + 0xf0f5d: 0xf0f5d, + 0xf0f5e: 0xf0f5e, + 0xf0f5f: 0xf0f5f, + 0xf0f60: 0xf0f60, + 0xf0f61: 0xf0f61, + 0xf0f62: 0xf0f62, + 0xf0f63: 0xf0f63, + 0xf0f64: 0xf0f64, + 0xf0f65: 0xf0f65, + 0xf0f66: 0xf0f66, + 0xf0f67: 0xf0f67, + 0xf0f68: 0xf0f68, + 0xf0f69: 0xf0f69, + 0xf0f6a: 0xf0f6a, + 0xf0f6b: 0xf0f6b, + 0xf0f6c: 0xf0f6c, + 0xf0f6d: 0xf0f6d, + 0xf0f6e: 0xf0f6e, + 0xf0f6f: 0xf0f6f, + 0xf0f70: 0xf0f70, + 0xf0f71: 0xf0f71, + 0xf0f72: 0xf0f72, + 0xf0f73: 0xf0f73, + 0xf0f74: 0xf0f74, + 0xf0f75: 0xf0f75, + 0xf0f76: 0xf0f76, + 0xf0f77: 0xf0f77, + 0xf0f78: 0xf0f78, + 0xf0f79: 0xf0f79, + 0xf0f7a: 0xf0f7a, + 0xf0f7b: 0xf0f7b, + 0xf0f7c: 0xf0f7c, + 0xf0f7d: 0xf0f7d, + 0xf0f7e: 0xf0f7e, + 0xf0f7f: 0xf0f7f, + 0xf0f80: 0xf0f80, + 0xf0f81: 0xf0f81, + 0xf0f82: 0xf0f82, + 0xf0f83: 0xf0f83, + 0xf0f84: 0xf0f84, + 0xf0f85: 0xf0f85, + 0xf0f86: 0xf0f86, + 0xf0f87: 0xf0f87, + 0xf0f88: 0xf0f88, + 0xf0f89: 0xf0f89, + 0xf0f8a: 0xf0f8a, + 0xf0f8b: 0xf0f8b, + 0xf0f8c: 0xf0f8c, + 0xf0f8d: 0xf0f8d, + 0xf0f8e: 0xf0f8e, + 0xf0f8f: 0xf0f8f, + 0xf0f90: 0xf0f90, + 0xf0f91: 0xf0f91, + 0xf0f92: 0xf0f92, + 0xf0f93: 0xf0f93, + 0xf0f94: 0xf0f94, + 0xf0f95: 0xf0f95, + 0xf0f96: 0xf0f96, + 0xf0f97: 0xf0f97, + 0xf0f98: 0xf0f98, + 0xf0f99: 0xf0f99, + 0xf0f9a: 0xf0f9a, + 0xf0f9b: 0xf0f9b, + 0xf0f9c: 0xf0f9c, + 0xf0f9d: 0xf0f9d, + 0xf0f9e: 0xf0f9e, + 0xf0f9f: 0xf0f9f, + 0xf0fa0: 0xf0fa0, + 0xf0fa1: 0xf0fa1, + 0xf0fa2: 0xf0fa2, + 0xf0fa3: 0xf0fa3, + 0xf0fa4: 0xf0fa4, + 0xf0fa5: 0xf0fa5, + 0xf0fa6: 0xf0fa6, + 0xf0fa7: 0xf0fa7, + 0xf0fa8: 0xf0fa8, + 0xf0fa9: 0xf0fa9, + 0xf0faa: 0xf0faa, + 0xf0fab: 0xf0fab, + 0xf0fac: 0xf0fac, + 0xf0fad: 0xf0fad, + 0xf0fae: 0xf0fae, + 0xf0faf: 0xf0faf, + 0xf0fb0: 0xf0fb0, + 0xf0fb1: 0xf0fb1, + 0xf0fb2: 0xf0fb2, + 0xf0fb3: 0xf0fb3, + 0xf0fb4: 0xf0fb4, + 0xf0fb5: 0xf0fb5, + 0xf0fb6: 0xf0fb6, + 0xf0fb7: 0xf0fb7, + 0xf0fb8: 0xf0fb8, + 0xf0fb9: 0xf0fb9, + 0xf0fba: 0xf0fba, + 0xf0fbb: 0xf0fbb, + 0xf0fbc: 0xf0fbc, + 0xf0fbd: 0xf0fbd, + 0xf0fbe: 0xf0fbe, + 0xf0fbf: 0xf0fbf, + 0xf0fc0: 0xf0fc0, + 0xf0fc1: 0xf0fc1, + 0xf0fc2: 0xf0fc2, + 0xf0fc3: 0xf0fc3, + 0xf0fc4: 0xf0fc4, + 0xf0fc5: 0xf0fc5, + 0xf0fc6: 0xf0fc6, + 0xf0fc7: 0xf0fc7, + 0xf0fc8: 0xf0fc8, + 0xf0fc9: 0xf0fc9, + 0xf0fca: 0xf0fca, + 0xf0fcb: 0xf0fcb, + 0xf0fcc: 0xf0fcc, + 0xf0fcd: 0xf0fcd, + 0xf0fce: 0xf0fce, + 0xf0fcf: 0xf0fcf, + 0xf0fd0: 0xf0fd0, + 0xf0fd1: 0xf0fd1, + 0xf0fd2: 0xf0fd2, + 0xf0fd3: 0xf0fd3, + 0xf0fd4: 0xf0fd4, + 0xf0fd5: 0xf0fd5, + 0xf0fd6: 0xf0fd6, + 0xf0fd7: 0xf0fd7, + 0xf0fd8: 0xf0fd8, + 0xf0fd9: 0xf0fd9, + 0xf0fda: 0xf0fda, + 0xf0fdb: 0xf0fdb, + 0xf0fdc: 0xf0fdc, + 0xf0fdd: 0xf0fdd, + 0xf0fde: 0xf0fde, + 0xf0fdf: 0xf0fdf, + 0xf0fe0: 0xf0fe0, + 0xf0fe1: 0xf0fe1, + 0xf0fe2: 0xf0fe2, + 0xf0fe3: 0xf0fe3, + 0xf0fe4: 0xf0fe4, + 0xf0fe5: 0xf0fe5, + 0xf0fe6: 0xf0fe6, + 0xf0fe7: 0xf0fe7, + 0xf0fe8: 0xf0fe8, + 0xf0fe9: 0xf0fe9, + 0xf0fea: 0xf0fea, + 0xf0feb: 0xf0feb, + 0xf0fec: 0xf0fec, + 0xf0fed: 0xf0fed, + 0xf0fee: 0xf0fee, + 0xf0fef: 0xf0fef, + 0xf0ff0: 0xf0ff0, + 0xf0ff1: 0xf0ff1, + 0xf0ff2: 0xf0ff2, + 0xf0ff3: 0xf0ff3, + 0xf0ff4: 0xf0ff4, + 0xf0ff5: 0xf0ff5, + 0xf0ff6: 0xf0ff6, + 0xf0ff7: 0xf0ff7, + 0xf0ff8: 0xf0ff8, + 0xf0ff9: 0xf0ff9, + 0xf0ffa: 0xf0ffa, + 0xf0ffb: 0xf0ffb, + 0xf0ffc: 0xf0ffc, + 0xf0ffd: 0xf0ffd, + 0xf0ffe: 0xf0ffe, + 0xf0fff: 0xf0fff, + 0xf1000: 0xf1000, + 0xf1001: 0xf1001, + 0xf1002: 0xf1002, + 0xf1003: 0xf1003, + 0xf1004: 0xf1004, + 0xf1005: 0xf1005, + 0xf1006: 0xf1006, + 0xf1007: 0xf1007, + 0xf1008: 0xf1008, + 0xf1009: 0xf1009, + 0xf100a: 0xf100a, + 0xf100b: 0xf100b, + 0xf100c: 0xf100c, + 0xf100d: 0xf100d, + 0xf100e: 0xf100e, + 0xf100f: 0xf100f, + 0xf1010: 0xf1010, + 0xf1011: 0xf1011, + 0xf1012: 0xf1012, + 0xf1013: 0xf1013, + 0xf1014: 0xf1014, + 0xf1015: 0xf1015, + 0xf1016: 0xf1016, + 0xf1017: 0xf1017, + 0xf1018: 0xf1018, + 0xf1019: 0xf1019, + 0xf101a: 0xf101a, + 0xf101b: 0xf101b, + 0xf101c: 0xf101c, + 0xf101d: 0xf101d, + 0xf101e: 0xf101e, + 0xf101f: 0xf101f, + 0xf1020: 0xf1020, + 0xf1021: 0xf1021, + 0xf1022: 0xf1022, + 0xf1023: 0xf1023, + 0xf1024: 0xf1024, + 0xf1025: 0xf1025, + 0xf1026: 0xf1026, + 0xf1027: 0xf1027, + 0xf1028: 0xf1028, + 0xf1029: 0xf1029, + 0xf102a: 0xf102a, + 0xf102b: 0xf102b, + 0xf102c: 0xf102c, + 0xf102d: 0xf102d, + 0xf102e: 0xf102e, + 0xf102f: 0xf102f, + 0xf1030: 0xf1030, + 0xf1031: 0xf1031, + 0xf1032: 0xf1032, + 0xf1033: 0xf1033, + 0xf1034: 0xf1034, + 0xf1035: 0xf1035, + 0xf1036: 0xf1036, + 0xf1037: 0xf1037, + 0xf1038: 0xf1038, + 0xf1039: 0xf1039, + 0xf103a: 0xf103a, + 0xf103b: 0xf103b, + 0xf103c: 0xf103c, + 0xf103d: 0xf103d, + 0xf103e: 0xf103e, + 0xf103f: 0xf103f, + 0xf1040: 0xf1040, + 0xf1041: 0xf1041, + 0xf1042: 0xf1042, + 0xf1043: 0xf1043, + 0xf1044: 0xf1044, + 0xf1045: 0xf1045, + 0xf1046: 0xf1046, + 0xf1047: 0xf1047, + 0xf1048: 0xf1048, + 0xf1049: 0xf1049, + 0xf104a: 0xf104a, + 0xf104b: 0xf104b, + 0xf104c: 0xf104c, + 0xf104d: 0xf104d, + 0xf104e: 0xf104e, + 0xf104f: 0xf104f, + 0xf1050: 0xf1050, + 0xf1051: 0xf1051, + 0xf1052: 0xf1052, + 0xf1053: 0xf1053, + 0xf1054: 0xf1054, + 0xf1055: 0xf1055, + 0xf1056: 0xf1056, + 0xf1057: 0xf1057, + 0xf1058: 0xf1058, + 0xf1059: 0xf1059, + 0xf105a: 0xf105a, + 0xf105b: 0xf105b, + 0xf105c: 0xf105c, + 0xf105d: 0xf105d, + 0xf105e: 0xf105e, + 0xf105f: 0xf105f, + 0xf1060: 0xf1060, + 0xf1061: 0xf1061, + 0xf1062: 0xf1062, + 0xf1063: 0xf1063, + 0xf1064: 0xf1064, + 0xf1065: 0xf1065, + 0xf1066: 0xf1066, + 0xf1067: 0xf1067, + 0xf1068: 0xf1068, + 0xf1069: 0xf1069, + 0xf106a: 0xf106a, + 0xf106b: 0xf106b, + 0xf106c: 0xf106c, + 0xf106d: 0xf106d, + 0xf106e: 0xf106e, + 0xf106f: 0xf106f, + 0xf1070: 0xf1070, + 0xf1071: 0xf1071, + 0xf1072: 0xf1072, + 0xf1073: 0xf1073, + 0xf1074: 0xf1074, + 0xf1075: 0xf1075, + 0xf1076: 0xf1076, + 0xf1077: 0xf1077, + 0xf1078: 0xf1078, + 0xf1079: 0xf1079, + 0xf107a: 0xf107a, + 0xf107b: 0xf107b, + 0xf107c: 0xf107c, + 0xf107d: 0xf107d, + 0xf107e: 0xf107e, + 0xf107f: 0xf107f, + 0xf1080: 0xf1080, + 0xf1081: 0xf1081, + 0xf1082: 0xf1082, + 0xf1083: 0xf1083, + 0xf1084: 0xf1084, + 0xf1085: 0xf1085, + 0xf1086: 0xf1086, + 0xf1087: 0xf1087, + 0xf1088: 0xf1088, + 0xf1089: 0xf1089, + 0xf108a: 0xf108a, + 0xf108b: 0xf108b, + 0xf108c: 0xf108c, + 0xf108d: 0xf108d, + 0xf108e: 0xf108e, + 0xf108f: 0xf108f, + 0xf1090: 0xf1090, + 0xf1091: 0xf1091, + 0xf1092: 0xf1092, + 0xf1093: 0xf1093, + 0xf1094: 0xf1094, + 0xf1095: 0xf1095, + 0xf1096: 0xf1096, + 0xf1097: 0xf1097, + 0xf1098: 0xf1098, + 0xf1099: 0xf1099, + 0xf109a: 0xf109a, + 0xf109b: 0xf109b, + 0xf109c: 0xf109c, + 0xf109d: 0xf109d, + 0xf109e: 0xf109e, + 0xf109f: 0xf109f, + 0xf10a0: 0xf10a0, + 0xf10a1: 0xf10a1, + 0xf10a2: 0xf10a2, + 0xf10a3: 0xf10a3, + 0xf10a4: 0xf10a4, + 0xf10a5: 0xf10a5, + 0xf10a6: 0xf10a6, + 0xf10a7: 0xf10a7, + 0xf10a8: 0xf10a8, + 0xf10a9: 0xf10a9, + 0xf10aa: 0xf10aa, + 0xf10ab: 0xf10ab, + 0xf10ac: 0xf10ac, + 0xf10ad: 0xf10ad, + 0xf10ae: 0xf10ae, + 0xf10af: 0xf10af, + 0xf10b0: 0xf10b0, + 0xf10b1: 0xf10b1, + 0xf10b2: 0xf10b2, + 0xf10b3: 0xf10b3, + 0xf10b4: 0xf10b4, + 0xf10b5: 0xf10b5, + 0xf10b6: 0xf10b6, + 0xf10b7: 0xf10b7, + 0xf10b8: 0xf10b8, + 0xf10b9: 0xf10b9, + 0xf10ba: 0xf10ba, + 0xf10bb: 0xf10bb, + 0xf10bc: 0xf10bc, + 0xf10bd: 0xf10bd, + 0xf10be: 0xf10be, + 0xf10bf: 0xf10bf, + 0xf10c0: 0xf10c0, + 0xf10c1: 0xf10c1, + 0xf10c2: 0xf10c2, + 0xf10c3: 0xf10c3, + 0xf10c4: 0xf10c4, + 0xf10c5: 0xf10c5, + 0xf10c6: 0xf10c6, + 0xf10c7: 0xf10c7, + 0xf10c8: 0xf10c8, + 0xf10c9: 0xf10c9, + 0xf10ca: 0xf10ca, + 0xf10cb: 0xf10cb, + 0xf10cc: 0xf10cc, + 0xf10cd: 0xf10cd, + 0xf10ce: 0xf10ce, + 0xf10cf: 0xf10cf, + 0xf10d0: 0xf10d0, + 0xf10d1: 0xf10d1, + 0xf10d2: 0xf10d2, + 0xf10d3: 0xf10d3, + 0xf10d4: 0xf10d4, + 0xf10d5: 0xf10d5, + 0xf10d6: 0xf10d6, + 0xf10d7: 0xf10d7, + 0xf10d8: 0xf10d8, + 0xf10d9: 0xf10d9, + 0xf10da: 0xf10da, + 0xf10db: 0xf10db, + 0xf10dc: 0xf10dc, + 0xf10dd: 0xf10dd, + 0xf10de: 0xf10de, + 0xf10df: 0xf10df, + 0xf10e0: 0xf10e0, + 0xf10e1: 0xf10e1, + 0xf10e2: 0xf10e2, + 0xf10e3: 0xf10e3, + 0xf10e4: 0xf10e4, + 0xf10e5: 0xf10e5, + 0xf10e6: 0xf10e6, + 0xf10e7: 0xf10e7, + 0xf10e8: 0xf10e8, + 0xf10e9: 0xf10e9, + 0xf10ea: 0xf10ea, + 0xf10eb: 0xf10eb, + 0xf10ec: 0xf10ec, + 0xf10ed: 0xf10ed, + 0xf10ee: 0xf10ee, + 0xf10ef: 0xf10ef, + 0xf10f0: 0xf10f0, + 0xf10f1: 0xf10f1, + 0xf10f2: 0xf10f2, + 0xf10f3: 0xf10f3, + 0xf10f4: 0xf10f4, + 0xf10f5: 0xf10f5, + 0xf10f6: 0xf10f6, + 0xf10f7: 0xf10f7, + 0xf10f8: 0xf10f8, + 0xf10f9: 0xf10f9, + 0xf10fa: 0xf10fa, + 0xf10fb: 0xf10fb, + 0xf10fc: 0xf10fc, + 0xf10fd: 0xf10fd, + 0xf10fe: 0xf10fe, + 0xf10ff: 0xf10ff, + 0xf1100: 0xf1100, + 0xf1101: 0xf1101, + 0xf1102: 0xf1102, + 0xf1103: 0xf1103, + 0xf1104: 0xf1104, + 0xf1105: 0xf1105, + 0xf1106: 0xf1106, + 0xf1107: 0xf1107, + 0xf1108: 0xf1108, + 0xf1109: 0xf1109, + 0xf110a: 0xf110a, + 0xf110b: 0xf110b, + 0xf110c: 0xf110c, + 0xf110d: 0xf110d, + 0xf110e: 0xf110e, + 0xf110f: 0xf110f, + 0xf1110: 0xf1110, + 0xf1111: 0xf1111, + 0xf1112: 0xf1112, + 0xf1113: 0xf1113, + 0xf1114: 0xf1114, + 0xf1115: 0xf1115, + 0xf1116: 0xf1116, + 0xf1117: 0xf1117, + 0xf1118: 0xf1118, + 0xf1119: 0xf1119, + 0xf111a: 0xf111a, + 0xf111b: 0xf111b, + 0xf111c: 0xf111c, + 0xf111d: 0xf111d, + 0xf111e: 0xf111e, + 0xf111f: 0xf111f, + 0xf1120: 0xf1120, + 0xf1121: 0xf1121, + 0xf1122: 0xf1122, + 0xf1123: 0xf1123, + 0xf1124: 0xf1124, + 0xf1125: 0xf1125, + 0xf1126: 0xf1126, + 0xf1127: 0xf1127, + 0xf1128: 0xf1128, + 0xf1129: 0xf1129, + 0xf112a: 0xf112a, + 0xf112b: 0xf112b, + 0xf112c: 0xf112c, + 0xf112d: 0xf112d, + 0xf112e: 0xf112e, + 0xf112f: 0xf112f, + 0xf1130: 0xf1130, + 0xf1131: 0xf1131, + 0xf1132: 0xf1132, + 0xf1133: 0xf1133, + 0xf1134: 0xf1134, + 0xf1135: 0xf1135, + 0xf1136: 0xf1136, + 0xf1137: 0xf1137, + 0xf1138: 0xf1138, + 0xf1139: 0xf1139, + 0xf113a: 0xf113a, + 0xf113b: 0xf113b, + 0xf113c: 0xf113c, + 0xf113d: 0xf113d, + 0xf113e: 0xf113e, + 0xf113f: 0xf113f, + 0xf1140: 0xf1140, + 0xf1141: 0xf1141, + 0xf1142: 0xf1142, + 0xf1143: 0xf1143, + 0xf1144: 0xf1144, + 0xf1145: 0xf1145, + 0xf1146: 0xf1146, + 0xf1147: 0xf1147, + 0xf1148: 0xf1148, + 0xf1149: 0xf1149, + 0xf114a: 0xf114a, + 0xf114b: 0xf114b, + 0xf114c: 0xf114c, + 0xf114d: 0xf114d, + 0xf114e: 0xf114e, + 0xf114f: 0xf114f, + 0xf1150: 0xf1150, + 0xf1151: 0xf1151, + 0xf1152: 0xf1152, + 0xf1153: 0xf1153, + 0xf1154: 0xf1154, + 0xf1155: 0xf1155, + 0xf1156: 0xf1156, + 0xf1157: 0xf1157, + 0xf1158: 0xf1158, + 0xf1159: 0xf1159, + 0xf115a: 0xf115a, + 0xf115b: 0xf115b, + 0xf115c: 0xf115c, + 0xf115d: 0xf115d, + 0xf115e: 0xf115e, + 0xf115f: 0xf115f, + 0xf1160: 0xf1160, + 0xf1161: 0xf1161, + 0xf1162: 0xf1162, + 0xf1163: 0xf1163, + 0xf1164: 0xf1164, + 0xf1165: 0xf1165, + 0xf1166: 0xf1166, + 0xf1167: 0xf1167, + 0xf1168: 0xf1168, + 0xf1169: 0xf1169, + 0xf116a: 0xf116a, + 0xf116b: 0xf116b, + 0xf116c: 0xf116c, + 0xf116d: 0xf116d, + 0xf116e: 0xf116e, + 0xf116f: 0xf116f, + 0xf1170: 0xf1170, + 0xf1171: 0xf1171, + 0xf1172: 0xf1172, + 0xf1173: 0xf1173, + 0xf1174: 0xf1174, + 0xf1175: 0xf1175, + 0xf1176: 0xf1176, + 0xf1177: 0xf1177, + 0xf1178: 0xf1178, + 0xf1179: 0xf1179, + 0xf117a: 0xf117a, + 0xf117b: 0xf117b, + 0xf117c: 0xf117c, + 0xf117d: 0xf117d, + 0xf117e: 0xf117e, + 0xf117f: 0xf117f, + 0xf1180: 0xf1180, + 0xf1181: 0xf1181, + 0xf1182: 0xf1182, + 0xf1183: 0xf1183, + 0xf1184: 0xf1184, + 0xf1185: 0xf1185, + 0xf1186: 0xf1186, + 0xf1187: 0xf1187, + 0xf1188: 0xf1188, + 0xf1189: 0xf1189, + 0xf118a: 0xf118a, + 0xf118b: 0xf118b, + 0xf118c: 0xf118c, + 0xf118d: 0xf118d, + 0xf118e: 0xf118e, + 0xf118f: 0xf118f, + 0xf1190: 0xf1190, + 0xf1191: 0xf1191, + 0xf1192: 0xf1192, + 0xf1193: 0xf1193, + 0xf1194: 0xf1194, + 0xf1195: 0xf1195, + 0xf1196: 0xf1196, + 0xf1197: 0xf1197, + 0xf1198: 0xf1198, + 0xf1199: 0xf1199, + 0xf119a: 0xf119a, + 0xf119b: 0xf119b, + 0xf119c: 0xf119c, + 0xf119d: 0xf119d, + 0xf119e: 0xf119e, + 0xf119f: 0xf119f, + 0xf11a0: 0xf11a0, + 0xf11a1: 0xf11a1, + 0xf11a2: 0xf11a2, + 0xf11a3: 0xf11a3, + 0xf11a4: 0xf11a4, + 0xf11a5: 0xf11a5, + 0xf11a6: 0xf11a6, + 0xf11a7: 0xf11a7, + 0xf11a8: 0xf11a8, + 0xf11a9: 0xf11a9, + 0xf11aa: 0xf11aa, + 0xf11ab: 0xf11ab, + 0xf11ac: 0xf11ac, + 0xf11ad: 0xf11ad, + 0xf11ae: 0xf11ae, + 0xf11af: 0xf11af, + 0xf11b0: 0xf11b0, + 0xf11b1: 0xf11b1, + 0xf11b2: 0xf11b2, + 0xf11b3: 0xf11b3, + 0xf11b4: 0xf11b4, + 0xf11b5: 0xf11b5, + 0xf11b6: 0xf11b6, + 0xf11b7: 0xf11b7, + 0xf11b8: 0xf11b8, + 0xf11b9: 0xf11b9, + 0xf11ba: 0xf11ba, + 0xf11bb: 0xf11bb, + 0xf11bc: 0xf11bc, + 0xf11bd: 0xf11bd, + 0xf11be: 0xf11be, + 0xf11bf: 0xf11bf, + 0xf11c0: 0xf11c0, + 0xf11c1: 0xf11c1, + 0xf11c2: 0xf11c2, + 0xf11c3: 0xf11c3, + 0xf11c4: 0xf11c4, + 0xf11c5: 0xf11c5, + 0xf11c6: 0xf11c6, + 0xf11c7: 0xf11c7, + 0xf11c8: 0xf11c8, + 0xf11c9: 0xf11c9, + 0xf11ca: 0xf11ca, + 0xf11cb: 0xf11cb, + 0xf11cc: 0xf11cc, + 0xf11cd: 0xf11cd, + 0xf11ce: 0xf11ce, + 0xf11cf: 0xf11cf, + 0xf11d0: 0xf11d0, + 0xf11d1: 0xf11d1, + 0xf11d2: 0xf11d2, + 0xf11d3: 0xf11d3, + 0xf11d4: 0xf11d4, + 0xf11d5: 0xf11d5, + 0xf11d6: 0xf11d6, + 0xf11d7: 0xf11d7, + 0xf11d8: 0xf11d8, + 0xf11d9: 0xf11d9, + 0xf11da: 0xf11da, + 0xf11db: 0xf11db, + 0xf11dc: 0xf11dc, + 0xf11dd: 0xf11dd, + 0xf11de: 0xf11de, + 0xf11df: 0xf11df, + 0xf11e0: 0xf11e0, + 0xf11e1: 0xf11e1, + 0xf11e2: 0xf11e2, + 0xf11e3: 0xf11e3, + 0xf11e4: 0xf11e4, + 0xf11e5: 0xf11e5, + 0xf11e6: 0xf11e6, + 0xf11e7: 0xf11e7, + 0xf11e8: 0xf11e8, + 0xf11e9: 0xf11e9, + 0xf11ea: 0xf11ea, + 0xf11eb: 0xf11eb, + 0xf11ec: 0xf11ec, + 0xf11ed: 0xf11ed, + 0xf11ee: 0xf11ee, + 0xf11ef: 0xf11ef, + 0xf11f0: 0xf11f0, + 0xf11f1: 0xf11f1, + 0xf11f2: 0xf11f2, + 0xf11f3: 0xf11f3, + 0xf11f4: 0xf11f4, + 0xf11f5: 0xf11f5, + 0xf11f6: 0xf11f6, + 0xf11f7: 0xf11f7, + 0xf11f8: 0xf11f8, + 0xf11f9: 0xf11f9, + 0xf11fa: 0xf11fa, + 0xf11fb: 0xf11fb, + 0xf11fc: 0xf11fc, + 0xf11fd: 0xf11fd, + 0xf11fe: 0xf11fe, + 0xf11ff: 0xf11ff, + 0xf1200: 0xf1200, + 0xf1201: 0xf1201, + 0xf1202: 0xf1202, + 0xf1203: 0xf1203, + 0xf1204: 0xf1204, + 0xf1205: 0xf1205, + 0xf1206: 0xf1206, + 0xf1207: 0xf1207, + 0xf1208: 0xf1208, + 0xf1209: 0xf1209, + 0xf120a: 0xf120a, + 0xf120b: 0xf120b, + 0xf120c: 0xf120c, + 0xf120d: 0xf120d, + 0xf120e: 0xf120e, + 0xf120f: 0xf120f, + 0xf1210: 0xf1210, + 0xf1211: 0xf1211, + 0xf1212: 0xf1212, + 0xf1213: 0xf1213, + 0xf1214: 0xf1214, + 0xf1215: 0xf1215, + 0xf1216: 0xf1216, + 0xf1217: 0xf1217, + 0xf1218: 0xf1218, + 0xf1219: 0xf1219, + 0xf121a: 0xf121a, + 0xf121b: 0xf121b, + 0xf121c: 0xf121c, + 0xf121d: 0xf121d, + 0xf121e: 0xf121e, + 0xf121f: 0xf121f, + 0xf1220: 0xf1220, + 0xf1221: 0xf1221, + 0xf1222: 0xf1222, + 0xf1223: 0xf1223, + 0xf1224: 0xf1224, + 0xf1225: 0xf1225, + 0xf1226: 0xf1226, + 0xf1227: 0xf1227, + 0xf1228: 0xf1228, + 0xf1229: 0xf1229, + 0xf122a: 0xf122a, + 0xf122b: 0xf122b, + 0xf122c: 0xf122c, + 0xf122d: 0xf122d, + 0xf122e: 0xf122e, + 0xf122f: 0xf122f, + 0xf1230: 0xf1230, + 0xf1231: 0xf1231, + 0xf1232: 0xf1232, + 0xf1233: 0xf1233, + 0xf1234: 0xf1234, + 0xf1235: 0xf1235, + 0xf1236: 0xf1236, + 0xf1237: 0xf1237, + 0xf1238: 0xf1238, + 0xf1239: 0xf1239, + 0xf123a: 0xf123a, + 0xf123b: 0xf123b, + 0xf123c: 0xf123c, + 0xf123d: 0xf123d, + 0xf123e: 0xf123e, + 0xf123f: 0xf123f, + 0xf1240: 0xf1240, + 0xf1241: 0xf1241, + 0xf1242: 0xf1242, + 0xf1243: 0xf1243, + 0xf1244: 0xf1244, + 0xf1245: 0xf1245, + 0xf1246: 0xf1246, + 0xf1247: 0xf1247, + 0xf1248: 0xf1248, + 0xf1249: 0xf1249, + 0xf124a: 0xf124a, + 0xf124b: 0xf124b, + 0xf124c: 0xf124c, + 0xf124d: 0xf124d, + 0xf124e: 0xf124e, + 0xf124f: 0xf124f, + 0xf1250: 0xf1250, + 0xf1251: 0xf1251, + 0xf1252: 0xf1252, + 0xf1253: 0xf1253, + 0xf1254: 0xf1254, + 0xf1255: 0xf1255, + 0xf1256: 0xf1256, + 0xf1257: 0xf1257, + 0xf1258: 0xf1258, + 0xf1259: 0xf1259, + 0xf125a: 0xf125a, + 0xf125b: 0xf125b, + 0xf125c: 0xf125c, + 0xf125d: 0xf125d, + 0xf125e: 0xf125e, + 0xf125f: 0xf125f, + 0xf1260: 0xf1260, + 0xf1261: 0xf1261, + 0xf1262: 0xf1262, + 0xf1263: 0xf1263, + 0xf1264: 0xf1264, + 0xf1265: 0xf1265, + 0xf1266: 0xf1266, + 0xf1267: 0xf1267, + 0xf1268: 0xf1268, + 0xf1269: 0xf1269, + 0xf126a: 0xf126a, + 0xf126b: 0xf126b, + 0xf126c: 0xf126c, + 0xf126d: 0xf126d, + 0xf126e: 0xf126e, + 0xf126f: 0xf126f, + 0xf1270: 0xf1270, + 0xf1271: 0xf1271, + 0xf1272: 0xf1272, + 0xf1273: 0xf1273, + 0xf1274: 0xf1274, + 0xf1275: 0xf1275, + 0xf1276: 0xf1276, + 0xf1277: 0xf1277, + 0xf1278: 0xf1278, + 0xf1279: 0xf1279, + 0xf127a: 0xf127a, + 0xf127b: 0xf127b, + 0xf127c: 0xf127c, + 0xf127d: 0xf127d, + 0xf127e: 0xf127e, + 0xf127f: 0xf127f, + 0xf1280: 0xf1280, + 0xf1281: 0xf1281, + 0xf1282: 0xf1282, + 0xf1283: 0xf1283, + 0xf1284: 0xf1284, + 0xf1285: 0xf1285, + 0xf1286: 0xf1286, + 0xf1287: 0xf1287, + 0xf1288: 0xf1288, + 0xf1289: 0xf1289, + 0xf128a: 0xf128a, + 0xf128b: 0xf128b, + 0xf128c: 0xf128c, + 0xf128d: 0xf128d, + 0xf128e: 0xf128e, + 0xf128f: 0xf128f, + 0xf1290: 0xf1290, + 0xf1291: 0xf1291, + 0xf1292: 0xf1292, + 0xf1293: 0xf1293, + 0xf1294: 0xf1294, + 0xf1295: 0xf1295, + 0xf1296: 0xf1296, + 0xf1297: 0xf1297, + 0xf1298: 0xf1298, + 0xf1299: 0xf1299, + 0xf129a: 0xf129a, + 0xf129b: 0xf129b, + 0xf129c: 0xf129c, + 0xf129d: 0xf129d, + 0xf129e: 0xf129e, + 0xf129f: 0xf129f, + 0xf12a0: 0xf12a0, + 0xf12a1: 0xf12a1, + 0xf12a2: 0xf12a2, + 0xf12a3: 0xf12a3, + 0xf12a4: 0xf12a4, + 0xf12a5: 0xf12a5, + 0xf12a6: 0xf12a6, + 0xf12a7: 0xf12a7, + 0xf12a8: 0xf12a8, + 0xf12a9: 0xf12a9, + 0xf12aa: 0xf12aa, + 0xf12ab: 0xf12ab, + 0xf12ac: 0xf12ac, + 0xf12ad: 0xf12ad, + 0xf12ae: 0xf12ae, + 0xf12af: 0xf12af, + 0xf12b0: 0xf12b0, + 0xf12b1: 0xf12b1, + 0xf12b2: 0xf12b2, + 0xf12b3: 0xf12b3, + 0xf12b4: 0xf12b4, + 0xf12b5: 0xf12b5, + 0xf12b6: 0xf12b6, + 0xf12b7: 0xf12b7, + 0xf12b8: 0xf12b8, + 0xf12b9: 0xf12b9, + 0xf12ba: 0xf12ba, + 0xf12bb: 0xf12bb, + 0xf12bc: 0xf12bc, + 0xf12bd: 0xf12bd, + 0xf12be: 0xf12be, + 0xf12bf: 0xf12bf, + 0xf12c0: 0xf12c0, + 0xf12c1: 0xf12c1, + 0xf12c2: 0xf12c2, + 0xf12c3: 0xf12c3, + 0xf12c4: 0xf12c4, + 0xf12c5: 0xf12c5, + 0xf12c6: 0xf12c6, + 0xf12c7: 0xf12c7, + 0xf12c8: 0xf12c8, + 0xf12c9: 0xf12c9, + 0xf12ca: 0xf12ca, + 0xf12cb: 0xf12cb, + 0xf12cc: 0xf12cc, + 0xf12cd: 0xf12cd, + 0xf12ce: 0xf12ce, + 0xf12cf: 0xf12cf, + 0xf12d0: 0xf12d0, + 0xf12d1: 0xf12d1, + 0xf12d2: 0xf12d2, + 0xf12d3: 0xf12d3, + 0xf12d4: 0xf12d4, + 0xf12d5: 0xf12d5, + 0xf12d6: 0xf12d6, + 0xf12d7: 0xf12d7, + 0xf12d8: 0xf12d8, + 0xf12d9: 0xf12d9, + 0xf12da: 0xf12da, + 0xf12db: 0xf12db, + 0xf12dc: 0xf12dc, + 0xf12dd: 0xf12dd, + 0xf12de: 0xf12de, + 0xf12df: 0xf12df, + 0xf12e0: 0xf12e0, + 0xf12e1: 0xf12e1, + 0xf12e2: 0xf12e2, + 0xf12e3: 0xf12e3, + 0xf12e4: 0xf12e4, + 0xf12e5: 0xf12e5, + 0xf12e6: 0xf12e6, + 0xf12e7: 0xf12e7, + 0xf12e8: 0xf12e8, + 0xf12e9: 0xf12e9, + 0xf12ea: 0xf12ea, + 0xf12eb: 0xf12eb, + 0xf12ec: 0xf12ec, + 0xf12ed: 0xf12ed, + 0xf12ee: 0xf12ee, + 0xf12ef: 0xf12ef, + 0xf12f0: 0xf12f0, + 0xf12f1: 0xf12f1, + 0xf12f2: 0xf12f2, + 0xf12f3: 0xf12f3, + 0xf12f4: 0xf12f4, + 0xf12f5: 0xf12f5, + 0xf12f6: 0xf12f6, + 0xf12f7: 0xf12f7, + 0xf12f8: 0xf12f8, + 0xf12f9: 0xf12f9, + 0xf12fa: 0xf12fa, + 0xf12fb: 0xf12fb, + 0xf12fc: 0xf12fc, + 0xf12fd: 0xf12fd, + 0xf12fe: 0xf12fe, + 0xf12ff: 0xf12ff, + 0xf1300: 0xf1300, + 0xf1301: 0xf1301, + 0xf1302: 0xf1302, + 0xf1303: 0xf1303, + 0xf1304: 0xf1304, + 0xf1305: 0xf1305, + 0xf1306: 0xf1306, + 0xf1307: 0xf1307, + 0xf1308: 0xf1308, + 0xf1309: 0xf1309, + 0xf130a: 0xf130a, + 0xf130b: 0xf130b, + 0xf130c: 0xf130c, + 0xf130d: 0xf130d, + 0xf130e: 0xf130e, + 0xf130f: 0xf130f, + 0xf1310: 0xf1310, + 0xf1311: 0xf1311, + 0xf1312: 0xf1312, + 0xf1313: 0xf1313, + 0xf1314: 0xf1314, + 0xf1315: 0xf1315, + 0xf1316: 0xf1316, + 0xf1317: 0xf1317, + 0xf1318: 0xf1318, + 0xf1319: 0xf1319, + 0xf131a: 0xf131a, + 0xf131b: 0xf131b, + 0xf131c: 0xf131c, + 0xf131d: 0xf131d, + 0xf131e: 0xf131e, + 0xf131f: 0xf131f, + 0xf1320: 0xf1320, + 0xf1321: 0xf1321, + 0xf1322: 0xf1322, + 0xf1323: 0xf1323, + 0xf1324: 0xf1324, + 0xf1325: 0xf1325, + 0xf1326: 0xf1326, + 0xf1327: 0xf1327, + 0xf1328: 0xf1328, + 0xf1329: 0xf1329, + 0xf132a: 0xf132a, + 0xf132b: 0xf132b, + 0xf132c: 0xf132c, + 0xf132d: 0xf132d, + 0xf132e: 0xf132e, + 0xf132f: 0xf132f, + 0xf1330: 0xf1330, + 0xf1331: 0xf1331, + 0xf1332: 0xf1332, + 0xf1333: 0xf1333, + 0xf1334: 0xf1334, + 0xf1335: 0xf1335, + 0xf1336: 0xf1336, + 0xf1337: 0xf1337, + 0xf1338: 0xf1338, + 0xf1339: 0xf1339, + 0xf133a: 0xf133a, + 0xf133b: 0xf133b, + 0xf133c: 0xf133c, + 0xf133d: 0xf133d, + 0xf133e: 0xf133e, + 0xf133f: 0xf133f, + 0xf1340: 0xf1340, + 0xf1341: 0xf1341, + 0xf1342: 0xf1342, + 0xf1343: 0xf1343, + 0xf1344: 0xf1344, + 0xf1345: 0xf1345, + 0xf1346: 0xf1346, + 0xf1347: 0xf1347, + 0xf1348: 0xf1348, + 0xf1349: 0xf1349, + 0xf134a: 0xf134a, + 0xf134b: 0xf134b, + 0xf134c: 0xf134c, + 0xf134d: 0xf134d, + 0xf134e: 0xf134e, + 0xf134f: 0xf134f, + 0xf1350: 0xf1350, + 0xf1351: 0xf1351, + 0xf1352: 0xf1352, + 0xf1353: 0xf1353, + 0xf1354: 0xf1354, + 0xf1355: 0xf1355, + 0xf1356: 0xf1356, + 0xf1357: 0xf1357, + 0xf1358: 0xf1358, + 0xf1359: 0xf1359, + 0xf135a: 0xf135a, + 0xf135b: 0xf135b, + 0xf135c: 0xf135c, + 0xf135d: 0xf135d, + 0xf135e: 0xf135e, + 0xf135f: 0xf135f, + 0xf1360: 0xf1360, + 0xf1361: 0xf1361, + 0xf1362: 0xf1362, + 0xf1363: 0xf1363, + 0xf1364: 0xf1364, + 0xf1365: 0xf1365, + 0xf1366: 0xf1366, + 0xf1367: 0xf1367, + 0xf1368: 0xf1368, + 0xf1369: 0xf1369, + 0xf136a: 0xf136a, + 0xf136b: 0xf136b, + 0xf136c: 0xf136c, + 0xf136d: 0xf136d, + 0xf136e: 0xf136e, + 0xf136f: 0xf136f, + 0xf1370: 0xf1370, + 0xf1371: 0xf1371, + 0xf1372: 0xf1372, + 0xf1373: 0xf1373, + 0xf1374: 0xf1374, + 0xf1375: 0xf1375, + 0xf1376: 0xf1376, + 0xf1377: 0xf1377, + 0xf1378: 0xf1378, + 0xf1379: 0xf1379, + 0xf137a: 0xf137a, + 0xf137b: 0xf137b, + 0xf137c: 0xf137c, + 0xf137d: 0xf137d, + 0xf137e: 0xf137e, + 0xf137f: 0xf137f, + 0xf1380: 0xf1380, + 0xf1381: 0xf1381, + 0xf1382: 0xf1382, + 0xf1383: 0xf1383, + 0xf1384: 0xf1384, + 0xf1385: 0xf1385, + 0xf1386: 0xf1386, + 0xf1387: 0xf1387, + 0xf1388: 0xf1388, + 0xf1389: 0xf1389, + 0xf138a: 0xf138a, + 0xf138b: 0xf138b, + 0xf138c: 0xf138c, + 0xf138d: 0xf138d, + 0xf138e: 0xf138e, + 0xf138f: 0xf138f, + 0xf1390: 0xf1390, + 0xf1391: 0xf1391, + 0xf1392: 0xf1392, + 0xf1393: 0xf1393, + 0xf1394: 0xf1394, + 0xf1395: 0xf1395, + 0xf1396: 0xf1396, + 0xf1397: 0xf1397, + 0xf1398: 0xf1398, + 0xf1399: 0xf1399, + 0xf139a: 0xf139a, + 0xf139b: 0xf139b, + 0xf139c: 0xf139c, + 0xf139d: 0xf139d, + 0xf139e: 0xf139e, + 0xf139f: 0xf139f, + 0xf13a0: 0xf13a0, + 0xf13a1: 0xf13a1, + 0xf13a2: 0xf13a2, + 0xf13a3: 0xf13a3, + 0xf13a4: 0xf13a4, + 0xf13a5: 0xf13a5, + 0xf13a6: 0xf13a6, + 0xf13a7: 0xf13a7, + 0xf13a8: 0xf13a8, + 0xf13a9: 0xf13a9, + 0xf13aa: 0xf13aa, + 0xf13ab: 0xf13ab, + 0xf13ac: 0xf13ac, + 0xf13ad: 0xf13ad, + 0xf13ae: 0xf13ae, + 0xf13af: 0xf13af, + 0xf13b0: 0xf13b0, + 0xf13b1: 0xf13b1, + 0xf13b2: 0xf13b2, + 0xf13b3: 0xf13b3, + 0xf13b4: 0xf13b4, + 0xf13b5: 0xf13b5, + 0xf13b6: 0xf13b6, + 0xf13b7: 0xf13b7, + 0xf13b8: 0xf13b8, + 0xf13b9: 0xf13b9, + 0xf13ba: 0xf13ba, + 0xf13bb: 0xf13bb, + 0xf13bc: 0xf13bc, + 0xf13bd: 0xf13bd, + 0xf13be: 0xf13be, + 0xf13bf: 0xf13bf, + 0xf13c0: 0xf13c0, + 0xf13c1: 0xf13c1, + 0xf13c2: 0xf13c2, + 0xf13c3: 0xf13c3, + 0xf13c4: 0xf13c4, + 0xf13c5: 0xf13c5, + 0xf13c6: 0xf13c6, + 0xf13c7: 0xf13c7, + 0xf13c8: 0xf13c8, + 0xf13c9: 0xf13c9, + 0xf13ca: 0xf13ca, + 0xf13cb: 0xf13cb, + 0xf13cc: 0xf13cc, + 0xf13cd: 0xf13cd, + 0xf13ce: 0xf13ce, + 0xf13cf: 0xf13cf, + 0xf13d0: 0xf13d0, + 0xf13d1: 0xf13d1, + 0xf13d2: 0xf13d2, + 0xf13d3: 0xf13d3, + 0xf13d4: 0xf13d4, + 0xf13d5: 0xf13d5, + 0xf13d6: 0xf13d6, + 0xf13d7: 0xf13d7, + 0xf13d8: 0xf13d8, + 0xf13d9: 0xf13d9, + 0xf13da: 0xf13da, + 0xf13db: 0xf13db, + 0xf13dc: 0xf13dc, + 0xf13dd: 0xf13dd, + 0xf13de: 0xf13de, + 0xf13df: 0xf13df, + 0xf13e0: 0xf13e0, + 0xf13e1: 0xf13e1, + 0xf13e2: 0xf13e2, + 0xf13e3: 0xf13e3, + 0xf13e4: 0xf13e4, + 0xf13e5: 0xf13e5, + 0xf13e6: 0xf13e6, + 0xf13e7: 0xf13e7, + 0xf13e8: 0xf13e8, + 0xf13e9: 0xf13e9, + 0xf13ea: 0xf13ea, + 0xf13eb: 0xf13eb, + 0xf13ec: 0xf13ec, + 0xf13ed: 0xf13ed, + 0xf13ee: 0xf13ee, + 0xf13ef: 0xf13ef, + 0xf13f0: 0xf13f0, + 0xf13f1: 0xf13f1, + 0xf13f2: 0xf13f2, + 0xf13f3: 0xf13f3, + 0xf13f4: 0xf13f4, + 0xf13f5: 0xf13f5, + 0xf13f6: 0xf13f6, + 0xf13f7: 0xf13f7, + 0xf13f8: 0xf13f8, + 0xf13f9: 0xf13f9, + 0xf13fa: 0xf13fa, + 0xf13fb: 0xf13fb, + 0xf13fc: 0xf13fc, + 0xf13fd: 0xf13fd, + 0xf13fe: 0xf13fe, + 0xf13ff: 0xf13ff, + 0xf1400: 0xf1400, + 0xf1401: 0xf1401, + 0xf1402: 0xf1402, + 0xf1403: 0xf1403, + 0xf1404: 0xf1404, + 0xf1405: 0xf1405, + 0xf1406: 0xf1406, + 0xf1407: 0xf1407, + 0xf1408: 0xf1408, + 0xf1409: 0xf1409, + 0xf140a: 0xf140a, + 0xf140b: 0xf140b, + 0xf140c: 0xf140c, + 0xf140d: 0xf140d, + 0xf140e: 0xf140e, + 0xf140f: 0xf140f, + 0xf1410: 0xf1410, + 0xf1411: 0xf1411, + 0xf1412: 0xf1412, + 0xf1413: 0xf1413, + 0xf1414: 0xf1414, + 0xf1415: 0xf1415, + 0xf1416: 0xf1416, + 0xf1417: 0xf1417, + 0xf1418: 0xf1418, + 0xf1419: 0xf1419, + 0xf141a: 0xf141a, + 0xf141b: 0xf141b, + 0xf141c: 0xf141c, + 0xf141d: 0xf141d, + 0xf141e: 0xf141e, + 0xf141f: 0xf141f, + 0xf1420: 0xf1420, + 0xf1421: 0xf1421, + 0xf1422: 0xf1422, + 0xf1423: 0xf1423, + 0xf1424: 0xf1424, + 0xf1425: 0xf1425, + 0xf1426: 0xf1426, + 0xf1427: 0xf1427, + 0xf1428: 0xf1428, + 0xf1429: 0xf1429, + 0xf142a: 0xf142a, + 0xf142b: 0xf142b, + 0xf142c: 0xf142c, + 0xf142d: 0xf142d, + 0xf142e: 0xf142e, + 0xf142f: 0xf142f, + 0xf1430: 0xf1430, + 0xf1431: 0xf1431, + 0xf1432: 0xf1432, + 0xf1433: 0xf1433, + 0xf1434: 0xf1434, + 0xf1435: 0xf1435, + 0xf1436: 0xf1436, + 0xf1437: 0xf1437, + 0xf1438: 0xf1438, + 0xf1439: 0xf1439, + 0xf143a: 0xf143a, + 0xf143b: 0xf143b, + 0xf143c: 0xf143c, + 0xf143d: 0xf143d, + 0xf143e: 0xf143e, + 0xf143f: 0xf143f, + 0xf1440: 0xf1440, + 0xf1441: 0xf1441, + 0xf1442: 0xf1442, + 0xf1443: 0xf1443, + 0xf1444: 0xf1444, + 0xf1445: 0xf1445, + 0xf1446: 0xf1446, + 0xf1447: 0xf1447, + 0xf1448: 0xf1448, + 0xf1449: 0xf1449, + 0xf144a: 0xf144a, + 0xf144b: 0xf144b, + 0xf144c: 0xf144c, + 0xf144d: 0xf144d, + 0xf144e: 0xf144e, + 0xf144f: 0xf144f, + 0xf1450: 0xf1450, + 0xf1451: 0xf1451, + 0xf1452: 0xf1452, + 0xf1453: 0xf1453, + 0xf1454: 0xf1454, + 0xf1455: 0xf1455, + 0xf1456: 0xf1456, + 0xf1457: 0xf1457, + 0xf1458: 0xf1458, + 0xf1459: 0xf1459, + 0xf145a: 0xf145a, + 0xf145b: 0xf145b, + 0xf145c: 0xf145c, + 0xf145d: 0xf145d, + 0xf145e: 0xf145e, + 0xf145f: 0xf145f, + 0xf1460: 0xf1460, + 0xf1461: 0xf1461, + 0xf1462: 0xf1462, + 0xf1463: 0xf1463, + 0xf1464: 0xf1464, + 0xf1465: 0xf1465, + 0xf1466: 0xf1466, + 0xf1467: 0xf1467, + 0xf1468: 0xf1468, + 0xf1469: 0xf1469, + 0xf146a: 0xf146a, + 0xf146b: 0xf146b, + 0xf146c: 0xf146c, + 0xf146d: 0xf146d, + 0xf146e: 0xf146e, + 0xf146f: 0xf146f, + 0xf1470: 0xf1470, + 0xf1471: 0xf1471, + 0xf1472: 0xf1472, + 0xf1473: 0xf1473, + 0xf1474: 0xf1474, + 0xf1475: 0xf1475, + 0xf1476: 0xf1476, + 0xf1477: 0xf1477, + 0xf1478: 0xf1478, + 0xf1479: 0xf1479, + 0xf147a: 0xf147a, + 0xf147b: 0xf147b, + 0xf147c: 0xf147c, + 0xf147d: 0xf147d, + 0xf147e: 0xf147e, + 0xf147f: 0xf147f, + 0xf1480: 0xf1480, + 0xf1481: 0xf1481, + 0xf1482: 0xf1482, + 0xf1483: 0xf1483, + 0xf1484: 0xf1484, + 0xf1485: 0xf1485, + 0xf1486: 0xf1486, + 0xf1487: 0xf1487, + 0xf1488: 0xf1488, + 0xf1489: 0xf1489, + 0xf148a: 0xf148a, + 0xf148b: 0xf148b, + 0xf148c: 0xf148c, + 0xf148d: 0xf148d, + 0xf148e: 0xf148e, + 0xf148f: 0xf148f, + 0xf1490: 0xf1490, + 0xf1491: 0xf1491, + 0xf1492: 0xf1492, + 0xf1493: 0xf1493, + 0xf1494: 0xf1494, + 0xf1495: 0xf1495, + 0xf1496: 0xf1496, + 0xf1497: 0xf1497, + 0xf1498: 0xf1498, + 0xf1499: 0xf1499, + 0xf149a: 0xf149a, + 0xf149b: 0xf149b, + 0xf149c: 0xf149c, + 0xf149d: 0xf149d, + 0xf149e: 0xf149e, + 0xf149f: 0xf149f, + 0xf14a0: 0xf14a0, + 0xf14a1: 0xf14a1, + 0xf14a2: 0xf14a2, + 0xf14a3: 0xf14a3, + 0xf14a4: 0xf14a4, + 0xf14a5: 0xf14a5, + 0xf14a6: 0xf14a6, + 0xf14a7: 0xf14a7, + 0xf14a8: 0xf14a8, + 0xf14a9: 0xf14a9, + 0xf14aa: 0xf14aa, + 0xf14ab: 0xf14ab, + 0xf14ac: 0xf14ac, + 0xf14ad: 0xf14ad, + 0xf14ae: 0xf14ae, + 0xf14af: 0xf14af, + 0xf14b0: 0xf14b0, + 0xf14b1: 0xf14b1, + 0xf14b2: 0xf14b2, + 0xf14b3: 0xf14b3, + 0xf14b4: 0xf14b4, + 0xf14b5: 0xf14b5, + 0xf14b6: 0xf14b6, + 0xf14b7: 0xf14b7, + 0xf14b8: 0xf14b8, + 0xf14b9: 0xf14b9, + 0xf14ba: 0xf14ba, + 0xf14bb: 0xf14bb, + 0xf14bc: 0xf14bc, + 0xf14bd: 0xf14bd, + 0xf14be: 0xf14be, + 0xf14bf: 0xf14bf, + 0xf14c0: 0xf14c0, + 0xf14c1: 0xf14c1, + 0xf14c2: 0xf14c2, + 0xf14c3: 0xf14c3, + 0xf14c4: 0xf14c4, + 0xf14c5: 0xf14c5, + 0xf14c6: 0xf14c6, + 0xf14c7: 0xf14c7, + 0xf14c8: 0xf14c8, + 0xf14c9: 0xf14c9, + 0xf14ca: 0xf14ca, + 0xf14cb: 0xf14cb, + 0xf14cc: 0xf14cc, + 0xf14cd: 0xf14cd, + 0xf14ce: 0xf14ce, + 0xf14cf: 0xf14cf, + 0xf14d0: 0xf14d0, + 0xf14d1: 0xf14d1, + 0xf14d2: 0xf14d2, + 0xf14d3: 0xf14d3, + 0xf14d4: 0xf14d4, + 0xf14d5: 0xf14d5, + 0xf14d6: 0xf14d6, + 0xf14d7: 0xf14d7, + 0xf14d8: 0xf14d8, + 0xf14d9: 0xf14d9, + 0xf14da: 0xf14da, + 0xf14db: 0xf14db, + 0xf14dc: 0xf14dc, + 0xf14dd: 0xf14dd, + 0xf14de: 0xf14de, + 0xf14df: 0xf14df, + 0xf14e0: 0xf14e0, + 0xf14e1: 0xf14e1, + 0xf14e2: 0xf14e2, + 0xf14e3: 0xf14e3, + 0xf14e4: 0xf14e4, + 0xf14e5: 0xf14e5, + 0xf14e6: 0xf14e6, + 0xf14e7: 0xf14e7, + 0xf14e8: 0xf14e8, + 0xf14e9: 0xf14e9, + 0xf14ea: 0xf14ea, + 0xf14eb: 0xf14eb, + 0xf14ec: 0xf14ec, + 0xf14ed: 0xf14ed, + 0xf14ee: 0xf14ee, + 0xf14ef: 0xf14ef, + 0xf14f0: 0xf14f0, + 0xf14f1: 0xf14f1, + 0xf14f2: 0xf14f2, + 0xf14f3: 0xf14f3, + 0xf14f4: 0xf14f4, + 0xf14f5: 0xf14f5, + 0xf14f6: 0xf14f6, + 0xf14f7: 0xf14f7, + 0xf14f8: 0xf14f8, + 0xf14f9: 0xf14f9, + 0xf14fa: 0xf14fa, + 0xf14fb: 0xf14fb, + 0xf14fc: 0xf14fc, + 0xf14fd: 0xf14fd, + 0xf14fe: 0xf14fe, + 0xf14ff: 0xf14ff, + 0xf1500: 0xf1500, + 0xf1501: 0xf1501, + 0xf1502: 0xf1502, + 0xf1503: 0xf1503, + 0xf1504: 0xf1504, + 0xf1505: 0xf1505, + 0xf1506: 0xf1506, + 0xf1507: 0xf1507, + 0xf1508: 0xf1508, + 0xf1509: 0xf1509, + 0xf150a: 0xf150a, + 0xf150b: 0xf150b, + 0xf150c: 0xf150c, + 0xf150d: 0xf150d, + 0xf150e: 0xf150e, + 0xf150f: 0xf150f, + 0xf1510: 0xf1510, + 0xf1511: 0xf1511, + 0xf1512: 0xf1512, + 0xf1513: 0xf1513, + 0xf1514: 0xf1514, + 0xf1515: 0xf1515, + 0xf1516: 0xf1516, + 0xf1517: 0xf1517, + 0xf1518: 0xf1518, + 0xf1519: 0xf1519, + 0xf151a: 0xf151a, + 0xf151b: 0xf151b, + 0xf151c: 0xf151c, + 0xf151d: 0xf151d, + 0xf151e: 0xf151e, + 0xf151f: 0xf151f, + 0xf1520: 0xf1520, + 0xf1521: 0xf1521, + 0xf1522: 0xf1522, + 0xf1523: 0xf1523, + 0xf1524: 0xf1524, + 0xf1525: 0xf1525, + 0xf1526: 0xf1526, + 0xf1527: 0xf1527, + 0xf1528: 0xf1528, + 0xf1529: 0xf1529, + 0xf152a: 0xf152a, + 0xf152b: 0xf152b, + 0xf152c: 0xf152c, + 0xf152d: 0xf152d, + 0xf152e: 0xf152e, + 0xf152f: 0xf152f, + 0xf1530: 0xf1530, + 0xf1531: 0xf1531, + 0xf1532: 0xf1532, + 0xf1533: 0xf1533, + 0xf1534: 0xf1534, + 0xf1535: 0xf1535, + 0xf1536: 0xf1536, + 0xf1537: 0xf1537, + 0xf1538: 0xf1538, + 0xf1539: 0xf1539, + 0xf153a: 0xf153a, + 0xf153b: 0xf153b, + 0xf153c: 0xf153c, + 0xf153d: 0xf153d, + 0xf153e: 0xf153e, + 0xf153f: 0xf153f, + 0xf1540: 0xf1540, + 0xf1541: 0xf1541, + 0xf1542: 0xf1542, + 0xf1543: 0xf1543, + 0xf1544: 0xf1544, + 0xf1545: 0xf1545, + 0xf1546: 0xf1546, + 0xf1547: 0xf1547, + 0xf1548: 0xf1548, + 0xf1549: 0xf1549, + 0xf154a: 0xf154a, + 0xf154b: 0xf154b, + 0xf154c: 0xf154c, + 0xf154d: 0xf154d, + 0xf154e: 0xf154e, + 0xf154f: 0xf154f, + 0xf1550: 0xf1550, + 0xf1551: 0xf1551, + 0xf1552: 0xf1552, + 0xf1553: 0xf1553, + 0xf1554: 0xf1554, + 0xf1555: 0xf1555, + 0xf1556: 0xf1556, + 0xf1557: 0xf1557, + 0xf1558: 0xf1558, + 0xf1559: 0xf1559, + 0xf155a: 0xf155a, + 0xf155b: 0xf155b, + 0xf155c: 0xf155c, + 0xf155d: 0xf155d, + 0xf155e: 0xf155e, + 0xf155f: 0xf155f, + 0xf1560: 0xf1560, + 0xf1561: 0xf1561, + 0xf1562: 0xf1562, + 0xf1563: 0xf1563, + 0xf1564: 0xf1564, + 0xf1565: 0xf1565, + 0xf1566: 0xf1566, + 0xf1567: 0xf1567, + 0xf1568: 0xf1568, + 0xf1569: 0xf1569, + 0xf156a: 0xf156a, + 0xf156b: 0xf156b, + 0xf156c: 0xf156c, + 0xf156d: 0xf156d, + 0xf156e: 0xf156e, + 0xf156f: 0xf156f, + 0xf1570: 0xf1570, + 0xf1571: 0xf1571, + 0xf1572: 0xf1572, + 0xf1573: 0xf1573, + 0xf1574: 0xf1574, + 0xf1575: 0xf1575, + 0xf1576: 0xf1576, + 0xf1577: 0xf1577, + 0xf1578: 0xf1578, + 0xf1579: 0xf1579, + 0xf157a: 0xf157a, + 0xf157b: 0xf157b, + 0xf157c: 0xf157c, + 0xf157d: 0xf157d, + 0xf157e: 0xf157e, + 0xf157f: 0xf157f, + 0xf1580: 0xf1580, + 0xf1581: 0xf1581, + 0xf1582: 0xf1582, + 0xf1583: 0xf1583, + 0xf1584: 0xf1584, + 0xf1585: 0xf1585, + 0xf1586: 0xf1586, + 0xf1587: 0xf1587, + 0xf1588: 0xf1588, + 0xf1589: 0xf1589, + 0xf158a: 0xf158a, + 0xf158b: 0xf158b, + 0xf158c: 0xf158c, + 0xf158d: 0xf158d, + 0xf158e: 0xf158e, + 0xf158f: 0xf158f, + 0xf1590: 0xf1590, + 0xf1591: 0xf1591, + 0xf1592: 0xf1592, + 0xf1593: 0xf1593, + 0xf1594: 0xf1594, + 0xf1595: 0xf1595, + 0xf1596: 0xf1596, + 0xf1597: 0xf1597, + 0xf1598: 0xf1598, + 0xf1599: 0xf1599, + 0xf159a: 0xf159a, + 0xf159b: 0xf159b, + 0xf159c: 0xf159c, + 0xf159d: 0xf159d, + 0xf159e: 0xf159e, + 0xf159f: 0xf159f, + 0xf15a0: 0xf15a0, + 0xf15a1: 0xf15a1, + 0xf15a2: 0xf15a2, + 0xf15a3: 0xf15a3, + 0xf15a4: 0xf15a4, + 0xf15a5: 0xf15a5, + 0xf15a6: 0xf15a6, + 0xf15a7: 0xf15a7, + 0xf15a8: 0xf15a8, + 0xf15a9: 0xf15a9, + 0xf15aa: 0xf15aa, + 0xf15ab: 0xf15ab, + 0xf15ac: 0xf15ac, + 0xf15ad: 0xf15ad, + 0xf15ae: 0xf15ae, + 0xf15af: 0xf15af, + 0xf15b0: 0xf15b0, + 0xf15b1: 0xf15b1, + 0xf15b2: 0xf15b2, + 0xf15b3: 0xf15b3, + 0xf15b4: 0xf15b4, + 0xf15b5: 0xf15b5, + 0xf15b6: 0xf15b6, + 0xf15b7: 0xf15b7, + 0xf15b8: 0xf15b8, + 0xf15b9: 0xf15b9, + 0xf15ba: 0xf15ba, + 0xf15bb: 0xf15bb, + 0xf15bc: 0xf15bc, + 0xf15bd: 0xf15bd, + 0xf15be: 0xf15be, + 0xf15bf: 0xf15bf, + 0xf15c0: 0xf15c0, + 0xf15c1: 0xf15c1, + 0xf15c2: 0xf15c2, + 0xf15c3: 0xf15c3, + 0xf15c4: 0xf15c4, + 0xf15c5: 0xf15c5, + 0xf15c6: 0xf15c6, + 0xf15c7: 0xf15c7, + 0xf15c8: 0xf15c8, + 0xf15c9: 0xf15c9, + 0xf15ca: 0xf15ca, + 0xf15cb: 0xf15cb, + 0xf15cc: 0xf15cc, + 0xf15cd: 0xf15cd, + 0xf15ce: 0xf15ce, + 0xf15cf: 0xf15cf, + 0xf15d0: 0xf15d0, + 0xf15d1: 0xf15d1, + 0xf15d2: 0xf15d2, + 0xf15d3: 0xf15d3, + 0xf15d4: 0xf15d4, + 0xf15d5: 0xf15d5, + 0xf15d6: 0xf15d6, + 0xf15d7: 0xf15d7, + 0xf15d8: 0xf15d8, + 0xf15d9: 0xf15d9, + 0xf15da: 0xf15da, + 0xf15db: 0xf15db, + 0xf15dc: 0xf15dc, + 0xf15dd: 0xf15dd, + 0xf15de: 0xf15de, + 0xf15df: 0xf15df, + 0xf15e0: 0xf15e0, + 0xf15e1: 0xf15e1, + 0xf15e2: 0xf15e2, + 0xf15e3: 0xf15e3, + 0xf15e4: 0xf15e4, + 0xf15e5: 0xf15e5, + 0xf15e6: 0xf15e6, + 0xf15e7: 0xf15e7, + 0xf15e8: 0xf15e8, + 0xf15e9: 0xf15e9, + 0xf15ea: 0xf15ea, + 0xf15eb: 0xf15eb, + 0xf15ec: 0xf15ec, + 0xf15ed: 0xf15ed, + 0xf15ee: 0xf15ee, + 0xf15ef: 0xf15ef, + 0xf15f0: 0xf15f0, + 0xf15f1: 0xf15f1, + 0xf15f2: 0xf15f2, + 0xf15f3: 0xf15f3, + 0xf15f4: 0xf15f4, + 0xf15f5: 0xf15f5, + 0xf15f6: 0xf15f6, + 0xf15f7: 0xf15f7, + 0xf15f8: 0xf15f8, + 0xf15f9: 0xf15f9, + 0xf15fa: 0xf15fa, + 0xf15fb: 0xf15fb, + 0xf15fc: 0xf15fc, + 0xf15fd: 0xf15fd, + 0xf15fe: 0xf15fe, + 0xf15ff: 0xf15ff, + 0xf1600: 0xf1600, + 0xf1601: 0xf1601, + 0xf1602: 0xf1602, + 0xf1603: 0xf1603, + 0xf1604: 0xf1604, + 0xf1605: 0xf1605, + 0xf1606: 0xf1606, + 0xf1607: 0xf1607, + 0xf1608: 0xf1608, + 0xf1609: 0xf1609, + 0xf160a: 0xf160a, + 0xf160b: 0xf160b, + 0xf160c: 0xf160c, + 0xf160d: 0xf160d, + 0xf160e: 0xf160e, + 0xf160f: 0xf160f, + 0xf1610: 0xf1610, + 0xf1611: 0xf1611, + 0xf1612: 0xf1612, + 0xf1613: 0xf1613, + 0xf1614: 0xf1614, + 0xf1615: 0xf1615, + 0xf1616: 0xf1616, + 0xf1617: 0xf1617, + 0xf1618: 0xf1618, + 0xf1619: 0xf1619, + 0xf161a: 0xf161a, + 0xf161b: 0xf161b, + 0xf161c: 0xf161c, + 0xf161d: 0xf161d, + 0xf161e: 0xf161e, + 0xf161f: 0xf161f, + 0xf1620: 0xf1620, + 0xf1621: 0xf1621, + 0xf1622: 0xf1622, + 0xf1623: 0xf1623, + 0xf1624: 0xf1624, + 0xf1625: 0xf1625, + 0xf1626: 0xf1626, + 0xf1627: 0xf1627, + 0xf1628: 0xf1628, + 0xf1629: 0xf1629, + 0xf162a: 0xf162a, + 0xf162b: 0xf162b, + 0xf162c: 0xf162c, + 0xf162d: 0xf162d, + 0xf162e: 0xf162e, + 0xf162f: 0xf162f, + 0xf1630: 0xf1630, + 0xf1631: 0xf1631, + 0xf1632: 0xf1632, + 0xf1633: 0xf1633, + 0xf1634: 0xf1634, + 0xf1635: 0xf1635, + 0xf1636: 0xf1636, + 0xf1637: 0xf1637, + 0xf1638: 0xf1638, + 0xf1639: 0xf1639, + 0xf163a: 0xf163a, + 0xf163b: 0xf163b, + 0xf163c: 0xf163c, + 0xf163d: 0xf163d, + 0xf163e: 0xf163e, + 0xf163f: 0xf163f, + 0xf1640: 0xf1640, + 0xf1641: 0xf1641, + 0xf1642: 0xf1642, + 0xf1643: 0xf1643, + 0xf1644: 0xf1644, + 0xf1645: 0xf1645, + 0xf1646: 0xf1646, + 0xf1647: 0xf1647, + 0xf1648: 0xf1648, + 0xf1649: 0xf1649, + 0xf164a: 0xf164a, + 0xf164b: 0xf164b, + 0xf164c: 0xf164c, + 0xf164d: 0xf164d, + 0xf164e: 0xf164e, + 0xf164f: 0xf164f, + 0xf1650: 0xf1650, + 0xf1651: 0xf1651, + 0xf1652: 0xf1652, + 0xf1653: 0xf1653, + 0xf1654: 0xf1654, + 0xf1655: 0xf1655, + 0xf1656: 0xf1656, + 0xf1657: 0xf1657, + 0xf1658: 0xf1658, + 0xf1659: 0xf1659, + 0xf165a: 0xf165a, + 0xf165b: 0xf165b, + 0xf165c: 0xf165c, + 0xf165d: 0xf165d, + 0xf165e: 0xf165e, + 0xf165f: 0xf165f, + 0xf1660: 0xf1660, + 0xf1661: 0xf1661, + 0xf1662: 0xf1662, + 0xf1663: 0xf1663, + 0xf1664: 0xf1664, + 0xf1665: 0xf1665, + 0xf1666: 0xf1666, + 0xf1667: 0xf1667, + 0xf1668: 0xf1668, + 0xf1669: 0xf1669, + 0xf166a: 0xf166a, + 0xf166b: 0xf166b, + 0xf166c: 0xf166c, + 0xf166d: 0xf166d, + 0xf166e: 0xf166e, + 0xf166f: 0xf166f, + 0xf1670: 0xf1670, + 0xf1671: 0xf1671, + 0xf1672: 0xf1672, + 0xf1673: 0xf1673, + 0xf1674: 0xf1674, + 0xf1675: 0xf1675, + 0xf1676: 0xf1676, + 0xf1677: 0xf1677, + 0xf1678: 0xf1678, + 0xf1679: 0xf1679, + 0xf167a: 0xf167a, + 0xf167b: 0xf167b, + 0xf167c: 0xf167c, + 0xf167d: 0xf167d, + 0xf167e: 0xf167e, + 0xf167f: 0xf167f, + 0xf1680: 0xf1680, + 0xf1681: 0xf1681, + 0xf1682: 0xf1682, + 0xf1683: 0xf1683, + 0xf1684: 0xf1684, + 0xf1685: 0xf1685, + 0xf1686: 0xf1686, + 0xf1687: 0xf1687, + 0xf1688: 0xf1688, + 0xf1689: 0xf1689, + 0xf168a: 0xf168a, + 0xf168b: 0xf168b, + 0xf168c: 0xf168c, + 0xf168d: 0xf168d, + 0xf168e: 0xf168e, + 0xf168f: 0xf168f, + 0xf1690: 0xf1690, + 0xf1691: 0xf1691, + 0xf1692: 0xf1692, + 0xf1693: 0xf1693, + 0xf1694: 0xf1694, + 0xf1695: 0xf1695, + 0xf1696: 0xf1696, + 0xf1697: 0xf1697, + 0xf1698: 0xf1698, + 0xf1699: 0xf1699, + 0xf169a: 0xf169a, + 0xf169b: 0xf169b, + 0xf169c: 0xf169c, + 0xf169d: 0xf169d, + 0xf169e: 0xf169e, + 0xf169f: 0xf169f, + 0xf16a0: 0xf16a0, + 0xf16a1: 0xf16a1, + 0xf16a2: 0xf16a2, + 0xf16a3: 0xf16a3, + 0xf16a4: 0xf16a4, + 0xf16a5: 0xf16a5, + 0xf16a6: 0xf16a6, + 0xf16a7: 0xf16a7, + 0xf16a8: 0xf16a8, + 0xf16a9: 0xf16a9, + 0xf16aa: 0xf16aa, + 0xf16ab: 0xf16ab, + 0xf16ac: 0xf16ac, + 0xf16ad: 0xf16ad, + 0xf16ae: 0xf16ae, + 0xf16af: 0xf16af, + 0xf16b0: 0xf16b0, + 0xf16b1: 0xf16b1, + 0xf16b2: 0xf16b2, + 0xf16b3: 0xf16b3, + 0xf16b4: 0xf16b4, + 0xf16b5: 0xf16b5, + 0xf16b6: 0xf16b6, + 0xf16b7: 0xf16b7, + 0xf16b8: 0xf16b8, + 0xf16b9: 0xf16b9, + 0xf16ba: 0xf16ba, + 0xf16bb: 0xf16bb, + 0xf16bc: 0xf16bc, + 0xf16bd: 0xf16bd, + 0xf16be: 0xf16be, + 0xf16bf: 0xf16bf, + 0xf16c0: 0xf16c0, + 0xf16c1: 0xf16c1, + 0xf16c2: 0xf16c2, + 0xf16c3: 0xf16c3, + 0xf16c4: 0xf16c4, + 0xf16c5: 0xf16c5, + 0xf16c6: 0xf16c6, + 0xf16c7: 0xf16c7, + 0xf16c8: 0xf16c8, + 0xf16c9: 0xf16c9, + 0xf16ca: 0xf16ca, + 0xf16cb: 0xf16cb, + 0xf16cc: 0xf16cc, + 0xf16cd: 0xf16cd, + 0xf16ce: 0xf16ce, + 0xf16cf: 0xf16cf, + 0xf16d0: 0xf16d0, + 0xf16d1: 0xf16d1, + 0xf16d2: 0xf16d2, + 0xf16d3: 0xf16d3, + 0xf16d4: 0xf16d4, + 0xf16d5: 0xf16d5, + 0xf16d6: 0xf16d6, + 0xf16d7: 0xf16d7, + 0xf16d8: 0xf16d8, + 0xf16d9: 0xf16d9, + 0xf16da: 0xf16da, + 0xf16db: 0xf16db, + 0xf16dc: 0xf16dc, + 0xf16dd: 0xf16dd, + 0xf16de: 0xf16de, + 0xf16df: 0xf16df, + 0xf16e0: 0xf16e0, + 0xf16e1: 0xf16e1, + 0xf16e2: 0xf16e2, + 0xf16e3: 0xf16e3, + 0xf16e4: 0xf16e4, + 0xf16e5: 0xf16e5, + 0xf16e6: 0xf16e6, + 0xf16e7: 0xf16e7, + 0xf16e8: 0xf16e8, + 0xf16e9: 0xf16e9, + 0xf16ea: 0xf16ea, + 0xf16eb: 0xf16eb, + 0xf16ec: 0xf16ec, + 0xf16ed: 0xf16ed, + 0xf16ee: 0xf16ee, + 0xf16ef: 0xf16ef, + 0xf16f0: 0xf16f0, + 0xf16f1: 0xf16f1, + 0xf16f2: 0xf16f2, + 0xf16f3: 0xf16f3, + 0xf16f4: 0xf16f4, + 0xf16f5: 0xf16f5, + 0xf16f6: 0xf16f6, + 0xf16f7: 0xf16f7, + 0xf16f8: 0xf16f8, + 0xf16f9: 0xf16f9, + 0xf16fa: 0xf16fa, + 0xf16fb: 0xf16fb, + 0xf16fc: 0xf16fc, + 0xf16fd: 0xf16fd, + 0xf16fe: 0xf16fe, + 0xf16ff: 0xf16ff, + 0xf1700: 0xf1700, + 0xf1701: 0xf1701, + 0xf1702: 0xf1702, + 0xf1703: 0xf1703, + 0xf1704: 0xf1704, + 0xf1705: 0xf1705, + 0xf1706: 0xf1706, + 0xf1707: 0xf1707, + 0xf1708: 0xf1708, + 0xf1709: 0xf1709, + 0xf170a: 0xf170a, + 0xf170b: 0xf170b, + 0xf170c: 0xf170c, + 0xf170d: 0xf170d, + 0xf170e: 0xf170e, + 0xf170f: 0xf170f, + 0xf1710: 0xf1710, + 0xf1711: 0xf1711, + 0xf1712: 0xf1712, + 0xf1713: 0xf1713, + 0xf1714: 0xf1714, + 0xf1715: 0xf1715, + 0xf1716: 0xf1716, + 0xf1717: 0xf1717, + 0xf1718: 0xf1718, + 0xf1719: 0xf1719, + 0xf171a: 0xf171a, + 0xf171b: 0xf171b, + 0xf171c: 0xf171c, + 0xf171d: 0xf171d, + 0xf171e: 0xf171e, + 0xf171f: 0xf171f, + 0xf1720: 0xf1720, + 0xf1721: 0xf1721, + 0xf1722: 0xf1722, + 0xf1723: 0xf1723, + 0xf1724: 0xf1724, + 0xf1725: 0xf1725, + 0xf1726: 0xf1726, + 0xf1727: 0xf1727, + 0xf1728: 0xf1728, + 0xf1729: 0xf1729, + 0xf172a: 0xf172a, + 0xf172b: 0xf172b, + 0xf172c: 0xf172c, + 0xf172d: 0xf172d, + 0xf172e: 0xf172e, + 0xf172f: 0xf172f, + 0xf1730: 0xf1730, + 0xf1731: 0xf1731, + 0xf1732: 0xf1732, + 0xf1733: 0xf1733, + 0xf1734: 0xf1734, + 0xf1735: 0xf1735, + 0xf1736: 0xf1736, + 0xf1737: 0xf1737, + 0xf1738: 0xf1738, + 0xf1739: 0xf1739, + 0xf173a: 0xf173a, + 0xf173b: 0xf173b, + 0xf173c: 0xf173c, + 0xf173d: 0xf173d, + 0xf173e: 0xf173e, + 0xf173f: 0xf173f, + 0xf1740: 0xf1740, + 0xf1741: 0xf1741, + 0xf1742: 0xf1742, + 0xf1743: 0xf1743, + 0xf1744: 0xf1744, + 0xf1745: 0xf1745, + 0xf1746: 0xf1746, + 0xf1747: 0xf1747, + 0xf1748: 0xf1748, + 0xf1749: 0xf1749, + 0xf174a: 0xf174a, + 0xf174b: 0xf174b, + 0xf174c: 0xf174c, + 0xf174d: 0xf174d, + 0xf174e: 0xf174e, + 0xf174f: 0xf174f, + 0xf1750: 0xf1750, + 0xf1751: 0xf1751, + 0xf1752: 0xf1752, + 0xf1753: 0xf1753, + 0xf1754: 0xf1754, + 0xf1755: 0xf1755, + 0xf1756: 0xf1756, + 0xf1757: 0xf1757, + 0xf1758: 0xf1758, + 0xf1759: 0xf1759, + 0xf175a: 0xf175a, + 0xf175b: 0xf175b, + 0xf175c: 0xf175c, + 0xf175d: 0xf175d, + 0xf175e: 0xf175e, + 0xf175f: 0xf175f, + 0xf1760: 0xf1760, + 0xf1761: 0xf1761, + 0xf1762: 0xf1762, + 0xf1763: 0xf1763, + 0xf1764: 0xf1764, + 0xf1765: 0xf1765, + 0xf1766: 0xf1766, + 0xf1767: 0xf1767, + 0xf1768: 0xf1768, + 0xf1769: 0xf1769, + 0xf176a: 0xf176a, + 0xf176b: 0xf176b, + 0xf176c: 0xf176c, + 0xf176d: 0xf176d, + 0xf176e: 0xf176e, + 0xf176f: 0xf176f, + 0xf1770: 0xf1770, + 0xf1771: 0xf1771, + 0xf1772: 0xf1772, + 0xf1773: 0xf1773, + 0xf1774: 0xf1774, + 0xf1775: 0xf1775, + 0xf1776: 0xf1776, + 0xf1777: 0xf1777, + 0xf1778: 0xf1778, + 0xf1779: 0xf1779, + 0xf177a: 0xf177a, + 0xf177b: 0xf177b, + 0xf177c: 0xf177c, + 0xf177d: 0xf177d, + 0xf177e: 0xf177e, + 0xf177f: 0xf177f, + 0xf1780: 0xf1780, + 0xf1781: 0xf1781, + 0xf1782: 0xf1782, + 0xf1783: 0xf1783, + 0xf1784: 0xf1784, + 0xf1785: 0xf1785, + 0xf1786: 0xf1786, + 0xf1787: 0xf1787, + 0xf1788: 0xf1788, + 0xf1789: 0xf1789, + 0xf178a: 0xf178a, + 0xf178b: 0xf178b, + 0xf178c: 0xf178c, + 0xf178d: 0xf178d, + 0xf178e: 0xf178e, + 0xf178f: 0xf178f, + 0xf1790: 0xf1790, + 0xf1791: 0xf1791, + 0xf1792: 0xf1792, + 0xf1793: 0xf1793, + 0xf1794: 0xf1794, + 0xf1795: 0xf1795, + 0xf1796: 0xf1796, + 0xf1797: 0xf1797, + 0xf1798: 0xf1798, + 0xf1799: 0xf1799, + 0xf179a: 0xf179a, + 0xf179b: 0xf179b, + 0xf179c: 0xf179c, + 0xf179d: 0xf179d, + 0xf179e: 0xf179e, + 0xf179f: 0xf179f, + 0xf17a0: 0xf17a0, + 0xf17a1: 0xf17a1, + 0xf17a2: 0xf17a2, + 0xf17a3: 0xf17a3, + 0xf17a4: 0xf17a4, + 0xf17a5: 0xf17a5, + 0xf17a6: 0xf17a6, + 0xf17a7: 0xf17a7, + 0xf17a8: 0xf17a8, + 0xf17a9: 0xf17a9, + 0xf17aa: 0xf17aa, + 0xf17ab: 0xf17ab, + 0xf17ac: 0xf17ac, + 0xf17ad: 0xf17ad, + 0xf17ae: 0xf17ae, + 0xf17af: 0xf17af, + 0xf17b0: 0xf17b0, + 0xf17b1: 0xf17b1, + 0xf17b2: 0xf17b2, + 0xf17b3: 0xf17b3, + 0xf17b4: 0xf17b4, + 0xf17b5: 0xf17b5, + 0xf17b6: 0xf17b6, + 0xf17b7: 0xf17b7, + 0xf17b8: 0xf17b8, + 0xf17b9: 0xf17b9, + 0xf17ba: 0xf17ba, + 0xf17bb: 0xf17bb, + 0xf17bc: 0xf17bc, + 0xf17bd: 0xf17bd, + 0xf17be: 0xf17be, + 0xf17bf: 0xf17bf, + 0xf17c0: 0xf17c0, + 0xf17c1: 0xf17c1, + 0xf17c2: 0xf17c2, + 0xf17c3: 0xf17c3, + 0xf17c4: 0xf17c4, + 0xf17c5: 0xf17c5, + 0xf17c6: 0xf17c6, + 0xf17c7: 0xf17c7, + 0xf17c8: 0xf17c8, + 0xf17c9: 0xf17c9, + 0xf17ca: 0xf17ca, + 0xf17cb: 0xf17cb, + 0xf17cc: 0xf17cc, + 0xf17cd: 0xf17cd, + 0xf17ce: 0xf17ce, + 0xf17cf: 0xf17cf, + 0xf17d0: 0xf17d0, + 0xf17d1: 0xf17d1, + 0xf17d2: 0xf17d2, + 0xf17d3: 0xf17d3, + 0xf17d4: 0xf17d4, + 0xf17d5: 0xf17d5, + 0xf17d6: 0xf17d6, + 0xf17d7: 0xf17d7, + 0xf17d8: 0xf17d8, + 0xf17d9: 0xf17d9, + 0xf17da: 0xf17da, + 0xf17db: 0xf17db, + 0xf17dc: 0xf17dc, + 0xf17dd: 0xf17dd, + 0xf17de: 0xf17de, + 0xf17df: 0xf17df, + 0xf17e0: 0xf17e0, + 0xf17e1: 0xf17e1, + 0xf17e2: 0xf17e2, + 0xf17e3: 0xf17e3, + 0xf17e4: 0xf17e4, + 0xf17e5: 0xf17e5, + 0xf17e6: 0xf17e6, + 0xf17e7: 0xf17e7, + 0xf17e8: 0xf17e8, + 0xf17e9: 0xf17e9, + 0xf17ea: 0xf17ea, + 0xf17eb: 0xf17eb, + 0xf17ec: 0xf17ec, + 0xf17ed: 0xf17ed, + 0xf17ee: 0xf17ee, + 0xf17ef: 0xf17ef, + 0xf17f0: 0xf17f0, + 0xf17f1: 0xf17f1, + 0xf17f2: 0xf17f2, + 0xf17f3: 0xf17f3, + 0xf17f4: 0xf17f4, + 0xf17f5: 0xf17f5, + 0xf17f6: 0xf17f6, + 0xf17f7: 0xf17f7, + 0xf17f8: 0xf17f8, + 0xf17f9: 0xf17f9, + 0xf17fa: 0xf17fa, + 0xf17fb: 0xf17fb, + 0xf17fc: 0xf17fc, + 0xf17fd: 0xf17fd, + 0xf17fe: 0xf17fe, + 0xf17ff: 0xf17ff, + 0xf1800: 0xf1800, + 0xf1801: 0xf1801, + 0xf1802: 0xf1802, + 0xf1803: 0xf1803, + 0xf1804: 0xf1804, + 0xf1805: 0xf1805, + 0xf1806: 0xf1806, + 0xf1807: 0xf1807, + 0xf1808: 0xf1808, + 0xf1809: 0xf1809, + 0xf180a: 0xf180a, + 0xf180b: 0xf180b, + 0xf180c: 0xf180c, + 0xf180d: 0xf180d, + 0xf180e: 0xf180e, + 0xf180f: 0xf180f, + 0xf1810: 0xf1810, + 0xf1811: 0xf1811, + 0xf1812: 0xf1812, + 0xf1813: 0xf1813, + 0xf1814: 0xf1814, + 0xf1815: 0xf1815, + 0xf1816: 0xf1816, + 0xf1817: 0xf1817, + 0xf1818: 0xf1818, + 0xf1819: 0xf1819, + 0xf181a: 0xf181a, + 0xf181b: 0xf181b, + 0xf181c: 0xf181c, + 0xf181d: 0xf181d, + 0xf181e: 0xf181e, + 0xf181f: 0xf181f, + 0xf1820: 0xf1820, + 0xf1821: 0xf1821, + 0xf1822: 0xf1822, + 0xf1823: 0xf1823, + 0xf1824: 0xf1824, + 0xf1825: 0xf1825, + 0xf1826: 0xf1826, + 0xf1827: 0xf1827, + 0xf1828: 0xf1828, + 0xf1829: 0xf1829, + 0xf182a: 0xf182a, + 0xf182b: 0xf182b, + 0xf182c: 0xf182c, + 0xf182d: 0xf182d, + 0xf182e: 0xf182e, + 0xf182f: 0xf182f, + 0xf1830: 0xf1830, + 0xf1831: 0xf1831, + 0xf1832: 0xf1832, + 0xf1833: 0xf1833, + 0xf1834: 0xf1834, + 0xf1835: 0xf1835, + 0xf1836: 0xf1836, + 0xf1837: 0xf1837, + 0xf1838: 0xf1838, + 0xf1839: 0xf1839, + 0xf183a: 0xf183a, + 0xf183b: 0xf183b, + 0xf183c: 0xf183c, + 0xf183d: 0xf183d, + 0xf183e: 0xf183e, + 0xf183f: 0xf183f, + 0xf1840: 0xf1840, + 0xf1841: 0xf1841, + 0xf1842: 0xf1842, + 0xf1843: 0xf1843, + 0xf1844: 0xf1844, + 0xf1845: 0xf1845, + 0xf1846: 0xf1846, + 0xf1847: 0xf1847, + 0xf1848: 0xf1848, + 0xf1849: 0xf1849, + 0xf184a: 0xf184a, + 0xf184b: 0xf184b, + 0xf184c: 0xf184c, + 0xf184d: 0xf184d, + 0xf184e: 0xf184e, + 0xf184f: 0xf184f, + 0xf1850: 0xf1850, + 0xf1851: 0xf1851, + 0xf1852: 0xf1852, + 0xf1853: 0xf1853, + 0xf1854: 0xf1854, + 0xf1855: 0xf1855, + 0xf1856: 0xf1856, + 0xf1857: 0xf1857, + 0xf1858: 0xf1858, + 0xf1859: 0xf1859, + 0xf185a: 0xf185a, + 0xf185b: 0xf185b, + 0xf185c: 0xf185c, + 0xf185d: 0xf185d, + 0xf185e: 0xf185e, + 0xf185f: 0xf185f, + 0xf1860: 0xf1860, + 0xf1861: 0xf1861, + 0xf1862: 0xf1862, + 0xf1863: 0xf1863, + 0xf1864: 0xf1864, + 0xf1865: 0xf1865, + 0xf1866: 0xf1866, + 0xf1867: 0xf1867, + 0xf1868: 0xf1868, + 0xf1869: 0xf1869, + 0xf186a: 0xf186a, + 0xf186b: 0xf186b, + 0xf186c: 0xf186c, + 0xf186d: 0xf186d, + 0xf186e: 0xf186e, + 0xf186f: 0xf186f, + 0xf1870: 0xf1870, + 0xf1871: 0xf1871, + 0xf1872: 0xf1872, + 0xf1873: 0xf1873, + 0xf1874: 0xf1874, + 0xf1875: 0xf1875, + 0xf1876: 0xf1876, + 0xf1877: 0xf1877, + 0xf1878: 0xf1878, + 0xf1879: 0xf1879, + 0xf187a: 0xf187a, + 0xf187b: 0xf187b, + 0xf187c: 0xf187c, + 0xf187d: 0xf187d, + 0xf187e: 0xf187e, + 0xf187f: 0xf187f, + 0xf1880: 0xf1880, + 0xf1881: 0xf1881, + 0xf1882: 0xf1882, + 0xf1883: 0xf1883, + 0xf1884: 0xf1884, + 0xf1885: 0xf1885, + 0xf1886: 0xf1886, + 0xf1887: 0xf1887, + 0xf1888: 0xf1888, + 0xf1889: 0xf1889, + 0xf188a: 0xf188a, + 0xf188b: 0xf188b, + 0xf188c: 0xf188c, + 0xf188d: 0xf188d, + 0xf188e: 0xf188e, + 0xf188f: 0xf188f, + 0xf1890: 0xf1890, + 0xf1891: 0xf1891, + 0xf1892: 0xf1892, + 0xf1893: 0xf1893, + 0xf1894: 0xf1894, + 0xf1895: 0xf1895, + 0xf1896: 0xf1896, + 0xf1897: 0xf1897, + 0xf1898: 0xf1898, + 0xf1899: 0xf1899, + 0xf189a: 0xf189a, + 0xf189b: 0xf189b, + 0xf189c: 0xf189c, + 0xf189d: 0xf189d, + 0xf189e: 0xf189e, + 0xf189f: 0xf189f, + 0xf18a0: 0xf18a0, + 0xf18a1: 0xf18a1, + 0xf18a2: 0xf18a2, + 0xf18a3: 0xf18a3, + 0xf18a4: 0xf18a4, + 0xf18a5: 0xf18a5, + 0xf18a6: 0xf18a6, + 0xf18a7: 0xf18a7, + 0xf18a8: 0xf18a8, + 0xf18a9: 0xf18a9, + 0xf18aa: 0xf18aa, + 0xf18ab: 0xf18ab, + 0xf18ac: 0xf18ac, + 0xf18ad: 0xf18ad, + 0xf18ae: 0xf18ae, + 0xf18af: 0xf18af, + 0xf18b0: 0xf18b0, + 0xf18b1: 0xf18b1, + 0xf18b2: 0xf18b2, + 0xf18b3: 0xf18b3, + 0xf18b4: 0xf18b4, + 0xf18b5: 0xf18b5, + 0xf18b6: 0xf18b6, + 0xf18b7: 0xf18b7, + 0xf18b8: 0xf18b8, + 0xf18b9: 0xf18b9, + 0xf18ba: 0xf18ba, + 0xf18bb: 0xf18bb, + 0xf18bc: 0xf18bc, + 0xf18bd: 0xf18bd, + 0xf18be: 0xf18be, + 0xf18bf: 0xf18bf, + 0xf18c0: 0xf18c0, + 0xf18c1: 0xf18c1, + 0xf18c2: 0xf18c2, + 0xf18c3: 0xf18c3, + 0xf18c4: 0xf18c4, + 0xf18c5: 0xf18c5, + 0xf18c6: 0xf18c6, + 0xf18c7: 0xf18c7, + 0xf18c8: 0xf18c8, + 0xf18c9: 0xf18c9, + 0xf18ca: 0xf18ca, + 0xf18cb: 0xf18cb, + 0xf18cc: 0xf18cc, + 0xf18cd: 0xf18cd, + 0xf18ce: 0xf18ce, + 0xf18cf: 0xf18cf, + 0xf18d0: 0xf18d0, + 0xf18d1: 0xf18d1, + 0xf18d2: 0xf18d2, + 0xf18d3: 0xf18d3, + 0xf18d4: 0xf18d4, + 0xf18d5: 0xf18d5, + 0xf18d6: 0xf18d6, + 0xf18d7: 0xf18d7, + 0xf18d8: 0xf18d8, + 0xf18d9: 0xf18d9, + 0xf18da: 0xf18da, + 0xf18db: 0xf18db, + 0xf18dc: 0xf18dc, + 0xf18dd: 0xf18dd, + 0xf18de: 0xf18de, + 0xf18df: 0xf18df, + 0xf18e0: 0xf18e0, + 0xf18e1: 0xf18e1, + 0xf18e2: 0xf18e2, + 0xf18e3: 0xf18e3, + 0xf18e4: 0xf18e4, + 0xf18e5: 0xf18e5, + 0xf18e6: 0xf18e6, + 0xf18e7: 0xf18e7, + 0xf18e8: 0xf18e8, + 0xf18e9: 0xf18e9, + 0xf18ea: 0xf18ea, + 0xf18eb: 0xf18eb, + 0xf18ec: 0xf18ec, + 0xf18ed: 0xf18ed, + 0xf18ee: 0xf18ee, + 0xf18ef: 0xf18ef, + 0xf18f0: 0xf18f0, + 0xf18f1: 0xf18f1, + 0xf18f2: 0xf18f2, + 0xf18f3: 0xf18f3, + 0xf18f4: 0xf18f4, + 0xf18f5: 0xf18f5, + 0xf18f6: 0xf18f6, + 0xf18f7: 0xf18f7, + 0xf18f8: 0xf18f8, + 0xf18f9: 0xf18f9, + 0xf18fa: 0xf18fa, + 0xf18fb: 0xf18fb, + 0xf18fc: 0xf18fc, + 0xf18fd: 0xf18fd, + 0xf18fe: 0xf18fe, + 0xf18ff: 0xf18ff, + 0xf1900: 0xf1900, + 0xf1901: 0xf1901, + 0xf1902: 0xf1902, + 0xf1903: 0xf1903, + 0xf1904: 0xf1904, + 0xf1905: 0xf1905, + 0xf1906: 0xf1906, + 0xf1907: 0xf1907, + 0xf1908: 0xf1908, + 0xf1909: 0xf1909, + 0xf190a: 0xf190a, + 0xf190b: 0xf190b, + 0xf190c: 0xf190c, + 0xf190d: 0xf190d, + 0xf190e: 0xf190e, + 0xf190f: 0xf190f, + 0xf1910: 0xf1910, + 0xf1911: 0xf1911, + 0xf1912: 0xf1912, + 0xf1913: 0xf1913, + 0xf1914: 0xf1914, + 0xf1915: 0xf1915, + 0xf1916: 0xf1916, + 0xf1917: 0xf1917, + 0xf1918: 0xf1918, + 0xf1919: 0xf1919, + 0xf191a: 0xf191a, + 0xf191b: 0xf191b, + 0xf191c: 0xf191c, + 0xf191d: 0xf191d, + 0xf191e: 0xf191e, + 0xf191f: 0xf191f, + 0xf1920: 0xf1920, + 0xf1921: 0xf1921, + 0xf1922: 0xf1922, + 0xf1923: 0xf1923, + 0xf1924: 0xf1924, + 0xf1925: 0xf1925, + 0xf1926: 0xf1926, + 0xf1927: 0xf1927, + 0xf1928: 0xf1928, + 0xf1929: 0xf1929, + 0xf192a: 0xf192a, + 0xf192b: 0xf192b, + 0xf192c: 0xf192c, + 0xf192d: 0xf192d, + 0xf192e: 0xf192e, + 0xf192f: 0xf192f, + 0xf1930: 0xf1930, + 0xf1931: 0xf1931, + 0xf1932: 0xf1932, + 0xf1933: 0xf1933, + 0xf1934: 0xf1934, + 0xf1935: 0xf1935, + 0xf1936: 0xf1936, + 0xf1937: 0xf1937, + 0xf1938: 0xf1938, + 0xf1939: 0xf1939, + 0xf193a: 0xf193a, + 0xf193b: 0xf193b, + 0xf193c: 0xf193c, + 0xf193d: 0xf193d, + 0xf193e: 0xf193e, + 0xf193f: 0xf193f, + 0xf1940: 0xf1940, + 0xf1941: 0xf1941, + 0xf1942: 0xf1942, + 0xf1943: 0xf1943, + 0xf1944: 0xf1944, + 0xf1945: 0xf1945, + 0xf1946: 0xf1946, + 0xf1947: 0xf1947, + 0xf1948: 0xf1948, + 0xf1949: 0xf1949, + 0xf194a: 0xf194a, + 0xf194b: 0xf194b, + 0xf194c: 0xf194c, + 0xf194d: 0xf194d, + 0xf194e: 0xf194e, + 0xf194f: 0xf194f, + 0xf1950: 0xf1950, + 0xf1951: 0xf1951, + 0xf1952: 0xf1952, + 0xf1953: 0xf1953, + 0xf1954: 0xf1954, + 0xf1955: 0xf1955, + 0xf1956: 0xf1956, + 0xf1957: 0xf1957, + 0xf1958: 0xf1958, + 0xf1959: 0xf1959, + 0xf195a: 0xf195a, + 0xf195b: 0xf195b, + 0xf195c: 0xf195c, + 0xf195d: 0xf195d, + 0xf195e: 0xf195e, + 0xf195f: 0xf195f, + 0xf1960: 0xf1960, + 0xf1961: 0xf1961, + 0xf1962: 0xf1962, + 0xf1963: 0xf1963, + 0xf1964: 0xf1964, + 0xf1965: 0xf1965, + 0xf1966: 0xf1966, + 0xf1967: 0xf1967, + 0xf1968: 0xf1968, + 0xf1969: 0xf1969, + 0xf196a: 0xf196a, + 0xf196b: 0xf196b, + 0xf196c: 0xf196c, + 0xf196d: 0xf196d, + 0xf196e: 0xf196e, + 0xf196f: 0xf196f, + 0xf1970: 0xf1970, + 0xf1971: 0xf1971, + 0xf1972: 0xf1972, + 0xf1973: 0xf1973, + 0xf1974: 0xf1974, + 0xf1975: 0xf1975, + 0xf1976: 0xf1976, + 0xf1977: 0xf1977, + 0xf1978: 0xf1978, + 0xf1979: 0xf1979, + 0xf197a: 0xf197a, + 0xf197b: 0xf197b, + 0xf197c: 0xf197c, + 0xf197d: 0xf197d, + 0xf197e: 0xf197e, + 0xf197f: 0xf197f, + 0xf1980: 0xf1980, + 0xf1981: 0xf1981, + 0xf1982: 0xf1982, + 0xf1983: 0xf1983, + 0xf1984: 0xf1984, + 0xf1985: 0xf1985, + 0xf1986: 0xf1986, + 0xf1987: 0xf1987, + 0xf1988: 0xf1988, + 0xf1989: 0xf1989, + 0xf198a: 0xf198a, + 0xf198b: 0xf198b, + 0xf198c: 0xf198c, + 0xf198d: 0xf198d, + 0xf198e: 0xf198e, + 0xf198f: 0xf198f, + 0xf1990: 0xf1990, + 0xf1991: 0xf1991, + 0xf1992: 0xf1992, + 0xf1993: 0xf1993, + 0xf1994: 0xf1994, + 0xf1995: 0xf1995, + 0xf1996: 0xf1996, + 0xf1997: 0xf1997, + 0xf1998: 0xf1998, + 0xf1999: 0xf1999, + 0xf199a: 0xf199a, + 0xf199b: 0xf199b, + 0xf199c: 0xf199c, + 0xf199d: 0xf199d, + 0xf199e: 0xf199e, + 0xf199f: 0xf199f, + 0xf19a0: 0xf19a0, + 0xf19a1: 0xf19a1, + 0xf19a2: 0xf19a2, + 0xf19a3: 0xf19a3, + 0xf19a4: 0xf19a4, + 0xf19a5: 0xf19a5, + 0xf19a6: 0xf19a6, + 0xf19a7: 0xf19a7, + 0xf19a8: 0xf19a8, + 0xf19a9: 0xf19a9, + 0xf19aa: 0xf19aa, + 0xf19ab: 0xf19ab, + 0xf19ac: 0xf19ac, + 0xf19ad: 0xf19ad, + 0xf19ae: 0xf19ae, + 0xf19af: 0xf19af, + 0xf19b0: 0xf19b0, + 0xf19b1: 0xf19b1, + 0xf19b2: 0xf19b2, + 0xf19b3: 0xf19b3, + 0xf19b4: 0xf19b4, + 0xf19b5: 0xf19b5, + 0xf19b6: 0xf19b6, + 0xf19b7: 0xf19b7, + 0xf19b8: 0xf19b8, + 0xf19b9: 0xf19b9, + 0xf19ba: 0xf19ba, + 0xf19bb: 0xf19bb, + 0xf19bc: 0xf19bc, + 0xf19bd: 0xf19bd, + 0xf19be: 0xf19be, + 0xf19bf: 0xf19bf, + 0xf19c0: 0xf19c0, + 0xf19c1: 0xf19c1, + 0xf19c2: 0xf19c2, + 0xf19c3: 0xf19c3, + 0xf19c4: 0xf19c4, + 0xf19c5: 0xf19c5, + 0xf19c6: 0xf19c6, + 0xf19c7: 0xf19c7, + 0xf19c8: 0xf19c8, + 0xf19c9: 0xf19c9, + 0xf19ca: 0xf19ca, + 0xf19cb: 0xf19cb, + 0xf19cc: 0xf19cc, + 0xf19cd: 0xf19cd, + 0xf19ce: 0xf19ce, + 0xf19cf: 0xf19cf, + 0xf19d0: 0xf19d0, + 0xf19d1: 0xf19d1, + 0xf19d2: 0xf19d2, + 0xf19d3: 0xf19d3, + 0xf19d4: 0xf19d4, + 0xf19d5: 0xf19d5, + 0xf19d6: 0xf19d6, + 0xf19d7: 0xf19d7, + 0xf19d8: 0xf19d8, + 0xf19d9: 0xf19d9, + 0xf19da: 0xf19da, + 0xf19db: 0xf19db, + 0xf19dc: 0xf19dc, + 0xf19dd: 0xf19dd, + 0xf19de: 0xf19de, + 0xf19df: 0xf19df, + 0xf19e0: 0xf19e0, + 0xf19e1: 0xf19e1, + 0xf19e2: 0xf19e2, + 0xf19e3: 0xf19e3, + 0xf19e4: 0xf19e4, + 0xf19e5: 0xf19e5, + 0xf19e6: 0xf19e6, + 0xf19e7: 0xf19e7, + 0xf19e8: 0xf19e8, + 0xf19e9: 0xf19e9, + 0xf19ea: 0xf19ea, + 0xf19eb: 0xf19eb, + 0xf19ec: 0xf19ec, + 0xf19ed: 0xf19ed, + 0xf19ee: 0xf19ee, + 0xf19ef: 0xf19ef, + 0xf19f0: 0xf19f0, + 0xf19f1: 0xf19f1, + 0xf19f2: 0xf19f2, + 0xf19f3: 0xf19f3, + 0xf19f4: 0xf19f4, + 0xf19f5: 0xf19f5, + 0xf19f6: 0xf19f6, + 0xf19f7: 0xf19f7, + 0xf19f8: 0xf19f8, + 0xf19f9: 0xf19f9, + 0xf19fa: 0xf19fa, + 0xf19fb: 0xf19fb, + 0xf19fc: 0xf19fc, + 0xf19fd: 0xf19fd, + 0xf19fe: 0xf19fe, + 0xf19ff: 0xf19ff, + 0xf1a00: 0xf1a00, + 0xf1a01: 0xf1a01, + 0xf1a02: 0xf1a02, + 0xf1a03: 0xf1a03, + 0xf1a04: 0xf1a04, + 0xf1a05: 0xf1a05, + 0xf1a06: 0xf1a06, + 0xf1a07: 0xf1a07, + 0xf1a08: 0xf1a08, + 0xf1a09: 0xf1a09, + 0xf1a0a: 0xf1a0a, + 0xf1a0b: 0xf1a0b, + 0xf1a0c: 0xf1a0c, + 0xf1a0d: 0xf1a0d, + 0xf1a0e: 0xf1a0e, + 0xf1a0f: 0xf1a0f, + 0xf1a10: 0xf1a10, + 0xf1a11: 0xf1a11, + 0xf1a12: 0xf1a12, + 0xf1a13: 0xf1a13, + 0xf1a14: 0xf1a14, + 0xf1a15: 0xf1a15, + 0xf1a16: 0xf1a16, + 0xf1a17: 0xf1a17, + 0xf1a18: 0xf1a18, + 0xf1a19: 0xf1a19, + 0xf1a1a: 0xf1a1a, + 0xf1a1b: 0xf1a1b, + 0xf1a1c: 0xf1a1c, + 0xf1a1d: 0xf1a1d, + 0xf1a1e: 0xf1a1e, + 0xf1a1f: 0xf1a1f, + 0xf1a20: 0xf1a20, + 0xf1a21: 0xf1a21, + 0xf1a22: 0xf1a22, + 0xf1a23: 0xf1a23, + 0xf1a24: 0xf1a24, + 0xf1a25: 0xf1a25, + 0xf1a26: 0xf1a26, + 0xf1a27: 0xf1a27, + 0xf1a28: 0xf1a28, + 0xf1a29: 0xf1a29, + 0xf1a2a: 0xf1a2a, + 0xf1a2b: 0xf1a2b, + 0xf1a2c: 0xf1a2c, + 0xf1a2d: 0xf1a2d, + 0xf1a2e: 0xf1a2e, + 0xf1a2f: 0xf1a2f, + 0xf1a30: 0xf1a30, + 0xf1a31: 0xf1a31, + 0xf1a32: 0xf1a32, + 0xf1a33: 0xf1a33, + 0xf1a34: 0xf1a34, + 0xf1a35: 0xf1a35, + 0xf1a36: 0xf1a36, + 0xf1a37: 0xf1a37, + 0xf1a38: 0xf1a38, + 0xf1a39: 0xf1a39, + 0xf1a3a: 0xf1a3a, + 0xf1a3b: 0xf1a3b, + 0xf1a3c: 0xf1a3c, + 0xf1a3d: 0xf1a3d, + 0xf1a3e: 0xf1a3e, + 0xf1a3f: 0xf1a3f, + 0xf1a40: 0xf1a40, + 0xf1a41: 0xf1a41, + 0xf1a42: 0xf1a42, + 0xf1a43: 0xf1a43, + 0xf1a44: 0xf1a44, + 0xf1a45: 0xf1a45, + 0xf1a46: 0xf1a46, + 0xf1a47: 0xf1a47, + 0xf1a48: 0xf1a48, + 0xf1a49: 0xf1a49, + 0xf1a4a: 0xf1a4a, + 0xf1a4b: 0xf1a4b, + 0xf1a4c: 0xf1a4c, + 0xf1a4d: 0xf1a4d, + 0xf1a4e: 0xf1a4e, + 0xf1a4f: 0xf1a4f, + 0xf1a50: 0xf1a50, + 0xf1a51: 0xf1a51, + 0xf1a52: 0xf1a52, + 0xf1a53: 0xf1a53, + 0xf1a54: 0xf1a54, + 0xf1a55: 0xf1a55, + 0xf1a56: 0xf1a56, + 0xf1a57: 0xf1a57, + 0xf1a58: 0xf1a58, + 0xf1a59: 0xf1a59, + 0xf1a5a: 0xf1a5a, + 0xf1a5b: 0xf1a5b, + 0xf1a5c: 0xf1a5c, + 0xf1a5d: 0xf1a5d, + 0xf1a5e: 0xf1a5e, + 0xf1a5f: 0xf1a5f, + 0xf1a60: 0xf1a60, + 0xf1a61: 0xf1a61, + 0xf1a62: 0xf1a62, + 0xf1a63: 0xf1a63, + 0xf1a64: 0xf1a64, + 0xf1a65: 0xf1a65, + 0xf1a66: 0xf1a66, + 0xf1a67: 0xf1a67, + 0xf1a68: 0xf1a68, + 0xf1a69: 0xf1a69, + 0xf1a6a: 0xf1a6a, + 0xf1a6b: 0xf1a6b, + 0xf1a6c: 0xf1a6c, + 0xf1a6d: 0xf1a6d, + 0xf1a6e: 0xf1a6e, + 0xf1a6f: 0xf1a6f, + 0xf1a70: 0xf1a70, + 0xf1a71: 0xf1a71, + 0xf1a72: 0xf1a72, + 0xf1a73: 0xf1a73, + 0xf1a74: 0xf1a74, + 0xf1a75: 0xf1a75, + 0xf1a76: 0xf1a76, + 0xf1a77: 0xf1a77, + 0xf1a78: 0xf1a78, + 0xf1a79: 0xf1a79, + 0xf1a7a: 0xf1a7a, + 0xf1a7b: 0xf1a7b, + 0xf1a7c: 0xf1a7c, + 0xf1a7d: 0xf1a7d, + 0xf1a7e: 0xf1a7e, + 0xf1a7f: 0xf1a7f, + 0xf1a80: 0xf1a80, + 0xf1a81: 0xf1a81, + 0xf1a82: 0xf1a82, + 0xf1a83: 0xf1a83, + 0xf1a84: 0xf1a84, + 0xf1a85: 0xf1a85, + 0xf1a86: 0xf1a86, + 0xf1a87: 0xf1a87, + 0xf1a88: 0xf1a88, + 0xf1a89: 0xf1a89, + 0xf1a8a: 0xf1a8a, + 0xf1a8b: 0xf1a8b, + 0xf1a8c: 0xf1a8c, + 0xf1a8d: 0xf1a8d, + 0xf1a8e: 0xf1a8e, + 0xf1a8f: 0xf1a8f, + 0xf1a90: 0xf1a90, + 0xf1a91: 0xf1a91, + 0xf1a92: 0xf1a92, + 0xf1a93: 0xf1a93, + 0xf1a94: 0xf1a94, + 0xf1a95: 0xf1a95, + 0xf1a96: 0xf1a96, + 0xf1a97: 0xf1a97, + 0xf1a98: 0xf1a98, + 0xf1a99: 0xf1a99, + 0xf1a9a: 0xf1a9a, + 0xf1a9b: 0xf1a9b, + 0xf1a9c: 0xf1a9c, + 0xf1a9d: 0xf1a9d, + 0xf1a9e: 0xf1a9e, + 0xf1a9f: 0xf1a9f, + 0xf1aa0: 0xf1aa0, + 0xf1aa1: 0xf1aa1, + 0xf1aa2: 0xf1aa2, + 0xf1aa3: 0xf1aa3, + 0xf1aa4: 0xf1aa4, + 0xf1aa5: 0xf1aa5, + 0xf1aa6: 0xf1aa6, + 0xf1aa7: 0xf1aa7, + 0xf1aa8: 0xf1aa8, + 0xf1aa9: 0xf1aa9, + 0xf1aaa: 0xf1aaa, + 0xf1aab: 0xf1aab, + 0xf1aac: 0xf1aac, + 0xf1aad: 0xf1aad, + 0xf1aae: 0xf1aae, + 0xf1aaf: 0xf1aaf, + 0xf1ab0: 0xf1ab0, + 0xf1ab1: 0xf1ab1, + 0xf1ab2: 0xf1ab2, + 0xf1ab3: 0xf1ab3, + 0xf1ab4: 0xf1ab4, + 0xf1ab5: 0xf1ab5, + 0xf1ab6: 0xf1ab6, + 0xf1ab7: 0xf1ab7, + 0xf1ab8: 0xf1ab8, + 0xf1ab9: 0xf1ab9, + 0xf1aba: 0xf1aba, + 0xf1abb: 0xf1abb, + 0xf1abc: 0xf1abc, + 0xf1abd: 0xf1abd, + 0xf1abe: 0xf1abe, + 0xf1abf: 0xf1abf, + 0xf1ac0: 0xf1ac0, + 0xf1ac1: 0xf1ac1, + 0xf1ac2: 0xf1ac2, + 0xf1ac3: 0xf1ac3, + 0xf1ac4: 0xf1ac4, + 0xf1ac5: 0xf1ac5, + 0xf1ac6: 0xf1ac6, + 0xf1ac7: 0xf1ac7, + 0xf1ac8: 0xf1ac8, + 0xf1ac9: 0xf1ac9, + 0xf1aca: 0xf1aca, + 0xf1acb: 0xf1acb, + 0xf1acc: 0xf1acc, + 0xf1acd: 0xf1acd, + 0xf1ace: 0xf1ace, + 0xf1acf: 0xf1acf, + 0xf1ad0: 0xf1ad0, + 0xf1ad1: 0xf1ad1, + 0xf1ad2: 0xf1ad2, + 0xf1ad3: 0xf1ad3, + 0xf1ad4: 0xf1ad4, + 0xf1ad5: 0xf1ad5, + 0xf1ad6: 0xf1ad6, + 0xf1ad7: 0xf1ad7, + 0xf1ad8: 0xf1ad8, + 0xf1ad9: 0xf1ad9, + 0xf1ada: 0xf1ada, + 0xf1adb: 0xf1adb, + 0xf1adc: 0xf1adc, + 0xf1add: 0xf1add, + 0xf1ade: 0xf1ade, + 0xf1adf: 0xf1adf, + 0xf1ae0: 0xf1ae0, + 0xf1ae1: 0xf1ae1, + 0xf1ae2: 0xf1ae2, + 0xf1ae3: 0xf1ae3, + 0xf1ae4: 0xf1ae4, + 0xf1ae5: 0xf1ae5, + 0xf1ae6: 0xf1ae6, + 0xf1ae7: 0xf1ae7, + 0xf1ae8: 0xf1ae8, + 0xf1ae9: 0xf1ae9, + 0xf1aea: 0xf1aea, + 0xf1aeb: 0xf1aeb, + 0xf1aec: 0xf1aec, + 0xf1aed: 0xf1aed, + 0xf1aee: 0xf1aee, + 0xf1aef: 0xf1aef, + 0xf1af0: 0xf1af0, + }, + "Weather Icons": { + 0xf000: 0xe300, + 0xf001: 0xe301, + 0xf002: 0xe302, + 0xf003: 0xe303, + 0xf004: 0xe304, + 0xf005: 0xe305, + 0xf006: 0xe306, + 0xf007: 0xe307, + 0xf008: 0xe308, + 0xf009: 0xe309, + 0xf00a: 0xe30a, + 0xf00b: 0xe30b, + 0xf00c: 0xe30c, + 0xf00d: 0xe30d, + 0xf00e: 0xe30e, + 0xf010: 0xe30f, + 0xf011: 0xe310, + 0xf012: 0xe311, + 0xf013: 0xe312, + 0xf014: 0xe313, + 0xf015: 0xe314, + 0xf016: 0xe315, + 0xf017: 0xe316, + 0xf018: 0xe317, + 0xf019: 0xe318, + 0xf01a: 0xe319, + 0xf01b: 0xe31a, + 0xf01c: 0xe31b, + 0xf01d: 0xe31c, + 0xf01e: 0xe31d, + 0xf021: 0xe31e, + 0xf022: 0xe31f, + 0xf023: 0xe320, + 0xf024: 0xe321, + 0xf025: 0xe322, + 0xf026: 0xe323, + 0xf027: 0xe324, + 0xf028: 0xe325, + 0xf029: 0xe326, + 0xf02a: 0xe327, + 0xf02b: 0xe328, + 0xf02c: 0xe329, + 0xf02d: 0xe32a, + 0xf02e: 0xe32b, + 0xf02f: 0xe32c, + 0xf030: 0xe32d, + 0xf031: 0xe32e, + 0xf032: 0xe32f, + 0xf033: 0xe330, + 0xf034: 0xe331, + 0xf035: 0xe332, + 0xf036: 0xe333, + 0xf037: 0xe334, + 0xf038: 0xe335, + 0xf039: 0xe336, + 0xf03a: 0xe337, + 0xf03b: 0xe338, + 0xf03c: 0xe339, + 0xf03d: 0xe33a, + 0xf03e: 0xe33b, + 0xf040: 0xe33c, + 0xf041: 0xe33d, + 0xf042: 0xe33e, + 0xf043: 0xe33f, + 0xf044: 0xe340, + 0xf045: 0xe341, + 0xf046: 0xe342, + 0xf047: 0xe343, + 0xf048: 0xe344, + 0xf049: 0xe345, + 0xf04a: 0xe346, + 0xf04b: 0xe347, + 0xf04c: 0xe348, + 0xf04d: 0xe349, + 0xf04e: 0xe34a, + 0xf050: 0xe34b, + 0xf051: 0xe34c, + 0xf052: 0xe34d, + 0xf053: 0xe34e, + 0xf054: 0xe34f, + 0xf055: 0xe350, + 0xf056: 0xe351, + 0xf057: 0xe352, + 0xf058: 0xe353, + 0xf059: 0xe354, + 0xf05a: 0xe355, + 0xf05b: 0xe356, + 0xf05c: 0xe357, + 0xf05d: 0xe358, + 0xf05e: 0xe359, + 0xf060: 0xe35a, + 0xf061: 0xe35b, + 0xf062: 0xe35c, + 0xf063: 0xe35d, + 0xf064: 0xe35e, + 0xf065: 0xe35f, + 0xf066: 0xe360, + 0xf067: 0xe361, + 0xf068: 0xe362, + 0xf069: 0xe363, + 0xf06a: 0xe364, + 0xf06b: 0xe365, + 0xf06c: 0xe366, + 0xf06d: 0xe367, + 0xf06e: 0xe368, + 0xf070: 0xe369, + 0xf071: 0xe36a, + 0xf072: 0xe36b, + 0xf073: 0xe36c, + 0xf074: 0xe36d, + 0xf075: 0xe36e, + 0xf076: 0xe36f, + 0xf077: 0xe370, + 0xf078: 0xe371, + 0xf079: 0xe372, + 0xf07a: 0xe373, + 0xf07b: 0xe374, + 0xf07c: 0xe375, + 0xf07d: 0xe376, + 0xf07e: 0xe377, + 0xf080: 0xe378, + 0xf081: 0xe379, + 0xf082: 0xe37a, + 0xf083: 0xe37b, + 0xf084: 0xe37c, + 0xf085: 0xe37d, + 0xf086: 0xe37e, + 0xf087: 0xe37f, + 0xf088: 0xe380, + 0xf089: 0xe381, + 0xf08a: 0xe382, + 0xf08b: 0xe383, + 0xf08c: 0xe384, + 0xf08d: 0xe385, + 0xf08e: 0xe386, + 0xf08f: 0xe387, + 0xf090: 0xe388, + 0xf091: 0xe389, + 0xf092: 0xe38a, + 0xf093: 0xe38b, + 0xf094: 0xe38c, + 0xf095: 0xe38d, + 0xf096: 0xe38e, + 0xf097: 0xe38f, + 0xf098: 0xe390, + 0xf099: 0xe391, + 0xf09a: 0xe392, + 0xf09b: 0xe393, + 0xf09c: 0xe394, + 0xf09d: 0xe395, + 0xf09e: 0xe396, + 0xf09f: 0xe397, + 0xf0a0: 0xe398, + 0xf0a1: 0xe399, + 0xf0a2: 0xe39a, + 0xf0a3: 0xe39b, + 0xf0a4: 0xe39c, + 0xf0a5: 0xe39d, + 0xf0a6: 0xe39e, + 0xf0a7: 0xe39f, + 0xf0a8: 0xe3a0, + 0xf0a9: 0xe3a1, + 0xf0aa: 0xe3a2, + 0xf0ab: 0xe3a3, + 0xf0ac: 0xe3a4, + 0xf0ad: 0xe3a5, + 0xf0ae: 0xe3a6, + 0xf0af: 0xe3a7, + 0xf0b0: 0xe3a8, + 0xf0b1: 0xe3a9, + 0xf0b2: 0xe3aa, + 0xf0b3: 0xe3ab, + 0xf0b4: 0xe3ac, + 0xf0b5: 0xe3ad, + 0xf0b6: 0xe3ae, + 0xf0b7: 0xe3af, + 0xf0b8: 0xe3b0, + 0xf0b9: 0xe3b1, + 0xf0ba: 0xe3b2, + 0xf0bb: 0xe3b3, + 0xf0bc: 0xe3b4, + 0xf0bd: 0xe3b5, + 0xf0be: 0xe3b6, + 0xf0bf: 0xe3b7, + 0xf0c0: 0xe3b8, + 0xf0c1: 0xe3b9, + 0xf0c2: 0xe3ba, + 0xf0c3: 0xe3bb, + 0xf0c4: 0xe3bc, + 0xf0c5: 0xe3bd, + 0xf0c6: 0xe3be, + 0xf0c7: 0xe3bf, + 0xf0c8: 0xe3c0, + 0xf0c9: 0xe3c1, + 0xf0ca: 0xe3c2, + 0xf0cb: 0xe3c3, + 0xf0cc: 0xe3c4, + 0xf0cd: 0xe3c5, + 0xf0ce: 0xe3c6, + 0xf0cf: 0xe3c7, + 0xf0d0: 0xe3c8, + 0xf0d1: 0xe3c9, + 0xf0d2: 0xe3ca, + 0xf0d3: 0xe3cb, + 0xf0d4: 0xe3cc, + 0xf0d5: 0xe3cd, + 0xf0d6: 0xe3ce, + 0xf0d7: 0xe3cf, + 0xf0d8: 0xe3d0, + 0xf0d9: 0xe3d1, + 0xf0da: 0xe3d2, + 0xf0db: 0xe3d3, + 0xf0dc: 0xe3d4, + 0xf0dd: 0xe3d5, + 0xf0de: 0xe3d6, + 0xf0df: 0xe3d7, + 0xf0e0: 0xe3d8, + 0xf0e1: 0xe3d9, + 0xf0e2: 0xe3da, + 0xf0e3: 0xe3db, + 0xf0e4: 0xe3dc, + 0xf0e5: 0xe3dd, + 0xf0e6: 0xe3de, + 0xf0e7: 0xe3df, + 0xf0e8: 0xe3e0, + 0xf0e9: 0xe3e1, + 0xf0ea: 0xe3e2, + 0xf0eb: 0xe3e3, + }, + "Font Logos": { + 0xf300: 0xf300, + 0xf301: 0xf301, + 0xf302: 0xf302, + 0xf303: 0xf303, + 0xf304: 0xf304, + 0xf305: 0xf305, + 0xf306: 0xf306, + 0xf307: 0xf307, + 0xf308: 0xf308, + 0xf309: 0xf309, + 0xf30a: 0xf30a, + 0xf30b: 0xf30b, + 0xf30c: 0xf30c, + 0xf30d: 0xf30d, + 0xf30e: 0xf30e, + 0xf30f: 0xf30f, + 0xf310: 0xf310, + 0xf311: 0xf311, + 0xf312: 0xf312, + 0xf313: 0xf313, + 0xf314: 0xf314, + 0xf315: 0xf315, + 0xf316: 0xf316, + 0xf317: 0xf317, + 0xf318: 0xf318, + 0xf319: 0xf319, + 0xf31a: 0xf31a, + 0xf31b: 0xf31b, + 0xf31c: 0xf31c, + 0xf31d: 0xf31d, + 0xf31e: 0xf31e, + 0xf31f: 0xf31f, + 0xf320: 0xf320, + 0xf321: 0xf321, + 0xf322: 0xf322, + 0xf323: 0xf323, + 0xf324: 0xf324, + 0xf325: 0xf325, + 0xf326: 0xf326, + 0xf327: 0xf327, + 0xf328: 0xf328, + 0xf329: 0xf329, + 0xf32a: 0xf32a, + 0xf32b: 0xf32b, + 0xf32c: 0xf32c, + 0xf32d: 0xf32d, + 0xf32e: 0xf32e, + 0xf32f: 0xf32f, + 0xf330: 0xf330, + 0xf331: 0xf331, + 0xf332: 0xf332, + 0xf333: 0xf333, + 0xf334: 0xf334, + 0xf335: 0xf335, + 0xf336: 0xf336, + 0xf337: 0xf337, + 0xf338: 0xf338, + 0xf339: 0xf339, + 0xf33a: 0xf33a, + 0xf33b: 0xf33b, + 0xf33c: 0xf33c, + 0xf33d: 0xf33d, + 0xf33e: 0xf33e, + 0xf33f: 0xf33f, + 0xf340: 0xf340, + 0xf341: 0xf341, + 0xf342: 0xf342, + 0xf343: 0xf343, + 0xf344: 0xf344, + 0xf345: 0xf345, + 0xf346: 0xf346, + 0xf347: 0xf347, + 0xf348: 0xf348, + 0xf349: 0xf349, + 0xf34a: 0xf34a, + 0xf34b: 0xf34b, + 0xf34c: 0xf34c, + 0xf34d: 0xf34d, + 0xf34e: 0xf34e, + 0xf34f: 0xf34f, + 0xf350: 0xf350, + 0xf351: 0xf351, + 0xf352: 0xf352, + 0xf353: 0xf353, + 0xf354: 0xf354, + 0xf355: 0xf355, + 0xf356: 0xf356, + 0xf357: 0xf357, + 0xf358: 0xf358, + 0xf359: 0xf359, + 0xf35a: 0xf35a, + 0xf35b: 0xf35b, + 0xf35c: 0xf35c, + 0xf35d: 0xf35d, + 0xf35e: 0xf35e, + 0xf35f: 0xf35f, + 0xf360: 0xf360, + 0xf361: 0xf361, + 0xf362: 0xf362, + 0xf363: 0xf363, + 0xf364: 0xf364, + 0xf365: 0xf365, + 0xf366: 0xf366, + 0xf367: 0xf367, + 0xf368: 0xf368, + 0xf369: 0xf369, + 0xf36a: 0xf36a, + 0xf36b: 0xf36b, + 0xf36c: 0xf36c, + 0xf36d: 0xf36d, + 0xf36e: 0xf36e, + 0xf36f: 0xf36f, + 0xf370: 0xf370, + 0xf371: 0xf371, + 0xf372: 0xf372, + 0xf373: 0xf373, + 0xf374: 0xf374, + 0xf375: 0xf375, + 0xf376: 0xf376, + 0xf377: 0xf377, + 0xf378: 0xf378, + 0xf379: 0xf379, + 0xf37a: 0xf37a, + 0xf37b: 0xf37b, + 0xf37c: 0xf37c, + 0xf37d: 0xf37d, + 0xf37e: 0xf37e, + 0xf37f: 0xf37f, + 0xf380: 0xf380, + 0xf381: 0xf381, + }, + "Octicons": { + 0xf000: 0xf400, + 0xf001: 0xf401, + 0xf002: 0xf402, + 0xf005: 0xf403, + 0xf006: 0xf404, + 0xf007: 0xf405, + 0xf008: 0xf406, + 0xf009: 0xf407, + 0xf00a: 0xf408, + 0xf00b: 0xf409, + 0xf00c: 0xf40a, + 0xf00d: 0xf40b, + 0xf00e: 0xf40c, + 0xf010: 0xf40d, + 0xf011: 0xf40e, + 0xf012: 0xf40f, + 0xf013: 0xf410, + 0xf014: 0xf411, + 0xf015: 0xf412, + 0xf016: 0xf413, + 0xf017: 0xf414, + 0xf018: 0xf415, + 0xf019: 0xf416, + 0xf01f: 0xf417, + 0xf020: 0xf418, + 0xf023: 0xf419, + 0xf024: 0xf41a, + 0xf026: 0xf41b, + 0xf027: 0xf41c, + 0xf028: 0xf41d, + 0xf02a: 0xf41e, + 0xf02b: 0xf41f, + 0xf02c: 0xf420, + 0xf02d: 0xf421, + 0xf02e: 0xf422, + 0xf02f: 0xf423, + 0xf030: 0xf424, + 0xf031: 0xf425, + 0xf032: 0xf426, + 0xf033: 0xf427, + 0xf034: 0xf428, + 0xf035: 0xf429, + 0xf036: 0xf42a, + 0xf037: 0xf42b, + 0xf038: 0xf42c, + 0xf039: 0xf42d, + 0xf03a: 0xf42e, + 0xf03b: 0xf42f, + 0xf03c: 0xf430, + 0xf03d: 0xf431, + 0xf03e: 0xf432, + 0xf03f: 0xf433, + 0xf040: 0xf434, + 0xf041: 0xf435, + 0xf042: 0xf436, + 0xf043: 0xf437, + 0xf044: 0xf438, + 0xf045: 0xf439, + 0xf046: 0xf43a, + 0xf047: 0xf43b, + 0xf048: 0xf43c, + 0xf049: 0xf43d, + 0xf04a: 0xf43e, + 0xf04c: 0xf43f, + 0xf04d: 0xf440, + 0xf04e: 0xf441, + 0xf04f: 0xf442, + 0xf051: 0xf443, + 0xf052: 0xf444, + 0xf053: 0xf445, + 0xf056: 0xf446, + 0xf057: 0xf447, + 0xf058: 0xf448, + 0xf059: 0xf449, + 0xf05a: 0xf44a, + 0xf05b: 0xf44b, + 0xf05c: 0xf44c, + 0xf05d: 0xf44d, + 0xf05e: 0xf44e, + 0xf05f: 0xf44f, + 0xf060: 0xf450, + 0xf061: 0xf451, + 0xf062: 0xf452, + 0xf063: 0xf453, + 0xf064: 0xf454, + 0xf068: 0xf455, + 0xf06a: 0xf456, + 0xf06b: 0xf457, + 0xf06c: 0xf458, + 0xf06d: 0xf459, + 0xf06e: 0xf45a, + 0xf070: 0xf45b, + 0xf071: 0xf45c, + 0xf075: 0xf45d, + 0xf076: 0xf45e, + 0xf077: 0xf45f, + 0xf078: 0xf460, + 0xf07b: 0xf461, + 0xf07c: 0xf462, + 0xf07d: 0xf463, + 0xf07e: 0xf464, + 0xf07f: 0xf465, + 0xf080: 0xf466, + 0xf081: 0xf467, + 0xf084: 0xf468, + 0xf085: 0xf469, + 0xf087: 0xf46a, + 0xf088: 0xf46b, + 0xf08c: 0xf46c, + 0xf08d: 0xf46d, + 0xf08f: 0xf46e, + 0xf091: 0xf46f, + 0xf092: 0xf470, + 0xf094: 0xf471, + 0xf096: 0xf472, + 0xf097: 0xf473, + 0xf099: 0xf474, + 0xf09a: 0xf475, + 0xf09c: 0xf476, + 0xf09d: 0xf477, + 0xf09f: 0xf478, + 0xf0a0: 0xf479, + 0xf0a1: 0xf47a, + 0xf0a2: 0xf47b, + 0xf0a3: 0xf47c, + 0xf0a4: 0xf47d, + 0xf0aa: 0xf47e, + 0xf0ac: 0xf47f, + 0xf0ad: 0xf480, + 0xf0b0: 0xf481, + 0xf0b1: 0xf482, + 0xf0b2: 0xf483, + 0xf0b6: 0xf484, + 0xf0ba: 0xf485, + 0xf0be: 0xf486, + 0xf0c4: 0xf487, + 0xf0c5: 0xf488, + 0xf0c8: 0xf489, + 0xf0c9: 0xf48a, + 0xf0ca: 0xf48b, + 0xf0cc: 0xf48c, + 0xf0cf: 0xf48d, + 0xf0d0: 0xf48e, + 0xf0d1: 0xf48f, + 0xf0d2: 0xf490, + 0xf0d3: 0xf491, + 0xf0d4: 0xf492, + 0xf0d6: 0xf493, + 0xf0d7: 0xf494, + 0xf0d8: 0xf495, + 0xf0da: 0xf496, + 0xf0db: 0xf497, + 0xf0dc: 0xf498, + 0xf0dd: 0xf499, + 0xf0de: 0xf49a, + 0xf0e0: 0xf49b, + 0xf0e1: 0xf49c, + 0xf0e2: 0xf49d, + 0xf0e3: 0xf49e, + 0xf0e4: 0xf49f, + 0xf0e5: 0xf4a0, + 0xf0e6: 0xf4a1, + 0xf0e7: 0xf4a2, + 0xf0e8: 0xf4a3, + 0xf101: 0xf4a4, + 0xf102: 0xf4a5, + 0xf103: 0xf4a6, + 0xf104: 0xf4a7, + 0xf105: 0xf4a8, + 0x2665: 0x2665, + 0x26a1: 0x26a1, + 0xf27c: 0xf4a9, + 0xf27d: 0xf4aa, + 0xf27e: 0xf4ab, + 0xf27f: 0xf4ac, + 0xf280: 0xf4ad, + 0xf281: 0xf4ae, + 0xf282: 0xf4af, + 0xf283: 0xf4b0, + 0xf284: 0xf4b1, + 0xf285: 0xf4b2, + 0xf286: 0xf4b3, + 0xf287: 0xf4b4, + 0xf288: 0xf4b5, + 0xf289: 0xf4b6, + 0xf28a: 0xf4b7, + 0xf28b: 0xf4b8, + 0xf28c: 0xf4b9, + 0xf28d: 0xf4ba, + 0xf28e: 0xf4bb, + 0xf28f: 0xf4bc, + 0xf290: 0xf4bd, + 0xf291: 0xf4be, + 0xf292: 0xf4bf, + 0xf293: 0xf4c0, + 0xf294: 0xf4c1, + 0xf295: 0xf4c2, + 0xf296: 0xf4c3, + 0xf297: 0xf4c4, + 0xf298: 0xf4c5, + 0xf299: 0xf4c6, + 0xf29a: 0xf4c7, + 0xf29b: 0xf4c8, + 0xf29c: 0xf4c9, + 0xf29d: 0xf4ca, + 0xf29e: 0xf4cb, + 0xf29f: 0xf4cc, + 0xf2a0: 0xf4cd, + 0xf2a1: 0xf4ce, + 0xf2a2: 0xf4cf, + 0xf2a3: 0xf4d0, + 0xf2a4: 0xf4d1, + 0xf2a5: 0xf4d2, + 0xf2a6: 0xf4d3, + 0xf2a7: 0xf4d4, + 0xf2a8: 0xf4d5, + 0xf2a9: 0xf4d6, + 0xf2aa: 0xf4d7, + 0xf2ab: 0xf4d8, + 0xf2ac: 0xf4d9, + 0xf2ad: 0xf4da, + 0xf2ae: 0xf4db, + 0xf2af: 0xf4dc, + 0xf2b0: 0xf4dd, + 0xf2b1: 0xf4de, + 0xf2b2: 0xf4df, + 0xf2b3: 0xf4e0, + 0xf2b4: 0xf4e1, + 0xf2b5: 0xf4e2, + 0xf2b6: 0xf4e3, + 0xf2b7: 0xf4e4, + 0xf2b8: 0xf4e5, + 0xf2b9: 0xf4e6, + 0xf2ba: 0xf4e7, + 0xf2bb: 0xf4e8, + 0xf2bc: 0xf4e9, + 0xf2bd: 0xf4ea, + 0xf2be: 0xf4eb, + 0xf2bf: 0xf4ec, + 0xf2c0: 0xf4ed, + 0xf2c1: 0xf4ee, + 0xf2c2: 0xf4ef, + 0xf2c3: 0xf4f0, + 0xf2c4: 0xf4f1, + 0xf2c5: 0xf4f2, + 0xf2c6: 0xf4f3, + 0xf2c7: 0xf4f4, + 0xf2c8: 0xf4f5, + 0xf2c9: 0xf4f6, + 0xf2ca: 0xf4f7, + 0xf2cb: 0xf4f8, + 0xf2cc: 0xf4f9, + 0xf2cd: 0xf4fa, + 0xf2ce: 0xf4fb, + 0xf2cf: 0xf4fc, + 0xf2d0: 0xf4fd, + 0xf2d1: 0xf4fe, + 0xf2d2: 0xf4ff, + 0xf2d3: 0xf500, + 0xf2d4: 0xf501, + 0xf2d5: 0xf502, + 0xf2d6: 0xf503, + 0xf2d7: 0xf504, + 0xf2d8: 0xf505, + 0xf2d9: 0xf506, + 0xf2da: 0xf507, + 0xf2db: 0xf508, + 0xf2dc: 0xf509, + 0xf2dd: 0xf50a, + 0xf2de: 0xf50b, + 0xf2df: 0xf50c, + 0xf2e0: 0xf50d, + 0xf2e1: 0xf50e, + 0xf2e2: 0xf50f, + 0xf2e3: 0xf510, + 0xf2e4: 0xf511, + 0xf2e5: 0xf512, + 0xf2e6: 0xf513, + 0xf2e7: 0xf514, + 0xf2e8: 0xf515, + 0xf2e9: 0xf516, + 0xf2ea: 0xf517, + 0xf2eb: 0xf518, + 0xf2ec: 0xf519, + 0xf2ed: 0xf51a, + 0xf2ee: 0xf51b, + 0xf2ef: 0xf51c, + 0xf2f0: 0xf51d, + 0xf2f1: 0xf51e, + 0xf2f2: 0xf51f, + 0xf2f3: 0xf520, + 0xf2f4: 0xf521, + 0xf2f5: 0xf522, + 0xf2f6: 0xf523, + 0xf2f7: 0xf524, + 0xf2f8: 0xf525, + 0xf2f9: 0xf526, + 0xf2fa: 0xf527, + 0xf2fb: 0xf528, + 0xf2fc: 0xf529, + 0xf2fd: 0xf52a, + 0xf2fe: 0xf52b, + 0xf2ff: 0xf52c, + 0xf300: 0xf52d, + 0xf301: 0xf52e, + 0xf302: 0xf52f, + 0xf303: 0xf530, + 0xf304: 0xf531, + 0xf305: 0xf532, + 0xf306: 0xf533, + }, + "Codicons": { + 0xea60: 0xea60, + 0xea61: 0xea61, + 0xea62: 0xea62, + 0xea63: 0xea63, + 0xea64: 0xea64, + 0xea65: 0xea65, + 0xea66: 0xea66, + 0xea67: 0xea67, + 0xea68: 0xea68, + 0xea69: 0xea69, + 0xea6a: 0xea6a, + 0xea6b: 0xea6b, + 0xea6c: 0xea6c, + 0xea6d: 0xea6d, + 0xea6e: 0xea6e, + 0xea6f: 0xea6f, + 0xea70: 0xea70, + 0xea71: 0xea71, + 0xea72: 0xea72, + 0xea73: 0xea73, + 0xea74: 0xea74, + 0xea75: 0xea75, + 0xea76: 0xea76, + 0xea77: 0xea77, + 0xea78: 0xea78, + 0xea79: 0xea79, + 0xea7a: 0xea7a, + 0xea7b: 0xea7b, + 0xea7c: 0xea7c, + 0xea7d: 0xea7d, + 0xea7e: 0xea7e, + 0xea7f: 0xea7f, + 0xea80: 0xea80, + 0xea81: 0xea81, + 0xea82: 0xea82, + 0xea83: 0xea83, + 0xea84: 0xea84, + 0xea85: 0xea85, + 0xea86: 0xea86, + 0xea87: 0xea87, + 0xea88: 0xea88, + 0xea8a: 0xea8a, + 0xea8b: 0xea8b, + 0xea8c: 0xea8c, + 0xea8f: 0xea8f, + 0xea90: 0xea90, + 0xea91: 0xea91, + 0xea92: 0xea92, + 0xea93: 0xea93, + 0xea94: 0xea94, + 0xea95: 0xea95, + 0xea96: 0xea96, + 0xea97: 0xea97, + 0xea98: 0xea98, + 0xea99: 0xea99, + 0xea9a: 0xea9a, + 0xea9b: 0xea9b, + 0xea9c: 0xea9c, + 0xea9d: 0xea9d, + 0xea9e: 0xea9e, + 0xea9f: 0xea9f, + 0xeaa0: 0xeaa0, + 0xeaa1: 0xeaa1, + 0xeaa2: 0xeaa2, + 0xeaa3: 0xeaa3, + 0xeaa4: 0xeaa4, + 0xeaa5: 0xeaa5, + 0xeaa6: 0xeaa6, + 0xeaa7: 0xeaa7, + 0xeaa8: 0xeaa8, + 0xeaa9: 0xeaa9, + 0xeaaa: 0xeaaa, + 0xeaab: 0xeaab, + 0xeaac: 0xeaac, + 0xeaad: 0xeaad, + 0xeaae: 0xeaae, + 0xeaaf: 0xeaaf, + 0xeab0: 0xeab0, + 0xeab1: 0xeab1, + 0xeab2: 0xeab2, + 0xeab3: 0xeab3, + 0xeab4: 0xeab4, + 0xeab5: 0xeab5, + 0xeab6: 0xeab6, + 0xeab7: 0xeab7, + 0xeab8: 0xeab8, + 0xeab9: 0xeab9, + 0xeaba: 0xeaba, + 0xeabb: 0xeabb, + 0xeabc: 0xeabc, + 0xeabd: 0xeabd, + 0xeabe: 0xeabe, + 0xeabf: 0xeabf, + 0xeac0: 0xeac0, + 0xeac1: 0xeac1, + 0xeac2: 0xeac2, + 0xeac3: 0xeac3, + 0xeac4: 0xeac4, + 0xeac5: 0xeac5, + 0xeac6: 0xeac6, + 0xeac7: 0xeac7, + 0xeac9: 0xeac9, + 0xeacc: 0xeacc, + 0xeacd: 0xeacd, + 0xeace: 0xeace, + 0xeacf: 0xeacf, + 0xead0: 0xead0, + 0xead1: 0xead1, + 0xead2: 0xead2, + 0xead3: 0xead3, + 0xead4: 0xead4, + 0xead5: 0xead5, + 0xead6: 0xead6, + 0xead7: 0xead7, + 0xead8: 0xead8, + 0xead9: 0xead9, + 0xeada: 0xeada, + 0xeadb: 0xeadb, + 0xeadc: 0xeadc, + 0xeadd: 0xeadd, + 0xeade: 0xeade, + 0xeadf: 0xeadf, + 0xeae0: 0xeae0, + 0xeae1: 0xeae1, + 0xeae2: 0xeae2, + 0xeae3: 0xeae3, + 0xeae4: 0xeae4, + 0xeae5: 0xeae5, + 0xeae6: 0xeae6, + 0xeae7: 0xeae7, + 0xeae8: 0xeae8, + 0xeae9: 0xeae9, + 0xeaea: 0xeaea, + 0xeaeb: 0xeaeb, + 0xeaec: 0xeaec, + 0xeaed: 0xeaed, + 0xeaee: 0xeaee, + 0xeaef: 0xeaef, + 0xeaf0: 0xeaf0, + 0xeaf1: 0xeaf1, + 0xeaf2: 0xeaf2, + 0xeaf3: 0xeaf3, + 0xeaf4: 0xeaf4, + 0xeaf5: 0xeaf5, + 0xeaf6: 0xeaf6, + 0xeaf7: 0xeaf7, + 0xeaf8: 0xeaf8, + 0xeaf9: 0xeaf9, + 0xeafa: 0xeafa, + 0xeafb: 0xeafb, + 0xeafc: 0xeafc, + 0xeafd: 0xeafd, + 0xeafe: 0xeafe, + 0xeaff: 0xeaff, + 0xeb00: 0xeb00, + 0xeb01: 0xeb01, + 0xeb02: 0xeb02, + 0xeb03: 0xeb03, + 0xeb04: 0xeb04, + 0xeb05: 0xeb05, + 0xeb06: 0xeb06, + 0xeb07: 0xeb07, + 0xeb08: 0xeb08, + 0xeb09: 0xeb09, + 0xeb0b: 0xeb0b, + 0xeb0c: 0xeb0c, + 0xeb0d: 0xeb0d, + 0xeb0e: 0xeb0e, + 0xeb0f: 0xeb0f, + 0xeb10: 0xeb10, + 0xeb11: 0xeb11, + 0xeb12: 0xeb12, + 0xeb13: 0xeb13, + 0xeb14: 0xeb14, + 0xeb15: 0xeb15, + 0xeb16: 0xeb16, + 0xeb17: 0xeb17, + 0xeb18: 0xeb18, + 0xeb19: 0xeb19, + 0xeb1a: 0xeb1a, + 0xeb1b: 0xeb1b, + 0xeb1c: 0xeb1c, + 0xeb1d: 0xeb1d, + 0xeb1e: 0xeb1e, + 0xeb1f: 0xeb1f, + 0xeb20: 0xeb20, + 0xeb21: 0xeb21, + 0xeb22: 0xeb22, + 0xeb23: 0xeb23, + 0xeb24: 0xeb24, + 0xeb25: 0xeb25, + 0xeb26: 0xeb26, + 0xeb27: 0xeb27, + 0xeb28: 0xeb28, + 0xeb29: 0xeb29, + 0xeb2a: 0xeb2a, + 0xeb2b: 0xeb2b, + 0xeb2c: 0xeb2c, + 0xeb2d: 0xeb2d, + 0xeb2e: 0xeb2e, + 0xeb2f: 0xeb2f, + 0xeb30: 0xeb30, + 0xeb31: 0xeb31, + 0xeb32: 0xeb32, + 0xeb33: 0xeb33, + 0xeb34: 0xeb34, + 0xeb35: 0xeb35, + 0xeb36: 0xeb36, + 0xeb37: 0xeb37, + 0xeb38: 0xeb38, + 0xeb39: 0xeb39, + 0xeb3a: 0xeb3a, + 0xeb3b: 0xeb3b, + 0xeb3c: 0xeb3c, + 0xeb3d: 0xeb3d, + 0xeb3e: 0xeb3e, + 0xeb3f: 0xeb3f, + 0xeb40: 0xeb40, + 0xeb41: 0xeb41, + 0xeb42: 0xeb42, + 0xeb43: 0xeb43, + 0xeb44: 0xeb44, + 0xeb45: 0xeb45, + 0xeb46: 0xeb46, + 0xeb47: 0xeb47, + 0xeb48: 0xeb48, + 0xeb49: 0xeb49, + 0xeb4a: 0xeb4a, + 0xeb4b: 0xeb4b, + 0xeb4c: 0xeb4c, + 0xeb4d: 0xeb4d, + 0xeb4e: 0xeb4e, + 0xeb50: 0xeb50, + 0xeb51: 0xeb51, + 0xeb52: 0xeb52, + 0xeb53: 0xeb53, + 0xeb54: 0xeb54, + 0xeb55: 0xeb55, + 0xeb56: 0xeb56, + 0xeb57: 0xeb57, + 0xeb58: 0xeb58, + 0xeb59: 0xeb59, + 0xeb5a: 0xeb5a, + 0xeb5b: 0xeb5b, + 0xeb5c: 0xeb5c, + 0xeb5d: 0xeb5d, + 0xeb5e: 0xeb5e, + 0xeb5f: 0xeb5f, + 0xeb60: 0xeb60, + 0xeb61: 0xeb61, + 0xeb62: 0xeb62, + 0xeb63: 0xeb63, + 0xeb64: 0xeb64, + 0xeb65: 0xeb65, + 0xeb66: 0xeb66, + 0xeb67: 0xeb67, + 0xeb68: 0xeb68, + 0xeb69: 0xeb69, + 0xeb6a: 0xeb6a, + 0xeb6b: 0xeb6b, + 0xeb6c: 0xeb6c, + 0xeb6d: 0xeb6d, + 0xeb6e: 0xeb6e, + 0xeb6f: 0xeb6f, + 0xeb70: 0xeb70, + 0xeb71: 0xeb71, + 0xeb72: 0xeb72, + 0xeb73: 0xeb73, + 0xeb74: 0xeb74, + 0xeb75: 0xeb75, + 0xeb76: 0xeb76, + 0xeb77: 0xeb77, + 0xeb78: 0xeb78, + 0xeb79: 0xeb79, + 0xeb7a: 0xeb7a, + 0xeb7b: 0xeb7b, + 0xeb7c: 0xeb7c, + 0xeb7d: 0xeb7d, + 0xeb7e: 0xeb7e, + 0xeb7f: 0xeb7f, + 0xeb80: 0xeb80, + 0xeb81: 0xeb81, + 0xeb82: 0xeb82, + 0xeb83: 0xeb83, + 0xeb84: 0xeb84, + 0xeb85: 0xeb85, + 0xeb86: 0xeb86, + 0xeb87: 0xeb87, + 0xeb88: 0xeb88, + 0xeb89: 0xeb89, + 0xeb8a: 0xeb8a, + 0xeb8b: 0xeb8b, + 0xeb8c: 0xeb8c, + 0xeb8d: 0xeb8d, + 0xeb8e: 0xeb8e, + 0xeb8f: 0xeb8f, + 0xeb90: 0xeb90, + 0xeb91: 0xeb91, + 0xeb92: 0xeb92, + 0xeb93: 0xeb93, + 0xeb94: 0xeb94, + 0xeb95: 0xeb95, + 0xeb96: 0xeb96, + 0xeb97: 0xeb97, + 0xeb98: 0xeb98, + 0xeb99: 0xeb99, + 0xeb9a: 0xeb9a, + 0xeb9b: 0xeb9b, + 0xeb9c: 0xeb9c, + 0xeb9d: 0xeb9d, + 0xeb9e: 0xeb9e, + 0xeb9f: 0xeb9f, + 0xeba0: 0xeba0, + 0xeba1: 0xeba1, + 0xeba2: 0xeba2, + 0xeba3: 0xeba3, + 0xeba4: 0xeba4, + 0xeba5: 0xeba5, + 0xeba6: 0xeba6, + 0xeba7: 0xeba7, + 0xeba8: 0xeba8, + 0xeba9: 0xeba9, + 0xebaa: 0xebaa, + 0xebab: 0xebab, + 0xebac: 0xebac, + 0xebad: 0xebad, + 0xebae: 0xebae, + 0xebaf: 0xebaf, + 0xebb0: 0xebb0, + 0xebb1: 0xebb1, + 0xebb2: 0xebb2, + 0xebb3: 0xebb3, + 0xebb4: 0xebb4, + 0xebb5: 0xebb5, + 0xebb6: 0xebb6, + 0xebb7: 0xebb7, + 0xebb8: 0xebb8, + 0xebb9: 0xebb9, + 0xebba: 0xebba, + 0xebbb: 0xebbb, + 0xebbc: 0xebbc, + 0xebbd: 0xebbd, + 0xebbe: 0xebbe, + 0xebbf: 0xebbf, + 0xebc0: 0xebc0, + 0xebc1: 0xebc1, + 0xebc2: 0xebc2, + 0xebc3: 0xebc3, + 0xebc4: 0xebc4, + 0xebc5: 0xebc5, + 0xebc6: 0xebc6, + 0xebc7: 0xebc7, + 0xebc8: 0xebc8, + 0xebc9: 0xebc9, + 0xebca: 0xebca, + 0xebcb: 0xebcb, + 0xebcc: 0xebcc, + 0xebcd: 0xebcd, + 0xebce: 0xebce, + 0xebcf: 0xebcf, + 0xebd0: 0xebd0, + 0xebd1: 0xebd1, + 0xebd2: 0xebd2, + 0xebd3: 0xebd3, + 0xebd4: 0xebd4, + 0xebd5: 0xebd5, + 0xebd6: 0xebd6, + 0xebd7: 0xebd7, + 0xebd8: 0xebd8, + 0xebd9: 0xebd9, + 0xebda: 0xebda, + 0xebdb: 0xebdb, + 0xebdc: 0xebdc, + 0xebdd: 0xebdd, + 0xebde: 0xebde, + 0xebdf: 0xebdf, + 0xebe0: 0xebe0, + 0xebe1: 0xebe1, + 0xebe2: 0xebe2, + 0xebe3: 0xebe3, + 0xebe4: 0xebe4, + 0xebe5: 0xebe5, + 0xebe6: 0xebe6, + 0xebe7: 0xebe7, + 0xebe8: 0xebe8, + 0xebe9: 0xebe9, + 0xebea: 0xebea, + 0xebeb: 0xebeb, + 0xebec: 0xebec, + 0xebed: 0xebed, + 0xebee: 0xebee, + 0xebef: 0xebef, + 0xebf0: 0xebf0, + 0xebf1: 0xebf1, + 0xebf2: 0xebf2, + 0xebf3: 0xebf3, + 0xebf4: 0xebf4, + 0xebf5: 0xebf5, + 0xebf6: 0xebf6, + 0xebf7: 0xebf7, + 0xebf8: 0xebf8, + 0xebf9: 0xebf9, + 0xebfa: 0xebfa, + 0xebfb: 0xebfb, + 0xebfc: 0xebfc, + 0xebfd: 0xebfd, + 0xebfe: 0xebfe, + 0xebff: 0xebff, + 0xec00: 0xec00, + 0xec01: 0xec01, + 0xec02: 0xec02, + 0xec03: 0xec03, + 0xec04: 0xec04, + 0xec05: 0xec05, + 0xec06: 0xec06, + 0xec07: 0xec07, + 0xec08: 0xec08, + 0xec09: 0xec09, + 0xec0a: 0xec0a, + 0xec0b: 0xec0b, + 0xec0c: 0xec0c, + 0xec0d: 0xec0d, + 0xec0e: 0xec0e, + 0xec0f: 0xec0f, + 0xec10: 0xec10, + 0xec11: 0xec11, + 0xec12: 0xec12, + 0xec13: 0xec13, + 0xec14: 0xec14, + 0xec15: 0xec15, + 0xec16: 0xec16, + 0xec17: 0xec17, + 0xec18: 0xec18, + 0xec19: 0xec19, + 0xec1a: 0xec1a, + 0xec1b: 0xec1b, + 0xec1c: 0xec1c, + 0xec1d: 0xec1d, + 0xec1e: 0xec1e, + }, +} From d3ee3c5b8a382aee982973cce704411b9a33f052 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 11 Oct 2025 14:49:31 -0700 Subject: [PATCH 248/319] macos: update permission request response should move state back to idle (#9151) Previously, the permission request response would not move the state so it'd stay in the titlebar. --- macos/Sources/Features/Update/UpdateDriver.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 81477ef67..ed58f1663 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -39,7 +39,10 @@ class UpdateDriver: NSObject, SPUUserDriver { func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { - viewModel.state = .permissionRequest(.init(request: request, reply: reply)) + viewModel.state = .permissionRequest(.init(request: request, reply: { [weak viewModel] response in + viewModel?.state = .idle + reply(response) + })) if !hasUnobtrusiveTarget { standard.show(request, reply: reply) } From aa0c68ee5ef209b9bca9ff4070d3858b3c23da95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 19:44:59 -0700 Subject: [PATCH 249/319] Update iTerm2 colorschemes (#9155) Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/release-20251006-150522-c07f0e8 Co-authored-by: mitchellh <1299+mitchellh@users.noreply.github.com> --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 8fe8daefb..45f1e3692 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -116,8 +116,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz", - .hash = "N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz", + .hash = "N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index c62a0ed85..e9f2b8cd8 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y": { + "N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz", - "hash": "sha256-GsEWVt4wMzp6+7N5I+QVuhCVJ70cFrdADwUds59AKnw=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz", + "hash": "sha256-yTN88FFxGVeK07fSQK+jWy9XLAln7f/W+xcDH+tLOEY=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index d699b454b..08e524e91 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y"; + name = "N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz"; - hash = "sha256-GsEWVt4wMzp6+7N5I+QVuhCVJ70cFrdADwUds59AKnw="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz"; + hash = "sha256-yTN88FFxGVeK07fSQK+jWy9XLAln7f/W+xcDH+tLOEY="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index d8a390c40..d7b76e59f 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -31,6 +31,6 @@ https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 3f573456a..df210bf22 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251002-142451-4a5043e/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AALIsAwDyo88G5mGJGN2lSVmmFMx4YePfUvp_2o3Y", - "sha256": "1ac11656de30333a7afbb37923e415ba109527bd1c16b7400f051db39f402a7c" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl", + "sha256": "c9337cf0517119578ad3b7d240afa35b2f572c0967edffd6fb17031feb4b3846" }, { "type": "archive", From 2e34f4e0e574612c7bae4730325e57a4154236b9 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 11 Oct 2025 19:48:08 -0700 Subject: [PATCH 250/319] fix(font): Additional scale group tweaks (#9152) Of course #9142 would require a minor follow-up! * Scale groups can cut across patch sets, but not across fonts. We had some scale group mixing between Font Awesome and the weather symbols, which is removed by this PR.[^cp_table_full] * There's one case where a scale group includes a glyph that's not part of any patch sets, just for padding out the group bounding box. Previously, an unrelated glyph from a different font would be pulled in. Now we use an appropriate stand-in. (See code comment for details.) * I noticed overlaps weren't being split between each side of the bounding box, they were added to both sides, resulting in twice as much padding as specified. Screenshots showing the extra vertical padding for progress bar elements due to the second bullet point: **Before** Screenshot 2025-10-11 at 15 33 54 **After** Screenshot 2025-10-11 at 15 33 20 [^cp_table_full]: Forming and using the merged `cp_table_full` table should have been a red flag. Such a table doesn't make sense, it would be a one-to-many map. You need the names of the original fonts to disambiguate. --- src/font/nerd_font_attributes.zig | 1530 +++++++++++++++++++++++++---- src/font/nerd_font_codegen.py | 51 +- 2 files changed, 1374 insertions(+), 207 deletions(-) diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 73fcf0bd3..f4a19d963 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -16,10 +16,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .center1, .align_vertical = .center1, - .pad_left = 0.1, - .pad_right = 0.1, - .pad_top = 0.1, - .pad_bottom = 0.1, + .pad_left = 0.05, + .pad_right = 0.05, + .pad_top = 0.05, + .pad_bottom = 0.05, }, 0x276c...0x276d, => .{ @@ -69,10 +69,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0b1, @@ -89,10 +89,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0b3, @@ -109,10 +109,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.59, }, 0xe0b5, @@ -129,10 +129,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.59, }, 0xe0b7, @@ -143,6 +143,18 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .max_xy_ratio = 0.5, }, + 0xe0b8, + 0xe0bc, + => .{ + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .start, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, + }, 0xe0b9, 0xe0bd, => .{ @@ -151,6 +163,18 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_horizontal = .start, .align_vertical = .center1, }, + 0xe0ba, + 0xe0be, + => .{ + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .end, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, + }, 0xe0bb, 0xe0bf, => .{ @@ -165,10 +189,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xe0c1, => .{ @@ -182,10 +206,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xe0c3, => .{ @@ -198,10 +222,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.86, }, 0xe0c5, @@ -209,10 +233,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.86, }, 0xe0c6, @@ -220,10 +244,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.78, }, 0xe0c7, @@ -231,10 +255,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.78, }, 0xe0cc, @@ -242,10 +266,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .size = .stretch, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.01, + .pad_right = -0.01, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.85, }, 0xe0cd, @@ -268,10 +292,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.01, + .pad_right = -0.01, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0d4, @@ -280,10 +304,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.01, + .pad_right = -0.01, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0d6, @@ -292,10 +316,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0d7, @@ -304,12 +328,39 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, + 0xe300, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8984375000000000, + .relative_y = 0.0986328125000000, + }, + 0xe301, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8798828125000000, + .relative_y = 0.1171875000000000, + }, + 0xe302, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7646484375000000, + .relative_y = 0.2314453125000000, + }, 0xe303, => .{ .size = .fit_cover1, @@ -328,6 +379,188 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.9755859375000000, .relative_y = 0.0244140625000000, }, + 0xe305, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9960937500000000, + .relative_y = 0.0019531250000000, + }, + 0xe306, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9863281250000000, + .relative_y = 0.0097656250000000, + }, + 0xe307, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9951171875000000, + .relative_y = 0.0039062500000000, + }, + 0xe308, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9785156250000000, + .relative_y = 0.0195312500000000, + }, + 0xe309, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9736328125000000, + .relative_y = 0.0214843750000000, + }, + 0xe30a, + 0xe35f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9648437500000000, + .relative_y = 0.0302734375000000, + }, + 0xe30b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8437500000000000, + .relative_y = 0.1513671875000000, + }, + 0xe30c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8027343750000000, + .relative_y = 0.1835937500000000, + }, + 0xe30d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7753906250000000, + .relative_y = 0.1083984375000000, + }, + 0xe30e, + 0xe365, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9833984375000000, + .relative_y = 0.0166015625000000, + }, + 0xe30f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9716796875000000, + .relative_y = 0.0263671875000000, + }, + 0xe310, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6621093750000000, + .relative_y = 0.0986328125000000, + }, + 0xe311, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6425781250000000, + .relative_y = 0.1171875000000000, + }, + 0xe312, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5322265625000000, + .relative_y = 0.2314453125000000, + }, + 0xe313, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6416015625000000, + .relative_y = 0.1181640625000000, + }, + 0xe314, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7382812500000000, + .relative_y = 0.0195312500000000, + }, + 0xe315, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6787109375000000, + .relative_y = 0.1357421875000000, + }, + 0xe316, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7480468750000000, + .relative_y = 0.0097656250000000, + }, + 0xe317, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7529296875000000, + .relative_y = 0.0048828125000000, + }, + 0xe318, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.0263671875000000, + }, 0xe319, => .{ .size = .fit_cover1, @@ -338,6 +571,7 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_y = 0.0195312500000000, }, 0xe31a, + 0xe35e, => .{ .size = .fit_cover1, .height = .icon, @@ -391,6 +625,24 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7363281250000000, .relative_y = 0.0986328125000000, }, + 0xe320, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7177734375000000, + .relative_y = 0.1171875000000000, + }, + 0xe321, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8085937500000000, + .relative_y = 0.0253906250000000, + }, 0xe322, => .{ .size = .fit_cover1, @@ -400,6 +652,32 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7509765625000000, .relative_y = 0.0839843750000000, }, + 0xe323, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8281250000000000, + .relative_y = 0.0097656250000000, + }, + 0xe324, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8349609375000000, + }, + 0xe325, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8154296875000000, + .relative_y = 0.0214843750000000, + }, 0xe326, => .{ .size = .fit_cover1, @@ -409,14 +687,294 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8144531250000000, .relative_y = 0.0195312500000000, }, - 0xe347, + 0xe327, + 0xe361, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8076171875000000, + .relative_y = 0.0273437500000000, + }, + 0xe328, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6845703125000000, + .relative_y = 0.1503906250000000, + }, + 0xe329, + 0xe367, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8173828125000000, + .relative_y = 0.0175781250000000, + }, + 0xe32a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8105468750000000, + .relative_y = 0.0263671875000000, + }, + 0xe32b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5175781250000000, + .relative_y = 0.2421875000000000, + }, + 0xe32c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6992187500000000, + .relative_y = 0.1005859375000000, + }, + 0xe32d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6787109375000000, + .relative_y = 0.1201171875000000, + }, + 0xe32e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5654296875000000, + .relative_y = 0.2324218750000000, + }, + 0xe32f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0273437500000000, + }, + 0xe330, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7148437500000000, + .relative_y = 0.0830078125000000, + }, + 0xe331, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7919921875000000, + .relative_y = 0.0097656250000000, + }, + 0xe332, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7871093750000000, + .relative_y = 0.0126953125000000, + }, + 0xe333, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0263671875000000, + }, + 0xe334, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7773437500000000, + .relative_y = 0.0195312500000000, + }, + 0xe335, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0283203125000000, + }, + 0xe336, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6503906250000000, + .relative_y = 0.1503906250000000, + }, + 0xe337, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7753906250000000, + .relative_y = 0.0234375000000000, + }, + 0xe338, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7792968750000000, + .relative_y = 0.0185546875000000, + }, + 0xe339, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8445945945945946, + }, + 0xe33a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5283203125000000, + .relative_y = 0.2324218750000000, + }, + 0xe33b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5449218750000000, + .relative_y = 0.2148437500000000, + }, + 0xe33c...0xe33d, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, .relative_height = 0.5273437500000000, - .relative_y = 0.2617187500000000, + .relative_y = 0.2324218750000000, + }, + 0xe33e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3293918918918919, + .relative_y = 0.6706081081081081, + }, + 0xe33f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5200000000000000, + .relative_y = 0.2707692307692308, + }, + 0xe340, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8307692307692308, + .relative_y = 0.0861538461538462, + }, + 0xe341, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8327702702702703, + .relative_y = 0.0050675675675676, + }, + 0xe344, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5307692307692308, + .relative_y = 0.2092307692307692, + }, + 0xe345, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5332112630208333, + .relative_y = 0.2040934244791667, + }, + 0xe347, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8307692307692308, + .relative_y = 0.1246153846153846, + }, + 0xe349, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5307967032967034, + .relative_y = 0.2615384615384616, + }, + 0xe34c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8659995118379302, + .relative_y = 0.1340004881620698, + }, + 0xe34d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9890163534293386, + .relative_y = 0.0002440810349036, }, 0xe34f, => .{ @@ -427,14 +985,69 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.5751953125000000, .relative_y = 0.1142578125000000, }, - 0xe35f, + 0xe351, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.9648437500000000, - .relative_y = 0.0302734375000000, + .relative_height = 0.6533203125000000, + .relative_y = 0.1328125000000000, + }, + 0xe352, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5215384615384615, + .relative_y = 0.2846153846153846, + }, + 0xe353, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8308012820512821, + .relative_y = 0.1230448717948718, + }, + 0xe354...0xe356, + 0xe358...0xe359, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9935233160621761, + .relative_y = 0.0025906735751295, + }, + 0xe357, + 0xe3a9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9961139896373057, + }, + 0xe35a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9935233160621761, + .relative_y = 0.0012953367875648, + }, + 0xe35b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987046632124352, + .relative_y = 0.0012953367875648, }, 0xe360, => .{ @@ -445,14 +1058,14 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7695312500000000, .relative_y = 0.0302734375000000, }, - 0xe361, + 0xe362, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.8076171875000000, - .relative_y = 0.0273437500000000, + .relative_height = 0.9902343750000000, + .relative_y = 0.0097656250000000, }, 0xe363, => .{ @@ -463,6 +1076,42 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7900390625000000, .relative_y = 0.0097656250000000, }, + 0xe364, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8251953125000000, + .relative_y = 0.0097656250000000, + }, + 0xe366, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7832031250000000, + .relative_y = 0.0166015625000000, + }, + 0xe369, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4902343750000000, + .relative_y = 0.2548828125000000, + }, + 0xe36b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9333658774713205, + .relative_y = 0.0266048328044911, + }, 0xe36c, => .{ .size = .fit_cover1, @@ -481,6 +1130,42 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8427734375000000, .relative_y = 0.0625000000000000, }, + 0xe36e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7529721467391304, + .relative_y = 0.0956606657608696, + }, + 0xe36f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6835937500000000, + .relative_y = 0.1250000000000000, + }, + 0xe370, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8642578125000000, + .relative_y = 0.0625000000000000, + }, + 0xe371, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6103515625000000, + .relative_y = 0.1933593750000000, + }, 0xe372, => .{ .size = .fit_cover1, @@ -499,6 +1184,60 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.8652343750000000, .relative_y = 0.0058593750000000, }, + 0xe374, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3154296875000000, + .relative_y = 0.2861328125000000, + }, + 0xe375, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6772460937500000, + .relative_y = 0.1303710937500000, + }, + 0xe376, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6992187500000000, + .relative_y = 0.1337890625000000, + }, + 0xe377, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.1552734375000000, + }, + 0xe378, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.1542968750000000, + }, + 0xe379, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5751953125000000, + .relative_y = 0.1826171875000000, + }, 0xe37a, => .{ .size = .fit_cover1, @@ -517,6 +1256,15 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.5751953125000000, .relative_y = 0.1835937500000000, }, + 0xe37d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9003906250000000, + .relative_y = 0.0957031250000000, + }, 0xe37e, => .{ .size = .fit_cover1, @@ -526,39 +1274,118 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.6015625000000000, .relative_y = 0.2324218750000000, }, - 0xe381...0xe383, - 0xe386, - 0xe388, - 0xe38b, + 0xe37f, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7500000000000000, - .relative_y = 0.1250000000000000, + .relative_height = 0.5200000000000000, + .relative_y = 0.2784615384615385, }, - 0xe38d, + 0xe380, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7519531250000000, - .relative_y = 0.1240234375000000, + .relative_height = 0.5200000000000000, + .relative_y = 0.2630769230769231, + }, + 0xe38e...0xe391, + 0xe394, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4990253411306043, + .relative_height = 0.9987012987012988, + .relative_x = 0.4996751137102014, + }, + 0xe392...0xe393, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4996751137102014, + .relative_height = 0.9987012987012988, + .relative_x = 0.4990253411306043, + }, + 0xe395, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5471085120207927, + .relative_height = 0.9987012987012988, + .relative_x = 0.4515919428200130, }, - 0xe390, - 0xe393, 0xe396, - 0xe3a3, - 0xe3a6...0xe3a7, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7509765625000000, - .relative_y = 0.1240234375000000, + .relative_width = 0.5945419103313840, + .relative_height = 0.9987012987012988, + .relative_x = 0.4041585445094217, + }, + 0xe397, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6426250812215725, + .relative_x = 0.3573749187784275, + }, + 0xe398, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6900584795321637, + .relative_x = 0.3099415204678362, + }, + 0xe399, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7381416504223521, + .relative_x = 0.2618583495776478, + }, + 0xe39a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7855750487329435, + .relative_x = 0.2144249512670565, + }, + 0xe39b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987004548408057, + .relative_height = 0.9987012987012988, + }, + 0xe39c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8323586744639376, + .relative_height = 0.9935064935064936, }, 0xe39d, => .{ @@ -566,17 +1393,35 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7480468750000000, - .relative_y = 0.1240234375000000, + .relative_width = 0.7855750487329435, + .relative_height = 0.9948051948051948, }, - 0xe39e...0xe3a0, + 0xe39e, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7490234375000000, - .relative_y = 0.1240234375000000, + .relative_width = 0.7381416504223521, + .relative_height = 0.9961038961038962, + }, + 0xe39f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6907082521117609, + .relative_height = 0.9961038961038962, + }, + 0xe3a0, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6426250812215725, + .relative_height = 0.9961038961038962, }, 0xe3a1, => .{ @@ -584,8 +1429,38 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7500000000000000, - .relative_y = 0.1240234375000000, + .relative_width = 0.5945419103313840, + .relative_height = 0.9974025974025974, + }, + 0xe3a2...0xe3a3, + 0xe3a5, + 0xe3a7...0xe3a8, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4990253411306043, + .relative_height = 0.9987012987012988, + }, + 0xe3a4, + 0xe3a6, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4996751137102014, + .relative_height = 0.9987012987012988, + }, + 0xe3aa, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9902343750000000, + .relative_y = 0.0078125000000000, }, 0xe3ab, => .{ @@ -614,35 +1489,51 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7519531250000000, .relative_y = 0.0068359375000000, }, + 0xe3ae, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6152343750000000, + .relative_y = 0.2324218750000000, + }, 0xe3af, 0xe3b3, - 0xe3b5, - 0xe3b7...0xe3bb, + 0xe3b5...0xe3bb, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7001953125000000, - .relative_y = 0.1503906250000000, + .relative_height = 0.9986072423398329, + .relative_y = 0.0013927576601671, }, - 0xe3b0...0xe3b1, + 0xe3b0...0xe3b2, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.6982421875000000, - .relative_y = 0.1523437500000000, + .relative_height = 0.9958217270194986, + .relative_y = 0.0041782729805014, }, - 0xe3b4, + 0xe3c1, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.7011718750000000, - .relative_y = 0.1494140625000000, + .relative_height = 0.6590187942396876, + .relative_y = 0.1349768123016842, + }, + 0xe3c2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7939956065413717, }, 0x23fb...0x23fe, 0x2665, @@ -650,34 +1541,20 @@ pub fn getConstraint(cp: u21) ?Constraint { 0x2b58, 0xe000...0xe00a, 0xe200...0xe2a9, - 0xe300...0xe302, - 0xe305...0xe318, - 0xe320...0xe321, - 0xe323...0xe325, - 0xe327...0xe346, - 0xe348...0xe34e, - 0xe350...0xe35e, - 0xe362, - 0xe364...0xe36b, - 0xe36e...0xe371, - 0xe374...0xe379, - 0xe37c...0xe37d, - 0xe37f...0xe380, - 0xe384...0xe385, - 0xe387, - 0xe389...0xe38a, - 0xe38c, - 0xe38e...0xe38f, - 0xe391...0xe392, - 0xe394...0xe395, - 0xe397...0xe39c, - 0xe3a2, - 0xe3a4...0xe3a5, - 0xe3a8...0xe3aa, - 0xe3ae, - 0xe3b2, - 0xe3b6, - 0xe3bc...0xe3e3, + 0xe342...0xe343, + 0xe346, + 0xe348, + 0xe34a...0xe34b, + 0xe34e, + 0xe350, + 0xe35c...0xe35d, + 0xe368, + 0xe36a, + 0xe37c, + 0xe381...0xe38d, + 0xe3b4, + 0xe3bc...0xe3c0, + 0xe3c3...0xe3e3, 0xe5fa...0xe6b8, 0xe700...0xe8ef, 0xea60, @@ -701,9 +1578,23 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xec0d...0xec1e, 0xed00...0xedff, 0xee0c...0xefce, - 0xf000...0xf0db, + 0xf000...0xf004, + 0xf006...0xf025, + 0xf028...0xf02a, + 0xf02c...0xf030, + 0xf034, + 0xf036...0xf043, + 0xf045, + 0xf047, + 0xf053...0xf05f, + 0xf062, + 0xf064...0xf076, + 0xf079...0xf07d, + 0xf07f...0xf088, + 0xf08a...0xf0a3, + 0xf0a6...0xf0d6, + 0xf0db, 0xf0df...0xf0ff, - 0xf101...0xf105, 0xf108...0xf12f, 0xf131...0xf140, 0xf142...0xf152, @@ -1017,8 +1908,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_width = 0.8008342022940563, .relative_x = 0.1991657977059437, }, - 0xe0ba, - 0xe0be, 0xee00, 0xee03, => .{ @@ -1026,10 +1915,14 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .end, .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .relative_width = 0.8681172291296625, + .relative_height = 0.8626692456479691, + .relative_x = 0.1314387211367673, + .relative_y = 0.0686653771760155, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xee01, 0xee04, @@ -1038,13 +1931,13 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .center1, .align_vertical = .center1, - .pad_left = -0.1, - .pad_right = -0.1, - .pad_top = -0.01, - .pad_bottom = -0.01, + .relative_height = 0.8626692456479691, + .relative_y = 0.0686653771760155, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.005, + .pad_bottom = -0.005, }, - 0xe0b8, - 0xe0bc, 0xee02, 0xee05, => .{ @@ -1052,10 +1945,13 @@ pub fn getConstraint(cp: u21) ?Constraint { .max_constraint_width = 1, .align_horizontal = .start, .align_vertical = .center1, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .relative_width = 0.8685612788632326, + .relative_height = 0.8626692456479691, + .relative_y = 0.0686653771760155, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xee06, => .{ @@ -1067,10 +1963,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.2234524408656266, .relative_x = 0.1470292044310171, .relative_y = 0.7765475591343735, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, 0xee07, => .{ @@ -1082,10 +1978,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.7498741821841973, .relative_x = 0.5000000000000000, .relative_y = 0.2501258178158027, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, 0xee08, => .{ @@ -1096,10 +1992,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_width = 0.6299093655589124, .relative_height = 0.8535480624056366, .relative_x = 0.3700906344410876, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, 0xee09, => .{ @@ -1108,10 +2004,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_horizontal = .center1, .align_vertical = .center1, .relative_height = 0.4997483643683945, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, 0xee0a, => .{ @@ -1121,10 +2017,10 @@ pub fn getConstraint(cp: u21) ?Constraint { .align_vertical = .center1, .relative_width = 0.6299093655589124, .relative_height = 0.8535480624056366, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, 0xee0b, => .{ @@ -1135,25 +2031,279 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_width = 0.5000000000000000, .relative_height = 0.7498741821841973, .relative_y = 0.2501258178158027, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, - 0xf0dc...0xf0de, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - }, - 0xf100, + 0xf005, => .{ .size = .fit_cover1, .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.6923828125000000, - .relative_y = 0.1538085937500000, + .relative_height = 0.9999664113932554, + .relative_y = 0.0000335886067446, + }, + 0xf026...0xf027, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9786184354605580, + .relative_y = 0.0103951316192896, + }, + 0xf02b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9758052740827267, + .relative_y = 0.0238869355863696, + }, + 0xf031...0xf033, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987922705314010, + .relative_y = 0.0006038647342995, + }, + 0xf035, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9989935587761675, + .relative_y = 0.0004025764895330, + }, + 0xf044, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9925925925925926, + }, + 0xf046, + 0xf153...0xf154, + 0xf158, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, + }, + 0xf048, + 0xf04a, + 0xf04e, + 0xf051, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8577706898990622, + .relative_y = 0.0711892586341537, + }, + 0xf049, + 0xf050, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8579450878868969, + .relative_y = 0.0710148606463189, + }, + 0xf04b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9997041418532618, + .relative_y = 0.0002958581467381, + }, + 0xf04c...0xf04d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8572940020656472, + .relative_y = 0.0713404035569438, + }, + 0xf04f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7138835298072554, + .relative_y = 0.1433479295317200, + }, + 0xf052, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9999748091795350, + }, + 0xf060...0xf061, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8567975830815709, + .relative_y = 0.0719033232628399, + }, + 0xf063, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987915407854985, + .relative_y = 0.0006042296072508, + }, + 0xf077, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5700483091787439, + .relative_y = 0.2862318840579710, + }, + 0xf078, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5700483091787439, + .relative_y = 0.1437198067632850, + }, + 0xf07e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4989429175475687, + .relative_y = 0.2505285412262157, + }, + 0xf089, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9998488512696494, + .relative_y = 0.0001511487303507, + }, + 0xf0a4...0xf0a5, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7502645502645503, + .relative_y = 0.1248677248677249, + }, + 0xf0d7, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4281400966183575, + .relative_y = 0.2053140096618357, + }, + 0xf0d8, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4281400966183575, + .relative_y = 0.3472222222222222, + }, + 0xf0d9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7140772371750631, + .relative_y = 0.1333462732919255, + }, + 0xf0da, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7140396210163651, + .relative_y = 0.1333838894506235, + }, + 0xf0dc, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + }, + 0xf0dd, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .relative_height = 0.4275362318840580, + .relative_y = 0.0012077294685990, + }, + 0xf0de, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .relative_height = 0.4287439613526570, + .relative_y = 0.5712560386473430, + }, + 0xf100...0xf101, + 0xf104...0xf105, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8573155985489722, + .relative_y = 0.0713422007255139, + }, + 0xf102, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9286577992744861, + .relative_y = 0.0713422007255139, + }, + 0xf103, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9286577992744861, }, 0xf106...0xf107, => .{ @@ -1161,8 +2311,8 @@ pub fn getConstraint(cp: u21) ?Constraint { .height = .icon, .align_horizontal = .center1, .align_vertical = .center1, - .relative_height = 0.4038085937500000, - .relative_y = 0.3266601562500000, + .relative_height = 0.5000000000000000, + .relative_y = 0.2853688029020556, }, 0xf130, => .{ @@ -1181,16 +2331,6 @@ pub fn getConstraint(cp: u21) ?Constraint { .relative_height = 0.2593984962406015, .relative_y = 0.3696741854636592, }, - 0xf153...0xf154, - 0xf158, - => .{ - .size = .fit_cover1, - .height = .icon, - .align_horizontal = .center1, - .align_vertical = .center1, - .relative_height = 0.8751322751322751, - .relative_y = 0.0624338624338624, - }, 0xf156, => .{ .size = .fit_cover1, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index f5bac5e86..8ddc0c113 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -282,12 +282,12 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) # `overlap` and `ypadding` are mutually exclusive, # this is asserted in the nerd fonts patcher itself. if overlap: - pad = -overlap + pad = -overlap / 2 s += f" .pad_left = {pad},\n" s += f" .pad_right = {pad},\n" # In the nerd fonts patcher, overlap values # are capped at 0.01 in the vertical direction. - v_pad = -min(0.01, overlap) + v_pad = -min(0.01, overlap) / 2 s += f" .pad_top = {v_pad},\n" s += f" .pad_bottom = {v_pad},\n" elif y_padding: @@ -314,7 +314,7 @@ def generate_codepoint_tables( return nerd_font_codepoint_tables.cp_tables cp_tables: dict[str, dict[int, int]] = {} - cp_table_full: dict[int, int] = {} + cp_nerdfont_used: set[int] = set() cmap = nerd_font.getBestCmap() for entry in patch_sets: patch_set_name = entry["Name"] @@ -381,12 +381,12 @@ def generate_codepoint_tables( raise ValueError( f"Missing codepoint in Symbols Only Font: {hex(cp_nerdfont)} in patch set '{patch_set_name}'" ) - elif cp_nerdfont in cp_table_full.values(): + elif cp_nerdfont in cp_nerdfont_used: raise ValueError( f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'" ) cp_tables[patch_set_name][cp_original] = cp_nerdfont - cp_table_full |= cp_tables[patch_set_name] + cp_nerdfont_used.add(cp_nerdfont) # Store the table and corresponding Nerd Fonts version together in a module. with open("nerd_font_codepoint_tables.py", "w") as f: @@ -419,9 +419,6 @@ def generate_zig_switch_arms( cmap = nerd_font.getBestCmap() glyphs = nerd_font.getGlyphSet() cp_tables = generate_codepoint_tables(patch_sets, nerd_font, nf_version) - cp_table_full: dict[int, int] = {} - for cp_table in cp_tables.values(): - cp_table_full |= cp_table entries: dict[int, PatchSetAttributeEntry] = {} for entry in patch_sets: @@ -454,9 +451,37 @@ def generate_zig_switch_arms( individual_bounds: dict[int, tuple[int, int, int, int]] = {} individual_advances: set[float] = set() for cp_original in group: - # Scale groups may cut across patch sets, so we need to use - # the full lookup table here - cp_nerdfont = cp_table_full[cp_original] + if cp_original not in cp_table: + # There is one special case where a scale group includes + # a glyph from the original font that's not in any patch + # set, and hence not in the Symbols Only font. The point + # of this glyph is to add extra vertical padding to a + # stretched (^xy) scale group, which means that its + # scaled and aligned position would span the line height + # plus overlap. Thus, we can use any other stretched + # glyph with overlap as stand-in to get the vertical + # bounds, such as as 0xE0B0 (powerline left hard + # divider). We don't worry about the horizontal bounds, + # as they by design should not affect the group's + # bounding box. + if ( + patch_set_name == "Progress Indicators" + and cp_original == 0xEDFF + ): + glyph = glyphs[cmap[0xE0B0]] + bounds = BoundsPen(glyphSet=glyphs) + glyph.draw(bounds) + yMin = min(bounds.bounds[1], yMin) + yMax = max(bounds.bounds[3], yMax) + else: + # Other cases are due to lazily specified scale + # groups with gaps in the codepoint range. + print( + f"Info: Skipping scale group codepoint {hex(cp_original)}, which does not exist in patch set '{patch_set_name}'" + ) + continue + + cp_nerdfont = cp_table[cp_original] glyph = glyphs[cmap[cp_nerdfont]] individual_advances.add(glyph.width) bounds = BoundsPen(glyphSet=glyphs) @@ -472,7 +497,9 @@ def generate_zig_switch_arms( len(individual_advances) == 1 ) for cp_original in group: - cp_nerdfont = cp_table_full[cp_original] + if cp_original not in cp_table: + continue + cp_nerdfont = cp_table[cp_original] if ( # Scale groups may cut across patch sets, but we're only # updating a single patch set at a time, so we skip From 6faf7fcac3ff09d0471bcb7ed2dc0b77b052604b Mon Sep 17 00:00:00 2001 From: Rohan Alexander Date: Sun, 12 Oct 2025 10:21:49 -0400 Subject: [PATCH 251/319] Add missing word to README.md (#9165) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df86f7830..7124400fd 100644 --- a/README.md +++ b/README.md @@ -193,4 +193,4 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us. > purposely contain sensitive information, but it does contain the full > stack memory of each thread at the time of the crash. This information > is used to rebuild the stack trace but can also contain sensitive data -> depending when the crash occurred. +> depending on when the crash occurred. From cbc06a0abc2457b025438adc3d0f57e1c0eca3fa Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Sun, 12 Oct 2025 16:25:18 +0200 Subject: [PATCH 252/319] macOS: Support building with Xcode 16 (#9162) With this you can test most of the old tab bar behaviour without using a virtual machine --- .../Features/App Intents/CloseTerminalIntent.swift | 2 ++ .../Features/App Intents/CommandPaletteIntent.swift | 2 ++ .../Features/App Intents/FocusTerminalIntent.swift | 2 ++ .../App Intents/GetTerminalDetailsIntent.swift | 2 ++ macos/Sources/Features/App Intents/InputIntent.swift | 10 ++++++++++ macos/Sources/Features/App Intents/KeybindIntent.swift | 2 ++ .../Features/App Intents/NewTerminalIntent.swift | 2 ++ .../Features/App Intents/QuickTerminalIntent.swift | 2 ++ .../Sources/Features/Terminal/TerminalController.swift | 4 ++++ 9 files changed, 28 insertions(+) diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 923d22c97..0155cf855 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -12,8 +12,10 @@ struct CloseTerminalIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult { diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index fa983054b..2f07d7861 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -19,8 +19,10 @@ struct CommandPaletteIntent: AppIntent { ) var command: CommandEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult & ReturnsValue { diff --git a/macos/Sources/Features/App Intents/FocusTerminalIntent.swift b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift index 4e813e842..21dd71b15 100644 --- a/macos/Sources/Features/App Intents/FocusTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift @@ -12,8 +12,10 @@ struct FocusTerminalIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult { diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 1cbaa9d68..563e3719b 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -17,8 +17,10 @@ struct GetTerminalDetailsIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif static var parameterSummary: some ParameterSummary { Summary("Get \(\.$detail) from \(\.$terminal)") diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 17c97fbbb..d169b3a8c 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -24,8 +24,10 @@ struct InputTextIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -74,8 +76,10 @@ struct KeyEventIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -136,8 +140,10 @@ struct MouseButtonIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -197,8 +203,10 @@ struct MousePosIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -265,8 +273,10 @@ struct MouseScrollIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index b31da4a50..a8cea8561 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -16,8 +16,10 @@ struct KeybindIntent: AppIntent { ) var action: String +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult & ReturnsValue { diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 46a752198..be5c65bfa 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -45,8 +45,10 @@ struct NewTerminalIntent: AppIntent { // Performing in the background can avoid opening multiple windows at the same time // using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") static var openAppWhenRun = false diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index 2e6c9850c..2048a3b88 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -5,8 +5,10 @@ struct QuickTerminalIntent: AppIntent { static var title: LocalizedStringResource = "Open the Quick Terminal" static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.") +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 779c13d9c..9790063d7 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -23,11 +23,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" case "tabs": +#if compiler(>=6.2) if #available(macOS 26.0, *) { "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } +#else + "TerminalTabsTitlebarVentura" +#endif default: defaultValue } From 47a8f8083dd07934fa3284cb758c3e04bffcd638 Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Sun, 12 Oct 2025 16:31:42 +0200 Subject: [PATCH 253/319] macOS: Fix more `macos-titlebar-style` related issues (#9163) ### This PR depends on #9162 #1691 doesn't seem to be an issue anymore, but changing appearance while Ghosty is active will result in similar nastiness. https://github.com/user-attachments/assets/fcd7761e-a521-4382-8d7a-9d93dc0806bc ### Changes - [Sequoia/Ventura] Fix flickering new tab icon, and it also didn't respond to window's key status change correctly - [Sequoia/Ventura] Fix after changing appearance, tab bar may disappear or have inconsistent background colour - Fix initial tint of reset zoom button on Sequoia/Ventura with `macos-titlebar-style=tabs` and all `native/transparent` titlebars - Fix title alignment with custom font with `native/transparent` titlebar --- .../Window Styles/TerminalWindow.swift | 14 +++- .../TitlebarTabsVenturaTerminalWindow.swift | 69 +++++++++---------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 95126e188..8249c6cf7 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -159,6 +159,12 @@ class TerminalWindow: NSWindow { } else { tabBarDidDisappear() } + viewModel.isMainWindow = true + } + + override func resignMain() { + super.resignMain() + viewModel.isMainWindow = false } override func mergeAllWindows(_ sender: Any?) { @@ -298,7 +304,7 @@ class TerminalWindow: NSWindow { button.isBordered = false button.allowsExpansionToolTips = true button.toolTip = "Reset Zoom" - button.contentTintColor = .controlAccentColor + button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor button.state = .on button.image = NSImage(named:"ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) @@ -324,6 +330,7 @@ class TerminalWindow: NSWindow { let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) titlebarTextField?.font = font + titlebarTextField?.usesSingleLineMode = true tab.attributedTitle = attributedTitle } } @@ -505,7 +512,8 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false - + @Published var isMainWindow: Bool = true + /// Calculates the top padding based on toolbar visibility and macOS version fileprivate var accessoryTopPadding: CGFloat { if #available(macOS 26.0, *) { @@ -525,7 +533,7 @@ extension TerminalWindow { VStack { Button(action: action) { Image("ResetZoom") - .foregroundColor(.accentColor) + .foregroundColor(viewModel.isMainWindow ? .accentColor : .secondary) } .buttonStyle(.plain) .help("Reset Split Zoom") diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 0c087faeb..9aa8ec2eb 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -145,6 +145,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { super.syncAppearance(surfaceConfig) // Update our window light/darkness based on our updated background color + let themeChanged = isLightTheme != OSColor(surfaceConfig.backgroundColor).isLightColor isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor // Update our titlebar color @@ -154,7 +155,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } - if (isOpaque) { + if (isOpaque || themeChanged) { // If there is transparency, calling this will make the titlebar opaque // so we only call this if we are opaque. updateTabBar() @@ -187,41 +188,33 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // so we need to do it manually. private func updateNewTabButtonOpacity() { guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } + guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return } newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5 } - // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, - // just as it does in the stock tab bar. + /// Update: This method only add a vibrant overlay now, + /// since the image itself supports light/dark tint, + /// and system could restore it any time, + /// altering it will only cause maintenance burden for us. + /// + /// And if we hide original image, + /// ``updateNewTabButtonOpacity`` will not work + /// + /// ~~Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,~~ + /// ~~just as it does in the stock tab bar.~~ private func updateNewTabButtonImage() { guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } + guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return } guard let newTabButtonImage = newTabButtonImageView.image else { return } + let imageLayer = newTabButtonImageLayer ?? VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! + imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) + imageLayer.contentsGravity = .resizeAspect + imageLayer.opacity = 0.5 - if newTabButtonImageLayer == nil { - let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) - let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in - newTabButtonImage.draw(in: rect) - fillColor.setFill() - rect.fill(using: .sourceAtop) - return true - } - let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! - imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) - imageLayer.contentsGravity = .resizeAspect - imageLayer.contents = newImage - imageLayer.opacity = 0.5 + newTabButtonImageLayer = imageLayer - newTabButtonImageLayer = imageLayer - } - - newTabButtonImageView.isHidden = true newTabButton.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() newTabButton.layer?.addSublayer(newTabButtonImageLayer!) } @@ -452,6 +445,13 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { + guard windowButtonsBackdrop?.superview != titlebarView else { + /// replacing existing backdrop aggressively + /// may cause incorrect hierarchy + /// + /// because multiple windows are adding this around the 'same time' + return + } windowButtonsBackdrop?.removeFromSuperview() windowButtonsBackdrop = nil @@ -470,16 +470,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) { // If we already made the view, just make sure it's unhidden and correctly placed as a subview. - if let view = windowDragHandle { - view.removeFromSuperview() - view.isHidden = false - titlebarView.superview?.addSubview(view) - view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true - view.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true - view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true - view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true + guard windowDragHandle?.superview != titlebarView.superview else { + // similar to `addWindowButtonsBackdrop` return } + windowDragHandle?.removeFromSuperview() let view = WindowDragView() view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle") @@ -540,7 +535,10 @@ fileprivate class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? - private let isLightTheme: Bool + private var isLightTheme: Bool { + // using up-to-date value from hosting window directly + terminalWindow?.isLightTheme ?? false + } private let overlayLayer = VibrantLayer() var isHighlighted: Bool = true { @@ -569,7 +567,6 @@ fileprivate class WindowButtonsBackdropView: NSView { init(window: TitlebarTabsVenturaTerminalWindow) { self.terminalWindow = window - self.isLightTheme = window.isLightTheme super.init(frame: .zero) From 65f73f5d20637234058b633855a04c4cbaf14092 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 12 Oct 2025 07:31:54 -0700 Subject: [PATCH 254/319] font: Apply `adjust-icon-height` to both large and small icons (#9160) As pointed out in #9156, an unintended consequence of all the work to get icon sizing right is that `adjust-icon-height` now only applies to the small icons you get when the next cell is not whitespace. Large icons are unaffected. With this PR, `adjust-icon-height` affects the maximum height of every symbol specifying the `.icon` constraint height, regardless of constraint width. This includes most Nerd Font icons, but excludes emoji and other unicode symbols, and also excludes terminal graphics-oriented Nerd Font symbols such as Powerline symbols. In the following screenshots, **Baseline** is without `adjust-icon-height`, while **Before** and **After** are with `adjust-icon-height = -25%`. **Baseline** Screenshot 2025-10-11 at 23 28 20 **Before** (only small icons affected) Screenshot 2025-10-11 at 23 20 12 **After** (both small and large icons affected, but not emoji) Screenshot 2025-10-11 at 23 21 05 --- src/config/Config.zig | 15 +++++------ src/font/Collection.zig | 6 +++-- src/font/Metrics.zig | 59 +++++++++++++++++++++++++++++++++++++++-- src/font/face.zig | 23 +++++++++------- 4 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index a203a32a1..7d8c7d9ff 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -412,16 +412,13 @@ pub const compatibility = std.StaticStringMap( @"adjust-box-thickness": ?MetricModifier = null, /// Height in pixels or percentage adjustment of maximum height for nerd font icons. /// -/// Increasing this value will allow nerd font icons to be larger, but won't -/// necessarily force them to be. Decreasing this value will make nerd font -/// icons smaller. +/// A positive (negative) value will increase (decrease) the maximum icon +/// height. This may not affect all icons equally: the effect depends on whether +/// the default size of the icon is height-constrained, which in turn depends on +/// the aspect ratio of both the icon and your primary font. /// -/// This value only applies to icons that are constrained to a single cell by -/// neighboring characters. An icon that is free to spread across two cells -/// can always use up to the full line height of the primary font. -/// -/// The default value is 2/3 times the height of capital letters in your primary -/// font plus 1/3 times the font's line height. +/// Certain icons designed for box drawing and terminal graphics, such as +/// Powerline symbols, are not affected by this option. /// /// See the notes about adjustments in `adjust-cell-width`. /// diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 5ec076608..b587245aa 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1228,7 +1228,8 @@ test "metrics" { .overline_thickness = 1, .box_thickness = 1, .cursor_height = 17, - .icon_height = 12.24, + .icon_height = 16.784, + .icon_height_single = 12.24, .face_width = 8.0, .face_height = 16.784, .face_y = -0.04, @@ -1248,7 +1249,8 @@ test "metrics" { .overline_thickness = 2, .box_thickness = 2, .cursor_height = 34, - .icon_height = 24.48, + .icon_height = 33.568, + .icon_height_single = 24.48, .face_width = 16.0, .face_height = 33.568, .face_y = -0.08, diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 668b6f15f..ec89763ea 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -38,6 +38,9 @@ cursor_height: u32, /// The constraint height for nerd fonts icons. icon_height: f64, +/// The constraint height for nerd fonts icons limited to a single cell width. +icon_height_single: f64, + /// The unrounded face width, used in scaling calculations. face_width: f64, @@ -60,6 +63,7 @@ const Minimums = struct { const cursor_thickness = 1; const cursor_height = 1; const icon_height = 1.0; + const icon_height_single = 1.0; const face_height = 1.0; const face_width = 1.0; }; @@ -251,8 +255,11 @@ pub fn calc(face: FaceMetrics) Metrics { const underline_position = @round(top_to_baseline - face.underlinePosition()); const strikethrough_position = @round(top_to_baseline - face.strikethroughPosition()); - // Same heuristic as the font_patcher script - const icon_height = (2 * cap_height + face_height) / 3; + // Same heuristic as the font_patcher script. We store icon_height + // separately from face_height such that modifiers can apply to the former + // without affecting the latter. + const icon_height = face_height; + const icon_height_single = (2 * cap_height + face_height) / 3; var result: Metrics = .{ .cell_width = @intFromFloat(cell_width), @@ -267,6 +274,7 @@ pub fn calc(face: FaceMetrics) Metrics { .box_thickness = @intFromFloat(underline_thickness), .cursor_height = @intFromFloat(cell_height), .icon_height = icon_height, + .icon_height_single = icon_height_single, .face_width = face_width, .face_height = face_height, .face_y = face_y, @@ -328,6 +336,10 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { } } }, + inline .icon_height => { + self.icon_height = entry.value_ptr.apply(self.icon_height); + self.icon_height_single = entry.value_ptr.apply(self.icon_height_single); + }, inline else => |tag| { @field(self, @tagName(tag)) = entry.value_ptr.apply(@field(self, @tagName(tag))); @@ -529,6 +541,7 @@ fn init() Metrics { .box_thickness = 0, .cursor_height = 0, .icon_height = 0.0, + .icon_height_single = 0.0, .face_width = 0.0, .face_height = 0.0, .face_y = 0.0, @@ -609,6 +622,48 @@ test "Metrics: adjust cell height larger" { try testing.expectEqual(@as(u32, 100), m.cursor_height); } +test "Metrics: adjust icon height by percentage" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: ModifierSet = .{}; + defer set.deinit(alloc); + try set.put(alloc, .icon_height, .{ .percent = 0.75 }); + + var m: Metrics = init(); + m.icon_height = 100.0; + m.icon_height_single = 80.0; + m.face_height = 100.0; + m.face_y = 1.0; + m.apply(set); + try testing.expectEqual(75.0, m.icon_height); + try testing.expectEqual(60.0, m.icon_height_single); + // Face metrics not affected + try testing.expectEqual(100.0, m.face_height); + try testing.expectEqual(1.0, m.face_y); +} + +test "Metrics: adjust icon height by absolute pixels" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: ModifierSet = .{}; + defer set.deinit(alloc); + try set.put(alloc, .icon_height, .{ .absolute = -5 }); + + var m: Metrics = init(); + m.icon_height = 100.0; + m.icon_height_single = 80.0; + m.face_height = 100.0; + m.face_y = 1.0; + m.apply(set); + try testing.expectEqual(95.0, m.icon_height); + try testing.expectEqual(75.0, m.icon_height_single); + // Face metrics not affected + try testing.expectEqual(100.0, m.face_height); + try testing.expectEqual(1.0, m.face_y); +} + test "Modifier: parse absolute" { const testing = std.testing; diff --git a/src/font/face.zig b/src/font/face.zig index 586de4ad6..a1312c45a 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -216,11 +216,13 @@ pub const RenderOptions = struct { }; pub const Height = enum { - /// Always use the full height of the cell for constraining this glyph. + /// Use the full line height of the primary face for + /// constraining this glyph. cell, - /// When the constraint width is 1, use the "icon height" from the grid - /// metrics as the height. (When the constraint width is >1, the - /// constraint height is always the full cell height.) + /// Use the icon height from the grid metrics for + /// constraining this glyph. Unlike `cell`, the value of + /// this height depends on both the constraint width and the + /// affected by the `adjust-icon-height` config option. icon, }; @@ -346,12 +348,14 @@ pub const RenderOptions = struct { const target_width = pad_width_factor * metrics.face_width; const target_height = pad_height_factor * switch (self.height) { .cell => metrics.face_height, - // icon_height only applies with single-cell constraints. - // This mirrors font_patcher. + // Like font-patcher, the icon constraint height depends on the + // constraint width. Unlike font-patcher, the multi-cell + // icon_height may be different from face_height due to the + // `adjust-icon-height` config option. .icon => if (multi_cell) - metrics.face_height + metrics.icon_height else - metrics.icon_height, + metrics.icon_height_single, }; var width_factor = target_width / group.width; @@ -528,7 +532,8 @@ test "Constraints" { .box_thickness = 1, .cursor_thickness = 1, .cursor_height = 22, - .icon_height = 44.48 / 3.0, + .icon_height = 21.12, + .icon_height_single = 44.48 / 3.0, .face_width = 9.6, .face_height = 21.12, .face_y = 0.2, From 5efb915771e3eec18592d25d57a7a82c1f7482b9 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Sun, 12 Oct 2025 13:55:21 -0400 Subject: [PATCH 255/319] Fix fish shell cursor integration in fish vi mode (#9157) Previously, the fish shell integration interfered with fish's builtin vi mode cursor switching configurations such as `$fish_cursor_default` and `$fish_cursor_insert`. ```console $ ghostty --config-default-files=false -e fish --no-config --init-command 'source "$GHOSTTY_RESOURCES_DIR"/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish; fish_vi_key_bindings' ``` The above command starts fish in vi mode with Ghostty shell integrations. Manually loading the integration is necessary due to `--no-config` blocking auto injection. 1. At the prompt, fish is in insert mode, and the cursor is a blinking beam. However, press escape and then "i" to exit then re-enter insert mode, and the cursor will be a solid beam due to the `$fish_cursor_unknown` setting. Without the shell integration, insert mode always uses a solid beam cursor. 2. A similar problem shows if we start fish with `fish_vi_key_bindings default`. The cursor ends up as a blinking beam in normal mode only due to the shell integration interfering. This glitch can also be reset away by entering then exiting insert mode. 3. Also, `$fish_cursor_external` has no effect when used with shell integration. After `fish_vi_key_bindings`, set it to `line`, run cat(1), and shell integration will give you a blinking block, not the asked for line/beam. I verified that this patch makes the shell integration stop interfering in three scenarios above, and it still changes the cursor when not using fish's vi mode. Note that `$fish_cursor_*` variables can be set when fish isn't in vi mode, so they're not great signals for the shell integration hooks. --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 7042f892a..f745bbb13 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -54,10 +54,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if contains cursor $features # Change the cursor to a beam on prompt. function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" - echo -en "\e[5 q" + if not functions -q fish_vi_cursor_handle + echo -en "\e[5 q" + end end function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape" - echo -en "\e[0 q" + if not functions -q fish_vi_cursor_handle + echo -en "\e[0 q" + end end end From 8d8821004ecf288d6d97fe019df72558e088da2f Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:04:18 +0200 Subject: [PATCH 256/319] macOS: fix title misalignment in tabs (#9168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While I was testing #9166, noticed another edge case🤯. This appears both in Tahoe and Sequoia👇🏻 https://github.com/user-attachments/assets/9cecea35-1241-4f31-9c15-0f2a7a6f342a --- .../Window Styles/TerminalWindow.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 8249c6cf7..16fcf227f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -205,9 +205,16 @@ class TerminalWindow: NSWindow { /// Returns true if there is a tab bar visible on this window. var hasTabBar: Bool { + // TODO: use titlebarView to find it instead contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil } + var hasMoreThanOneTabs: Bool { + /// accessing ``tabGroup?.windows`` here + /// will cause other edge cases, be careful + (tabbedWindows?.count ?? 0) > 1 + } + func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { if childViewController.identifier == nil { // The good case @@ -321,6 +328,12 @@ class TerminalWindow: NSWindow { // Whenever we change the window title we must also update our // tab title if we're using custom fonts. tab.attributedTitle = attributedTitle + /// We also needs to update this here, just in case + /// the value is not what we want + /// + /// Check ``titlebarFont`` down below + /// to see why we need to check `hasMoreThanOneTabs` here + titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs } } @@ -330,7 +343,12 @@ class TerminalWindow: NSWindow { let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) titlebarTextField?.font = font - titlebarTextField?.usesSingleLineMode = true + /// We check `hasMoreThanOneTabs` here because the system + /// may copy this setting to the tab’s text field at some point(e.g. entering/exiting fullscreen), + /// which can cause the title to be vertically misaligned (shifted downward). + /// + /// This behaviour is the opposite of what happens in the title bar’s text field, which is quite odd... + titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs tab.attributedTitle = attributedTitle } } From 03e71e86a486c09b7f3d24a1cc62f977d14f47c4 Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:07:29 +0200 Subject: [PATCH 257/319] macOS: distinguish between Debug and Release(Stable/Tip) (#9149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Background Been running Ghostty locally for a while now, and I use the Finder service a lot. It often confuses me which one is the official one, until I actually open it. ### Changes - Use blueprint to distinguish from release app, if no custom icon specified - Change BundleDisplayName to Ghostty[Debug] - Enable Info.plist preprocessing for reading `$(INFOPLIST_KEY_CFBundleDisplayName)` for providing different services with different configurations > (Preprocessing was once reverted before](https://github.com/ghostty-org/ghostty/commit/6508fec), so I'm not sure whether this follows the 'rules' here, but for now, there are no links in the plist file, so I think it’s [safe](https://developer.apple.com/library/archive/technotes/tn2175/_index.html#:~:text=can%20pass%20the-,%2Dtraditional,-flag%20to%20the) to enable it --- macos/Ghostty-Info.plist | 4 ++-- macos/Ghostty.xcodeproj/project.pbxproj | 7 +++++-- macos/Sources/App/macOS/AppDelegate.swift | 5 +++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index ff391c0f8..2bf3b0bae 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -61,7 +61,7 @@ NSMenuItem default - New Ghostty Tab Here + New $(INFOPLIST_KEY_CFBundleDisplayName) Tab Here NSMessage openTab @@ -80,7 +80,7 @@ NSMenuItem default - New Ghostty Window Here + New $(INFOPLIST_KEY_CFBundleDisplayName) Window Here NSMessage openWindow diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index c2baf834d..388122f62 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -553,6 +553,7 @@ INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; + INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -775,7 +776,7 @@ EXECUTABLE_NAME = ghostty; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Ghostty-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth."; @@ -793,6 +794,7 @@ INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; + INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -847,6 +849,7 @@ INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; + INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -865,7 +868,7 @@ A5D449A82B53AE7B000F5B83 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty; + ASSETCATALOG_COMPILER_APPICON_NAME = "Ghostty-Debug"; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 6f387f4ae..3319189b9 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -121,7 +121,12 @@ class AppDelegate: NSObject, /// The custom app icon image that is currently in use. @Published private(set) var appIcon: NSImage? = nil { didSet { +#if DEBUG + // if no custom icon specified, we use blueprint to distinguish from release app + NSApplication.shared.applicationIconImage = appIcon ?? NSImage(named: "BlueprintImage") +#else NSApplication.shared.applicationIconImage = appIcon +#endif let appPath = Bundle.main.bundlePath NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: []) NSWorkspace.shared.noteFileSystemChanged(appPath) From 37b3c270204e903b493593c2117d4a6e5b32a293 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 12 Oct 2025 15:11:52 -0500 Subject: [PATCH 258/319] synthetic: use std.Io.Writer for more of the interface (#9038) --- src/synthetic/Bytes.zig | 28 +++++++----- src/synthetic/Generator.zig | 14 +++--- src/synthetic/Osc.zig | 88 +++++++++++++++++++++---------------- src/synthetic/Utf8.zig | 23 ++++++---- src/synthetic/cli.zig | 4 +- src/synthetic/cli/Ascii.zig | 4 +- src/synthetic/cli/Osc.zig | 13 +++--- src/synthetic/cli/Utf8.zig | 4 +- 8 files changed, 98 insertions(+), 80 deletions(-) diff --git a/src/synthetic/Bytes.zig b/src/synthetic/Bytes.zig index 8a8207ba9..40a94e0e3 100644 --- a/src/synthetic/Bytes.zig +++ b/src/synthetic/Bytes.zig @@ -27,27 +27,35 @@ pub fn generator(self: *Bytes) Generator { return .init(self, next); } -pub fn next(self: *Bytes, buf: []u8) Generator.Error![]const u8 { +pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { + std.debug.assert(max_len >= 1); const len = @min( self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - buf.len, + max_len, ); - const result = buf[0..len]; - self.rand.bytes(result); - if (self.alphabet) |alphabet| { - for (result) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + var buf: [8]u8 = undefined; + var remaining = len; + while (remaining > 0) { + const data = buf[0..@min(remaining, buf.len)]; + self.rand.bytes(data); + if (self.alphabet) |alphabet| { + for (data) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + } + try writer.writeAll(data); + remaining -= data.len; } - - return result; } test "bytes" { const testing = std.testing; var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var v: Bytes = .{ .rand = prng.random() }; + v.min_len = buf.len; + v.max_len = buf.len; const gen = v.generator(); - const result = try gen.next(&buf); - try testing.expect(result.len > 0); + try gen.next(&writer, buf.len); + try testing.expectEqual(buf.len, writer.buffered().len); } diff --git a/src/synthetic/Generator.zig b/src/synthetic/Generator.zig index 7478a54c3..28929ecbe 100644 --- a/src/synthetic/Generator.zig +++ b/src/synthetic/Generator.zig @@ -6,27 +6,27 @@ const assert = std.debug.assert; /// For generators, this is the only error that is allowed to be /// returned by the next function. -pub const Error = error{NoSpaceLeft}; +pub const Error = error{WriteFailed}; /// The vtable for the generator. ptr: *anyopaque, -nextFn: *const fn (ptr: *anyopaque, buf: []u8) Error![]const u8, +nextFn: *const fn (ptr: *anyopaque, *std.Io.Writer, usize) Error!void, /// Create a new generator from a pointer and a function pointer. /// This usually is only called by generator implementations, not /// generator users. pub fn init( pointer: anytype, - comptime nextFn: fn (ptr: @TypeOf(pointer), buf: []u8) Error![]const u8, + comptime nextFn: fn (ptr: @TypeOf(pointer), *std.Io.Writer, usize) Error!void, ) Generator { const Ptr = @TypeOf(pointer); assert(@typeInfo(Ptr) == .pointer); // Must be a pointer assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct const gen = struct { - fn next(ptr: *anyopaque, buf: []u8) Error![]const u8 { + fn next(ptr: *anyopaque, writer: *std.Io.Writer, max_len: usize) Error!void { const self: Ptr = @ptrCast(@alignCast(ptr)); - return try nextFn(self, buf); + try nextFn(self, writer, max_len); } }; @@ -37,6 +37,6 @@ pub fn init( } /// Get the next value from the generator. Returns the data written. -pub fn next(self: Generator, buf: []u8) Error![]const u8 { - return try self.nextFn(self.ptr, buf); +pub fn next(self: Generator, writer: *std.Io.Writer, max_size: usize) Error!void { + try self.nextFn(self.ptr, writer, max_size); } diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig index 8d5d7d3a2..d78b95a1e 100644 --- a/src/synthetic/Osc.zig +++ b/src/synthetic/Osc.zig @@ -53,6 +53,9 @@ pub fn generator(self: *Osc) Generator { return .init(self, next); } +const osc = std.fmt.comptimePrint("{c}]", .{std.ascii.control_code.esc}); +const st = std.fmt.comptimePrint("{c}", .{std.ascii.control_code.bel}); + /// Get the next OSC request in bytes. The generated OSC request will /// have the prefix `ESC ]` and the terminator `BEL` (0x07). /// @@ -63,23 +66,22 @@ pub fn generator(self: *Osc) Generator { /// /// The buffer must be at least 3 bytes long to accommodate the /// prefix and terminator. -pub fn next(self: *Osc, buf: []u8) Generator.Error![]const u8 { - if (buf.len < 3) return error.NoSpaceLeft; - const unwrapped = try self.nextUnwrapped(buf[2 .. buf.len - 1]); - buf[0] = 0x1B; // ESC - buf[1] = ']'; - buf[unwrapped.len + 2] = 0x07; // BEL - return buf[0 .. unwrapped.len + 3]; +pub fn next(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { + assert(max_len >= 3); + try writer.writeAll(osc); + try self.nextUnwrapped(writer, max_len - (osc.len + st.len)); + try writer.writeAll(st); } -fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { +fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { return switch (self.chooseValidity()) { .valid => valid: { const Indexer = @TypeOf(self.p_valid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_valid_kind.values); break :valid try self.nextUnwrappedValidExact( - buf, + writer, Indexer.keyForIndex(idx), + max_len, ); }, @@ -87,70 +89,64 @@ fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { const Indexer = @TypeOf(self.p_invalid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_invalid_kind.values); break :invalid try self.nextUnwrappedInvalidExact( - buf, + writer, Indexer.keyForIndex(idx), + max_len, ); }, }; } -fn nextUnwrappedValidExact(self: *const Osc, buf: []u8, k: ValidKind) Generator.Error![]const u8 { - var fbs = std.io.fixedBufferStream(buf); +fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void { switch (k) { .change_window_title => { - try fbs.writer().writeAll("0;"); // Set window title + try writer.writeAll("0;"); // Set window title var bytes_gen = self.bytes(); - const title = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(title.len)); + try bytes_gen.next(writer, max_len - 2); }, .prompt_start => { - try fbs.writer().writeAll("133;A"); // Start prompt + try writer.writeAll("133;A"); // Start prompt // aid if (self.rand.boolean()) { var bytes_gen = self.bytes(); bytes_gen.max_len = 16; - try fbs.writer().writeAll(";aid="); - const aid = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(aid.len)); + try writer.writeAll(";aid="); + try bytes_gen.next(writer, max_len); } // redraw if (self.rand.boolean()) { - try fbs.writer().writeAll(";redraw="); + try writer.writeAll(";redraw="); if (self.rand.boolean()) { - try fbs.writer().writeAll("1"); + try writer.writeAll("1"); } else { - try fbs.writer().writeAll("0"); + try writer.writeAll("0"); } } }, - .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt + .prompt_end => try writer.writeAll("133;B"), // End prompt } - - return fbs.getWritten(); } fn nextUnwrappedInvalidExact( self: *const Osc, - buf: []u8, + writer: *std.Io.Writer, k: InvalidKind, -) Generator.Error![]const u8 { + max_len: usize, +) Generator.Error!void { switch (k) { .random => { var bytes_gen = self.bytes(); - return try bytes_gen.next(buf); + try bytes_gen.next(writer, max_len); }, .good_prefix => { - var fbs = std.io.fixedBufferStream(buf); - try fbs.writer().writeAll("133;"); + try writer.writeAll("133;"); var bytes_gen = self.bytes(); - const data = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(data.len)); - return fbs.getWritten(); + try bytes_gen.next(writer, max_len - 4); }, } } @@ -177,11 +173,21 @@ const Validity = enum { valid, invalid }; const test_seed = 0xC0FFEEEEEEEEEEEE; test "OSC generator" { + const testing = std.testing; var prng = std.Random.DefaultPrng.init(test_seed); - var buf: [4096]u8 = undefined; - var v: Osc = .{ .rand = prng.random() }; - const gen = v.generator(); - for (0..50) |_| _ = try gen.next(&buf); + var buf: [256]u8 = undefined; + { + var v: Osc = .{ + .rand = prng.random(), + }; + const gen = v.generator(); + for (0..50) |_| { + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const result = writer.buffered(); + try testing.expect(result.len > 0); + } + } } test "OSC generator valid" { @@ -195,7 +201,9 @@ test "OSC generator valid" { .p_valid = 1.0, }; for (0..50) |_| { - const seq = try gen.next(&buf); + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const seq = writer.buffered(); var parser: terminal.osc.Parser = .init(); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) != null); @@ -213,7 +221,9 @@ test "OSC generator invalid" { .p_valid = 0.0, }; for (0..50) |_| { - const seq = try gen.next(&buf); + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const seq = writer.buffered(); var parser: terminal.osc.Parser = .init(); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) == null); diff --git a/src/synthetic/Utf8.zig b/src/synthetic/Utf8.zig index c3ace6505..0d72a8bb2 100644 --- a/src/synthetic/Utf8.zig +++ b/src/synthetic/Utf8.zig @@ -41,13 +41,12 @@ pub fn generator(self: *Utf8) Generator { return .init(self, next); } -pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { +pub fn next(self: *Utf8, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { const len = @min( self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - buf.len, + max_len, ); - const result = buf[0..len]; var rem: usize = len; while (rem > 0) { // Pick a utf8 byte count to generate. @@ -75,9 +74,11 @@ pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { assert(std.unicode.utf8CodepointSequenceLength( cp, ) catch unreachable == @intFromEnum(utf8_len)); - rem -= std.unicode.utf8Encode( + + var buf: [4]u8 = undefined; + const l = std.unicode.utf8Encode( cp, - result[result.len - rem ..], + &buf, ) catch |err| switch (err) { // Impossible because our generation above is hardcoded to // produce a valid range. If not, a bug. @@ -86,18 +87,22 @@ pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { // Possible, in which case we redo the loop and encode nothing. error.Utf8CannotEncodeSurrogateHalf => continue, }; + try writer.writeAll(buf[0..l]); + rem -= l; } - - return result; } test "utf8" { const testing = std.testing; var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var v: Utf8 = .{ .rand = prng.random() }; + v.min_len = buf.len; + v.max_len = buf.len; const gen = v.generator(); - const result = try gen.next(&buf); - try testing.expect(result.len > 0); + try gen.next(&writer, buf.len); + const result = writer.buffered(); + try testing.expectEqual(256, result.len); try testing.expect(std.unicode.utf8ValidateSlice(result)); } diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig index b32469aab..d9b6a659d 100644 --- a/src/synthetic/cli.zig +++ b/src/synthetic/cli.zig @@ -100,7 +100,9 @@ fn mainActionImpl( try impl.run(writer, rand); // Always flush - try writer.flush(); + writer.flush() catch |err| switch (err) { + error.WriteFailed => return, + }; } test { diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index 339bdee2e..b2d57fa88 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -31,10 +31,8 @@ pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { .alphabet = synthetic.Bytes.Alphabet.ascii, }; - var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { + gen.next(writer, 1024) catch |err| { const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig index 23d19e4ae..8250b81de 100644 --- a/src/synthetic/cli/Osc.zig +++ b/src/synthetic/cli/Osc.zig @@ -37,14 +37,11 @@ pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void { var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { - const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); - switch (@as(Error, err)) { - error.BrokenPipe => return, // stdout closed - error.WriteFailed => return, // fixed buffer full - else => return err, - } + var fixed: std.Io.Writer = .fixed(&buf); + try gen.next(&fixed, buf.len); + const data = fixed.buffered(); + writer.writeAll(data) catch |err| switch (err) { + error.WriteFailed => return, }; } } diff --git a/src/synthetic/cli/Utf8.zig b/src/synthetic/cli/Utf8.zig index 3c2fddef7..635704755 100644 --- a/src/synthetic/cli/Utf8.zig +++ b/src/synthetic/cli/Utf8.zig @@ -30,10 +30,8 @@ pub fn run(self: *Utf8, writer: *std.Io.Writer, rand: std.Random) !void { .rand = rand, }; - var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { + gen.next(writer, 1024) catch |err| { const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed From cbeb6890c9ec4962ec7f5eae9506982d8eac994a Mon Sep 17 00:00:00 2001 From: Joshie <74162303+CoderJoshDK@users.noreply.github.com> Date: Sun, 12 Oct 2025 16:48:06 -0400 Subject: [PATCH 259/319] Add `.ghostty` extension to `config` (#8885) Resolves #8689 For various reason, ghostty wants to have a unique file extension for the config files. The name was settled on `config.ghostty`. This will help with tooling. See #8438 (original discussion) for more details. This PR introduces the preferred default of `.ghostty` while still supporting the previous `config` file. If both files exist, a warning log is sent. The docs / website will need to be updated to reflect this change. > [!NOTE] > Only tested on macOS 26.0. --------- Co-authored-by: Mitchell Hashimoto --- .../Features/Settings/SettingsView.swift | 2 +- src/build/GhosttyResources.zig | 2 +- src/build/mdgen/ghostty_1_footer.md | 6 +- src/build/mdgen/ghostty_5_footer.md | 6 +- src/build/mdgen/ghostty_5_header.md | 4 +- src/cli/edit_config.zig | 6 +- src/config.zig | 2 + src/config/Config.zig | 146 +++++---------- src/config/edit.zig | 15 +- src/config/file_load.zig | 166 ++++++++++++++++++ src/extra/vim.zig | 2 +- 11 files changed, 228 insertions(+), 129 deletions(-) create mode 100644 src/config/file_load.zig diff --git a/macos/Sources/Features/Settings/SettingsView.swift b/macos/Sources/Features/Settings/SettingsView.swift index 82d24181a..6b0a2c46c 100644 --- a/macos/Sources/Features/Settings/SettingsView.swift +++ b/macos/Sources/Features/Settings/SettingsView.swift @@ -14,7 +14,7 @@ struct SettingsView: View { VStack(alignment: .leading) { Text("Coming Soon. 🚧").font(.title) Text("You can't configure settings in the GUI yet. To modify settings, " + - "edit the file at $HOME/.config/ghostty/config and restart Ghostty.") + "edit the file at $HOME/.config/ghostty/config.ghostty and restart Ghostty.") .multilineTextAlignment(.leading) .lineLimit(nil) } diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 7880a98a0..b80aef97e 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -225,7 +225,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' // directory. The syntax then needs to be mapped to the correct language in // the config file within the '~.config/bat' directory - // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). + // (ex: --map-syntax "/Users/user/.config/ghostty/config.ghostty:Ghostty Config"). { const run = b.addRunArtifact(build_data_exe); run.addArg("+sublime"); diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index f8e502b45..88aa16273 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -1,15 +1,15 @@ # FILES -_\$XDG_CONFIG_HOME/ghostty/config_ +_\$XDG_CONFIG_HOME/ghostty/config.ghostty_ : Location of the default configuration file. -_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_ +_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_ : **On macOS**, location of the default configuration file. This location takes precedence over the XDG environment locations. -_\$LOCALAPPDATA/ghostty/config_ +_\$LOCALAPPDATA/ghostty/config.ghostty_ : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched for configuration files. diff --git a/src/build/mdgen/ghostty_5_footer.md b/src/build/mdgen/ghostty_5_footer.md index 380d83a53..d2cf024d1 100644 --- a/src/build/mdgen/ghostty_5_footer.md +++ b/src/build/mdgen/ghostty_5_footer.md @@ -1,15 +1,15 @@ # FILES -_\$XDG_CONFIG_HOME/ghostty/config_ +_\$XDG_CONFIG_HOME/ghostty/config.ghostty_ : Location of the default configuration file. -_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_ +_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_ : **On macOS**, location of the default configuration file. This location takes precedence over the XDG environment locations. -_\$LOCALAPPDATA/ghostty/config_ +_\$LOCALAPPDATA/ghostty/config.ghostty_ : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched for configuration files. diff --git a/src/build/mdgen/ghostty_5_header.md b/src/build/mdgen/ghostty_5_header.md index 078133861..b9d4cb751 100644 --- a/src/build/mdgen/ghostty_5_header.md +++ b/src/build/mdgen/ghostty_5_header.md @@ -8,11 +8,11 @@ To configure Ghostty, you must use a configuration file. GUI-based configuration is on the roadmap but not yet supported. The configuration file must be placed -at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config` +at `$XDG_CONFIG_HOME/ghostty/config.ghostty`, which defaults to `~/.config/ghostty/config.ghostty` if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). **If you are using macOS, the configuration file can also be placed at -`$HOME/Library/Application Support/com.mitchellh.ghostty/config`.** This is the +`$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.** This is the default configuration location for macOS. It will be searched before any of the XDG environment locations listed above. diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index f103ca4a0..37f961a44 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -30,9 +30,9 @@ pub const Options = struct { /// this yet. /// /// The filepath opened is the default user-specific configuration -/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`. +/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. /// On macOS, this may also be located at -/// `~/Library/Application Support/com.mitchellh.ghostty/config`. +/// `~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`. /// On macOS, whichever path exists and is non-empty will be prioritized, /// prioritizing the Application Support directory if neither are /// non-empty. @@ -73,7 +73,7 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { defer config.deinit(); // Find the preferred path. - const path = try Config.preferredDefaultFilePath(alloc); + const path = try configpkg.preferredDefaultFilePath(alloc); defer alloc.free(path); // We don't currently support Windows because we use the exec syscall. diff --git a/src/config.zig b/src/config.zig index a596eb5e6..4abd319a6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); +const file_load = @import("config/file_load.zig"); const formatter = @import("config/formatter.zig"); pub const Config = @import("config/Config.zig"); pub const conditional = @import("config/conditional.zig"); @@ -12,6 +13,7 @@ pub const ConditionalState = conditional.State; pub const FileFormatter = formatter.FileFormatter; pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; +pub const preferredDefaultFilePath = file_load.preferredDefaultFilePath; // Field types pub const BoldColor = Config.BoldColor; diff --git a/src/config/Config.zig b/src/config/Config.zig index 7d8c7d9ff..aabefcd8f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -24,6 +24,7 @@ const cli = @import("../cli.zig"); const conditional = @import("conditional.zig"); const Conditional = conditional.Conditional; +const file_load = @import("file_load.zig"); const formatterpkg = @import("formatter.zig"); const themepkg = @import("theme.zig"); const url = @import("url.zig"); @@ -2071,7 +2072,7 @@ keybind: Keybinds = .{}, /// When this is true, the default configuration file paths will be loaded. /// The default configuration file paths are currently only the XDG -/// config path ($XDG_CONFIG_HOME/ghostty/config). +/// config path ($XDG_CONFIG_HOME/ghostty/config.ghostty). /// /// If this is false, the default configuration paths will not be loaded. /// This is targeted directly at using Ghostty from the CLI in a way @@ -3397,7 +3398,7 @@ pub fn loadIter( /// `path` must be resolved and absolute. pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { assert(std.fs.path.isAbsolute(path)); - var file = openFile(path) catch |err| switch (err) { + var file = file_load.open(path) catch |err| switch (err) { error.NotAFile => { log.warn( "config-file {s}: not reading because it is not a file", @@ -3461,31 +3462,60 @@ fn writeConfigTemplate(path: []const u8) !void { } /// Load configurations from the default configuration files. The default -/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. +/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. /// -/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` +/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/` /// is also loaded. +/// +/// The legacy `config` file (without extension) is first loaded, +/// then `config.ghostty`. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { // Load XDG first - const xdg_path = try defaultXdgPath(alloc); + const legacy_xdg_path = try file_load.legacyDefaultXdgPath(alloc); + defer alloc.free(legacy_xdg_path); + const xdg_path = try file_load.defaultXdgPath(alloc); defer alloc.free(xdg_path); - const xdg_action = self.loadOptionalFile(alloc, xdg_path); + const xdg_loaded: bool = xdg_loaded: { + const legacy_xdg_action = self.loadOptionalFile(alloc, legacy_xdg_path); + const xdg_action = self.loadOptionalFile(alloc, xdg_path); + if (xdg_action != .not_found and legacy_xdg_action != .not_found) { + log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_xdg_path, xdg_path }); + log.warn("loading them both in that order", .{}); + break :xdg_loaded true; + } + + break :xdg_loaded xdg_action != .not_found or + legacy_xdg_action != .not_found; + }; // On macOS load the app support directory as well if (comptime builtin.os.tag == .macos) { - const app_support_path = try defaultAppSupportPath(alloc); + const legacy_app_support_path = try file_load.legacyDefaultAppSupportPath(alloc); + defer alloc.free(legacy_app_support_path); + const app_support_path = try file_load.preferredAppSupportPath(alloc); defer alloc.free(app_support_path); - const app_support_action = self.loadOptionalFile(alloc, app_support_path); + const app_support_loaded: bool = loaded: { + const legacy_app_support_action = self.loadOptionalFile(alloc, legacy_app_support_path); + const app_support_action = self.loadOptionalFile(alloc, app_support_path); + if (app_support_action != .not_found and legacy_app_support_action != .not_found) { + log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_app_support_path, app_support_path }); + log.warn("loading them both in that order", .{}); + break :loaded true; + } + + break :loaded app_support_action != .not_found or + legacy_app_support_action != .not_found; + }; // If both files are not found, then we create a template file. // For macOS, we only create the template file in the app support - if (app_support_action == .not_found and xdg_action == .not_found) { + if (app_support_loaded and xdg_loaded) { writeConfigTemplate(app_support_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; } } else { - if (xdg_action == .not_found) { + if (xdg_loaded) { writeConfigTemplate(xdg_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; @@ -3493,102 +3523,6 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { } } -/// Default path for the XDG home configuration file. Returned value -/// must be freed by the caller. -fn defaultXdgPath(alloc: Allocator) ![]const u8 { - return try internal_os.xdg.config( - alloc, - .{ .subdir = "ghostty/config" }, - ); -} - -/// Default path for the macOS Application Support configuration file. -/// Returned value must be freed by the caller. -fn defaultAppSupportPath(alloc: Allocator) ![]const u8 { - return try internal_os.macos.appSupportDir(alloc, "config"); -} - -/// Returns the path to the preferred default configuration file. -/// This is the file where users should place their configuration. -/// -/// This doesn't create or populate the file with any default -/// contents; downstream callers must handle this. -/// -/// The returned value must be freed by the caller. -pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 { - switch (builtin.os.tag) { - .macos => { - // macOS prefers the Application Support directory - // if it exists. - const app_support_path = try defaultAppSupportPath(alloc); - if (openFile(app_support_path)) |f| { - f.close(); - return app_support_path; - } else |_| {} - - // Try the XDG path if it exists - const xdg_path = try defaultXdgPath(alloc); - if (openFile(xdg_path)) |f| { - f.close(); - alloc.free(app_support_path); - return xdg_path; - } else |_| {} - defer alloc.free(xdg_path); - - // Neither exist, use app support - return app_support_path; - }, - - // All other platforms use XDG only - else => return try defaultXdgPath(alloc), - } -} - -const OpenFileError = error{ - FileNotFound, - FileIsEmpty, - FileOpenFailed, - NotAFile, -}; - -/// Opens the file at the given path and returns the file handle -/// if it exists and is non-empty. This also constrains the possible -/// errors to a smaller set that we can explicitly handle. -fn openFile(path: []const u8) OpenFileError!std.fs.File { - assert(std.fs.path.isAbsolute(path)); - - var file = std.fs.openFileAbsolute( - path, - .{}, - ) catch |err| switch (err) { - error.FileNotFound => return OpenFileError.FileNotFound, - else => { - log.warn("unexpected file open error path={s} err={}", .{ - path, - err, - }); - return OpenFileError.FileOpenFailed; - }, - }; - errdefer file.close(); - - const stat = file.stat() catch |err| { - log.warn("error getting file stat path={s} err={}", .{ - path, - err, - }); - return OpenFileError.FileOpenFailed; - }; - switch (stat.kind) { - .file => {}, - else => return OpenFileError.NotAFile, - } - - if (stat.size == 0) return OpenFileError.FileIsEmpty; - - return file; -} - /// Load and parse the CLI args. pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { diff --git a/src/config/edit.zig b/src/config/edit.zig index 07bb7ee5a..6087106e7 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -4,6 +4,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const internal_os = @import("../os/main.zig"); +const file_load = @import("file_load.zig"); /// The path to the configuration that should be opened for editing. /// @@ -89,20 +90,16 @@ fn configPath(alloc_arena: Allocator) ![]const u8 { /// Returns a const list of possible paths the main config file could be /// in for the current OS. fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 { - var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 2); + var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 4); errdefer paths.deinit(alloc_arena); if (comptime builtin.os.tag == .macos) { - paths.appendAssumeCapacity(try internal_os.macos.appSupportDir( - alloc_arena, - "config", - )); + paths.appendAssumeCapacity(try file_load.defaultAppSupportPath(alloc_arena)); + paths.appendAssumeCapacity(try file_load.legacyDefaultAppSupportPath(alloc_arena)); } - paths.appendAssumeCapacity(try internal_os.xdg.config( - alloc_arena, - .{ .subdir = "ghostty/config" }, - )); + paths.appendAssumeCapacity(try file_load.defaultXdgPath(alloc_arena)); + paths.appendAssumeCapacity(try file_load.legacyDefaultXdgPath(alloc_arena)); return paths.items; } diff --git a/src/config/file_load.zig b/src/config/file_load.zig new file mode 100644 index 000000000..8dbefeea8 --- /dev/null +++ b/src/config/file_load.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); + +const log = std.log.scoped(.config); + +/// Default path for the XDG home configuration file. Returned value +/// must be freed by the caller. +pub fn defaultXdgPath(alloc: Allocator) ![]const u8 { + return try internal_os.xdg.config( + alloc, + .{ .subdir = "ghostty/config.ghostty" }, + ); +} + +/// Ghostty <1.3.0 default path for the XDG home configuration file. +/// Returned value must be freed by the caller. +pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 { + return try internal_os.xdg.config( + alloc, + .{ .subdir = "ghostty/config" }, + ); +} + +/// Preferred default path for the XDG home configuration file. +/// Returned value must be freed by the caller. +pub fn preferredXdgPath(alloc: Allocator) ![]const u8 { + // If the XDG path exists, use that. + const xdg_path = try defaultXdgPath(alloc); + if (open(xdg_path)) |f| { + f.close(); + return xdg_path; + } else |_| {} + + // Try the legacy path + errdefer alloc.free(xdg_path); + const legacy_xdg_path = try legacyDefaultXdgPath(alloc); + if (open(legacy_xdg_path)) |f| { + f.close(); + alloc.free(xdg_path); + return legacy_xdg_path; + } else |_| {} + + // Legacy path and XDG path both don't exist. Return the + // new one. + alloc.free(legacy_xdg_path); + return xdg_path; +} + +/// Default path for the macOS Application Support configuration file. +/// Returned value must be freed by the caller. +pub fn defaultAppSupportPath(alloc: Allocator) ![]const u8 { + return try internal_os.macos.appSupportDir(alloc, "config.ghostty"); +} + +/// Ghostty <1.3.0 default path for the macOS Application Support +/// configuration file. Returned value must be freed by the caller. +pub fn legacyDefaultAppSupportPath(alloc: Allocator) ![]const u8 { + return try internal_os.macos.appSupportDir(alloc, "config"); +} + +/// Preferred default path for the macOS Application Support configuration file. +/// Returned value must be freed by the caller. +pub fn preferredAppSupportPath(alloc: Allocator) ![]const u8 { + // If the app support path exists, use that. + const app_support_path = try defaultAppSupportPath(alloc); + if (open(app_support_path)) |f| { + f.close(); + return app_support_path; + } else |_| {} + + // Try the legacy path + errdefer alloc.free(app_support_path); + const legacy_app_support_path = try legacyDefaultAppSupportPath(alloc); + if (open(legacy_app_support_path)) |f| { + f.close(); + alloc.free(app_support_path); + return legacy_app_support_path; + } else |_| {} + + // Legacy path and app support path both don't exist. Return the + // new one. + alloc.free(legacy_app_support_path); + return app_support_path; +} + +/// Returns the path to the preferred default configuration file. +/// This is the file where users should place their configuration. +/// +/// This doesn't create or populate the file with any default +/// contents; downstream callers must handle this. +/// +/// The returned value must be freed by the caller. +pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 { + switch (builtin.os.tag) { + .macos => { + // macOS prefers the Application Support directory + // if it exists. + const app_support_path = try preferredAppSupportPath(alloc); + const app_support_file = open(app_support_path) catch { + // Try the XDG path if it exists + const xdg_path = try preferredXdgPath(alloc); + const xdg_file = open(xdg_path) catch { + // If neither file exists, use app support + alloc.free(xdg_path); + return app_support_path; + }; + xdg_file.close(); + alloc.free(app_support_path); + return xdg_path; + }; + app_support_file.close(); + return app_support_path; + }, + + // All other platforms use XDG only + else => return try preferredXdgPath(alloc), + } +} + +const OpenFileError = error{ + FileNotFound, + FileIsEmpty, + FileOpenFailed, + NotAFile, +}; + +/// Opens the file at the given path and returns the file handle +/// if it exists and is non-empty. This also constrains the possible +/// errors to a smaller set that we can explicitly handle. +pub fn open(path: []const u8) OpenFileError!std.fs.File { + assert(std.fs.path.isAbsolute(path)); + + var file = std.fs.openFileAbsolute( + path, + .{}, + ) catch |err| switch (err) { + error.FileNotFound => return OpenFileError.FileNotFound, + else => { + log.warn("unexpected file open error path={s} err={}", .{ + path, + err, + }); + return OpenFileError.FileOpenFailed; + }, + }; + errdefer file.close(); + + const stat = file.stat() catch |err| { + log.warn("error getting file stat path={s} err={}", .{ + path, + err, + }); + return OpenFileError.FileOpenFailed; + }; + switch (stat.kind) { + .file => {}, + else => return OpenFileError.NotAFile, + } + + if (stat.size == 0) return OpenFileError.FileIsEmpty; + + return file; +} diff --git a/src/extra/vim.zig b/src/extra/vim.zig index 2c0192d03..9140b83f8 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -10,7 +10,7 @@ pub const ftdetect = \\" \\" THIS FILE IS AUTO-GENERATED \\ - \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* setf ghostty + \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty \\ ; pub const ftplugin = From 8f1a014afd9c6724b767b2fbf65d27d26b3c01e5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Oct 2025 15:20:26 -0700 Subject: [PATCH 260/319] macos: clean up the "installing" update state (#9170) This includes multiple changes to clean up the "installing" state: - Ghostty will not confirm quit, since the user has already confirmed they want to restart to install the update. - If termination fails for any reason, the popover has a button to retry restarting. - The copy and badge symbol have been updated to better match the reality of the "installing" state. CleanShot 2025-10-12 at 15 04 08@2x AI written: https://ampcode.com/threads/T-623d1030-419f-413f-a285-e79c86a4246b fully understood. --- macos/Sources/App/macOS/AppDelegate.swift | 6 ++++ .../Sources/Features/Update/UpdateBadge.swift | 2 +- .../Features/Update/UpdateController.swift | 5 +++ .../Features/Update/UpdateDriver.swift | 2 +- .../Features/Update/UpdatePopoverView.swift | 33 +++++++++++++------ .../Features/Update/UpdateSimulator.swift | 18 ++++++++-- .../Features/Update/UpdateViewModel.swift | 10 ++++-- macos/Tests/Update/UpdateStateTests.swift | 4 +-- macos/Tests/Update/UpdateViewModelTests.swift | 4 +-- 9 files changed, 62 insertions(+), 22 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 3319189b9..9a6eab47b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -322,6 +322,12 @@ class AppDelegate: NSObject, func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let windows = NSApplication.shared.windows if (windows.isEmpty) { return .terminateNow } + + // If we've already accepted to install an update, then we don't need to + // confirm quit. The user is already expecting the update to happen. + if updateController.isInstalling { + return .terminateNow + } // This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't // quite work with SwiftUI because windows are retained on close. So instead we check diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index a4a95f411..054fdf971 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -32,7 +32,7 @@ struct UpdateBadge: View { case .extracting(let extracting): ProgressRingView(progress: min(1, max(0, extracting.progress))) - case .checking, .installing: + case .checking: if let iconName = model.iconName { Image(systemName: iconName) .rotationEffect(.degrees(rotationAngle)) diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index aa875567c..2dfb0a420 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -17,6 +17,11 @@ class UpdateController { userDriver.viewModel } + /// True if we're installing an update. + var isInstalling: Bool { + installCancellable != nil + } + /// Initialize a new update controller. init() { let hostBundle = Bundle.main diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index ed58f1663..4bddda809 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -172,7 +172,7 @@ class UpdateDriver: NSObject, SPUUserDriver { } func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { - viewModel.state = .installing + viewModel.state = .installing(.init(retryTerminatingApplication: retryTerminatingApplication)) if !hasUnobtrusiveTarget { standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication) diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 236649c21..770b9aedd 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -38,8 +38,8 @@ struct UpdatePopoverView: View { case .readyToInstall(let ready): ReadyToInstallView(ready: ready, dismiss: dismiss) - case .installing: - InstallingView() + case .installing(let installing): + InstallingView(installing: installing, dismiss: dismiss) case .notFound(let notFound): NotFoundView(notFound: notFound, dismiss: dismiss) @@ -313,18 +313,31 @@ fileprivate struct ReadyToInstallView: View { } fileprivate struct InstallingView: View { + let installing: UpdateState.Installing + let dismiss: DismissAction + var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 10) { - ProgressView() - .controlSize(.small) - Text("Installing…") + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Restart Required") .font(.system(size: 13, weight: .semibold)) + + Text("The update is ready. Please restart the application to complete the installation.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) } - Text("The application will relaunch shortly.") - .font(.system(size: 11)) - .foregroundColor(.secondary) + HStack { + Spacer() + Button("Restart Now") { + installing.retryTerminatingApplication() + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } } .padding(16) } diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift index f40bbee1b..c855282c0 100644 --- a/macos/Sources/Features/Update/UpdateSimulator.swift +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -28,6 +28,9 @@ enum UpdateSimulator { /// User cancels while checking: checking (1s) → cancels → idle case cancelDuringChecking + /// Shows the installing state with restart button: installing (stays until dismissed) + case installing + func simulate(with viewModel: UpdateViewModel) { switch self { case .happyPath: @@ -44,6 +47,8 @@ enum UpdateSimulator { simulateCancelDuringDownload(viewModel) case .cancelDuringChecking: simulateCancelDuringChecking(viewModel) + case .installing: + simulateInstalling(viewModel) } } @@ -260,10 +265,10 @@ enum UpdateSimulator { viewModel.state = .readyToInstall(.init( reply: { choice in if choice == .install { - viewModel.state = .installing - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .installing(.init(retryTerminatingApplication: { + print("Restart button clicked in simulator - resetting to idle") viewModel.state = .idle - } + })) } else { viewModel.state = .idle } @@ -274,4 +279,11 @@ enum UpdateSimulator { } } } + + private func simulateInstalling(_ viewModel: UpdateViewModel) { + viewModel.state = .installing(.init(retryTerminatingApplication: { + print("Restart button clicked in simulator - resetting to idle") + viewModel.state = .idle + })) + } } diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index ccb03e731..f0b39ed2d 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -33,7 +33,7 @@ class UpdateViewModel: ObservableObject { case .readyToInstall: return "Install Update" case .installing: - return "Installing…" + return "Restart to Complete Update" case .notFound: return "No Updates Available" case .error(let err): @@ -72,7 +72,7 @@ class UpdateViewModel: ObservableObject { case .readyToInstall: return "checkmark.circle.fill" case .installing: - return "gear" + return "power.circle" case .notFound: return "info.circle" case .error: @@ -192,7 +192,7 @@ enum UpdateState: Equatable { case downloading(Downloading) case extracting(Extracting) case readyToInstall(ReadyToInstall) - case installing + case installing(Installing) var isIdle: Bool { if case .idle = self { return true } @@ -382,4 +382,8 @@ enum UpdateState: Equatable { struct ReadyToInstall { let reply: @Sendable (SPUUserUpdateChoice) -> Void } + + struct Installing { + let retryTerminatingApplication: () -> Void + } } diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift index 01819bb25..269cd3153 100644 --- a/macos/Tests/Update/UpdateStateTests.swift +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -25,8 +25,8 @@ struct UpdateStateTests { } @Test func testInstallingEquality() { - let state1: UpdateState = .installing - let state2: UpdateState = .installing + let state1: UpdateState = .installing(.init(retryTerminatingApplication: {})) + let state2: UpdateState = .installing(.init(retryTerminatingApplication: {})) #expect(state1 == state2) } diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift index d3b2e060b..56748ff83 100644 --- a/macos/Tests/Update/UpdateViewModelTests.swift +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -58,8 +58,8 @@ struct UpdateViewModelTests { @Test func testInstallingText() { let viewModel = UpdateViewModel() - viewModel.state = .installing - #expect(viewModel.text == "Installing…") + viewModel.state = .installing(.init(retryTerminatingApplication: {})) + #expect(viewModel.text == "Restart to Complete Update") } @Test func testNotFoundText() { From 97a5a59cc3edf86cb939ca563b3bb43afe43a79a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 12 Oct 2025 15:30:34 -0700 Subject: [PATCH 261/319] macos: update to Sparkle 2.8 (#9171) Most of the changes seem to be Tahoe UI related and now that we have a custom UI I don't think there is anything important here but we should update nonetheless. --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index db3dd11a5..89573fb88 100644 --- a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d", - "version" : "2.7.3" + "revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb", + "version" : "2.8.0" } } ], From 797c54a2d72c7f580cae894b19fdf9f8b4c69fcb Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 13 Oct 2025 08:45:21 -0500 Subject: [PATCH 262/319] deps: update libvaxis (#9177) Update libvaxis. The latest commit of libvaxis includes `uucode` as the unicode library. `uucode` has a much cleaner API and is actively developed by a Ghostty maintainer (@jacobsandlund). This also has the advantage of removing the last transitive dependency Ghostty has that is hosted on codeberg, which separates us from the frequent outages. Disclosures: I used AI to debug the import issue in `boo.zig` --------- Co-authored-by: Mitchell Hashimoto --- build.zig.zon | 4 ++-- build.zig.zon.json | 11 ++++++++--- build.zig.zon.nix | 14 +++++++++++--- build.zig.zon.txt | 3 ++- flatpak/zig-packages.json | 12 +++++++++--- src/cli/boo.zig | 2 +- src/cli/list_themes.zig | 2 +- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 45f1e3692..8c05cb50a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,8 +15,8 @@ }, .vaxis = .{ // rockorager/libvaxis - .url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", - .hash = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", + .url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + .hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", .lazy = true, }, .z2d = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index e9f2b8cd8..a94c3b13a 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -109,6 +109,11 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, + "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM": { + "name": "uucode", + "url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732", + "hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=" + }, "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": { "name": "uucode", "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", @@ -119,10 +124,10 @@ "url": "git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a", "hash": "sha256-5c+TjmiH4071lKI+U8SIJ0M+4ezzcAtLI2ZSfZYdXSA=" }, - "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": { + "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": { "name": "vaxis", - "url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", - "hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=" + "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + "hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY=" }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "name": "wayland", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 08e524e91..ec69854fe 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -258,6 +258,14 @@ in hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="; }; } + { + name = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM"; + path = fetchZigArtifact { + name = "uucode"; + url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732"; + hash = "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="; + }; + } { name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT"; path = fetchZigArtifact { @@ -275,11 +283,11 @@ in }; } { - name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ"; + name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS"; path = fetchZigArtifact { name = "vaxis"; - url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz"; - hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="; + url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz"; + hash = "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index d7b76e59f..ef854348a 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,3 +1,4 @@ +git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732 git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz @@ -22,7 +23,6 @@ https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3f https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz -https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz @@ -33,4 +33,5 @@ https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90 https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz +https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index df210bf22..ef9cb7624 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -131,6 +131,12 @@ "dest": "vendor/p/N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH", "sha256": "ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf" }, + { + "type": "git", + "url": "https://github.com/jacobsandlund/uucode", + "commit": "5f05f8f83a75caea201f12cc8ea32a2d82ea9732", + "dest": "vendor/p/uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", @@ -145,9 +151,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", - "dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", - "sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2" + "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + "dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", + "sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6" }, { "type": "archive", diff --git a/src/cli/boo.zig b/src/cli/boo.zig index 756b6d77a..f96fd6282 100644 --- a/src/cli/boo.zig +++ b/src/cli/boo.zig @@ -6,7 +6,7 @@ const Allocator = std.mem.Allocator; const help_strings = @import("help_strings"); const vaxis = @import("vaxis"); -const framedata = @embedFile("framedata"); +const framedata = @import("framedata").compressed; const vxfw = vaxis.vxfw; diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index cc6cfaf3e..63184ddfb 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -241,7 +241,7 @@ const Preview = struct { .hex = false, .mode = .normal, .color_scheme = .light, - .text_input = .init(allocator, &self.vx.unicode), + .text_input = .init(allocator), .theme_filter = theme_filter, }; From e805a987223183896d186c8597c9434c42baa83d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 06:45:46 -0700 Subject: [PATCH 263/319] build(deps): bump cachix/install-nix-action from 31.7.0 to 31.8.0 (#9175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.7.0 to 31.8.0.
Release notes

Sourced from cachix/install-nix-action's releases.

v31.8.0

What's Changed

Full Changelog: https://github.com/cachix/install-nix-action/compare/v31.7.0...v31.8.0

Commits
  • 7ab6e7f Merge pull request #257 from cachix/create-pull-request/patch
  • a851831 nix: 2.31.2 -> 2.32.0
  • 0b2de19 docs: update the ci badge
  • b8a94d3 ci: pass correct args to the act test
  • 0ef0505 ci: adjust oldest supported version for macos-15
  • 0b43574 ci: add macos-15-intel runner
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cachix/install-nix-action&package-manager=github_actions&previous-version=31.7.0&new-version=31.8.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 4 +- .github/workflows/test.yml | 48 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index ef6f96555..b28dd4299 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index af912215c..5718a85bb 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -89,7 +89,7 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index d3ea88def..93af7fe26 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -34,7 +34,7 @@ jobs: with: # Important so that build number generation works fetch-depth: 0 - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -168,7 +168,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6df4975b0..b71c8895b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -113,7 +113,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -146,7 +146,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -180,7 +180,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -222,7 +222,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -258,7 +258,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -287,7 +287,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -320,7 +320,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -366,7 +366,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -585,7 +585,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -627,7 +627,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -675,7 +675,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -710,7 +710,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -774,7 +774,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -801,7 +801,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -829,7 +829,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -856,7 +856,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -883,7 +883,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -910,7 +910,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -937,7 +937,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -971,7 +971,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -998,7 +998,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1035,7 +1035,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1123,7 +1123,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 4e9db4225..3d6c2ed1f 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -29,7 +29,7 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 From 1835a86a6aa94e1fbf1139773f78e8d9c44b2210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 06:45:55 -0700 Subject: [PATCH 264/319] build(deps): bump softprops/action-gh-release from 2.4.0 to 2.4.1 (#9174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.0 to 2.4.1.
Release notes

Sourced from softprops/action-gh-release's releases.

v2.4.1

What's Changed

Other Changes 🔄

Full Changelog: https://github.com/softprops/action-gh-release/compare/v2...v2.4.1

Changelog

Sourced from softprops/action-gh-release's changelog.

2.4.1

What's Changed

Other Changes 🔄

2.4.0

What's Changed

Exciting New Features 🎉

2.3.4

What's Changed

Bug fixes 🐛

Other Changes 🔄

  • dependency updates

2.3.3

What's Changed

Exciting New Features 🎉

Other Changes 🔄

  • dependency updates

2.3.2

  • fix: revert fs readableWebStream change

2.3.1

Bug fixes 🐛

... (truncated)

Commits
  • 6da8fa9 release 2.4.1
  • f38efde fix: gracefully fallback to body when body_path cannot be read (#671)
  • cec1a11 fix(util): support brace expansion globs containing commas in parseInputFiles...
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=2.4.0&new-version=2.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 93af7fe26..b4ef9e1f5 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -188,7 +188,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -359,7 +359,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -590,7 +590,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -775,7 +775,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From f5af3d4585e5e6c0045aa21983923f175d5656ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 06:46:03 -0700 Subject: [PATCH 265/319] build(deps): bump namespacelabs/nscloud-setup-buildx-action from 0.0.18 to 0.0.19 (#9173) Bumps [namespacelabs/nscloud-setup-buildx-action](https://github.com/namespacelabs/nscloud-setup-buildx-action) from 0.0.18 to 0.0.19.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=namespacelabs/nscloud-setup-buildx-action&package-manager=github_actions&previous-version=0.0.18&new-version=0.0.19)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b71c8895b..6757db2ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1057,7 +1057,7 @@ jobs: uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18 + uses: namespacelabs/nscloud-setup-buildx-action@7020d7d8e659afecbfec162ab4693c7e56278311 # v0.0.19 - name: Download Source Tarball Artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 From dafb9e89a3470674e0bfd5d9b01edbf1d87f330a Mon Sep 17 00:00:00 2001 From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:52:00 +0200 Subject: [PATCH 266/319] macOS: use default app for `*.ghostty` files first (#9180) A small improvement for #8885, tested `config` and `config.ghostty` image --- macos/Sources/Ghostty/Ghostty.App.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index bdc64e9e1..bf34b4a91 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -637,8 +637,9 @@ extension Ghostty { switch action.kind { case .text: - // Open with the default text editor - if let textEditor = NSWorkspace.shared.defaultTextEditor { + // Open with the default editor for `*.ghostty` file or just system text editor + let editor = NSWorkspace.shared.defaultApplicationURL(forExtension: url.pathExtension) ?? NSWorkspace.shared.defaultTextEditor + if let textEditor = editor { NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) return true } From 5f287774a6a05028c926c340896f7e8c52611947 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 13 Oct 2025 15:47:58 -0500 Subject: [PATCH 267/319] osc: simplify parser init (#9184) --- src/synthetic/Osc.zig | 4 +- src/terminal/Parser.zig | 2 +- src/terminal/c/osc.zig | 2 +- src/terminal/osc.zig | 224 +++++++++++++++++++--------------------- 4 files changed, 113 insertions(+), 119 deletions(-) diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig index d78b95a1e..52940fee9 100644 --- a/src/synthetic/Osc.zig +++ b/src/synthetic/Osc.zig @@ -204,7 +204,7 @@ test "OSC generator valid" { var writer: std.Io.Writer = .fixed(&buf); try gen.next(&writer, buf.len); const seq = writer.buffered(); - var parser: terminal.osc.Parser = .init(); + var parser: terminal.osc.Parser = .init(null); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) != null); } @@ -224,7 +224,7 @@ test "OSC generator invalid" { var writer: std.Io.Writer = .fixed(&buf); try gen.next(&writer, buf.len); const seq = writer.buffered(); - var parser: terminal.osc.Parser = .init(); + var parser: terminal.osc.Parser = .init(null); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) == null); } diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index ca2fd3718..625591d3f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -221,7 +221,7 @@ pub fn init() Parser { .params_idx = 0, .param_acc = 0, .param_acc_idx = 0, - .osc_parser = .init(), + .osc_parser = .init(null), .intermediates = undefined, .params = undefined, diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig index 8b6a8409c..1311eaff8 100644 --- a/src/terminal/c/osc.zig +++ b/src/terminal/c/osc.zig @@ -19,7 +19,7 @@ pub fn new( const alloc = lib_alloc.default(alloc_); const ptr = alloc.create(osc.Parser) catch return .out_of_memory; - ptr.* = .initAlloc(alloc); + ptr.* = .init(alloc); result.* = ptr; return .success; } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 12a6d1f5c..f7324636a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -444,9 +444,9 @@ pub const Parser = struct { conemu_guimacro, }; - pub fn init() Parser { + pub fn init(alloc: ?Allocator) Parser { var result: Parser = .{ - .alloc = null, + .alloc = alloc, .state = .empty, .command = .invalid, .buf_start = 0, @@ -469,12 +469,6 @@ pub const Parser = struct { return result; } - pub fn initAlloc(alloc: Allocator) Parser { - var result: Parser = .init(); - result.alloc = alloc; - return result; - } - /// This must be called to clean up any allocated memory. pub fn deinit(self: *Parser) void { self.reset(); @@ -1763,7 +1757,7 @@ test { test "OSC 0: change_window_title" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('0'); p.next(';'); p.next('a'); @@ -1776,7 +1770,7 @@ test "OSC 0: change_window_title" { test "OSC 0: longer than buffer" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); for (input) |ch| p.next(ch); @@ -1788,7 +1782,7 @@ test "OSC 0: longer than buffer" { test "OSC 0: one shorter than buffer length" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const prefix = "0;"; const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); @@ -1803,7 +1797,7 @@ test "OSC 0: one shorter than buffer length" { test "OSC 0: exactly at buffer length" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const prefix = "0;"; const title = "a" ** (Parser.MAX_BUF - prefix.len); @@ -1818,7 +1812,7 @@ test "OSC 0: exactly at buffer length" { test "OSC 1: change_window_icon" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('1'); p.next(';'); p.next('a'); @@ -1831,7 +1825,7 @@ test "OSC 1: change_window_icon" { test "OSC 2: change_window_title with 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('2'); p.next(';'); p.next('a'); @@ -1844,7 +1838,7 @@ test "OSC 2: change_window_title with 2" { test "OSC 2: change_window_title with utf8" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('2'); p.next(';'); // '—' EM DASH U+2014 (E2 80 94) @@ -1866,7 +1860,7 @@ test "OSC 2: change_window_title with utf8" { test "OSC 2: change_window_title empty" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('2'); p.next(';'); const cmd = p.end(null).?.*; @@ -1877,7 +1871,7 @@ test "OSC 2: change_window_title empty" { test "OSC 4: empty param" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "4;;"; for (input) |ch| p.next(ch); @@ -1893,7 +1887,7 @@ test "OSC 4: empty param" { test "OSC 7: report pwd" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "7;file:///tmp/example"; for (input) |ch| p.next(ch); @@ -1906,7 +1900,7 @@ test "OSC 7: report pwd" { test "OSC 7: report pwd empty" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "7;"; for (input) |ch| p.next(ch); @@ -1918,7 +1912,7 @@ test "OSC 7: report pwd empty" { test "OSC 8: hyperlink" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;;http://example.com"; for (input) |ch| p.next(ch); @@ -1931,7 +1925,7 @@ test "OSC 8: hyperlink" { test "OSC 8: hyperlink with id set" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id=foo;http://example.com"; for (input) |ch| p.next(ch); @@ -1945,7 +1939,7 @@ test "OSC 8: hyperlink with id set" { test "OSC 8: hyperlink with empty id" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id=;http://example.com"; for (input) |ch| p.next(ch); @@ -1959,7 +1953,7 @@ test "OSC 8: hyperlink with empty id" { test "OSC 8: hyperlink with incomplete key" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id;http://example.com"; for (input) |ch| p.next(ch); @@ -1973,7 +1967,7 @@ test "OSC 8: hyperlink with incomplete key" { test "OSC 8: hyperlink with empty key" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;=value;http://example.com"; for (input) |ch| p.next(ch); @@ -1987,7 +1981,7 @@ test "OSC 8: hyperlink with empty key" { test "OSC 8: hyperlink with empty key and id" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;=value:id=foo;http://example.com"; for (input) |ch| p.next(ch); @@ -2001,7 +1995,7 @@ test "OSC 8: hyperlink with empty key and id" { test "OSC 8: hyperlink with empty uri" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id=foo;"; for (input) |ch| p.next(ch); @@ -2013,7 +2007,7 @@ test "OSC 8: hyperlink with empty uri" { test "OSC 8: hyperlink end" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;;"; for (input) |ch| p.next(ch); @@ -2025,7 +2019,7 @@ test "OSC 8: hyperlink end" { test "OSC 9: show desktop notification" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;Hello world"; for (input) |ch| p.next(ch); @@ -2039,7 +2033,7 @@ test "OSC 9: show desktop notification" { test "OSC 9: show single character desktop notification" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;H"; for (input) |ch| p.next(ch); @@ -2053,7 +2047,7 @@ test "OSC 9: show single character desktop notification" { test "OSC 9;1: ConEmu sleep" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;1;420"; for (input) |ch| p.next(ch); @@ -2067,7 +2061,7 @@ test "OSC 9;1: ConEmu sleep" { test "OSC 9;1: ConEmu sleep with no value default to 100ms" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;1;"; for (input) |ch| p.next(ch); @@ -2081,7 +2075,7 @@ test "OSC 9;1: ConEmu sleep with no value default to 100ms" { test "OSC 9;1: conemu sleep cannot exceed 10000ms" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;1;12345"; for (input) |ch| p.next(ch); @@ -2095,7 +2089,7 @@ test "OSC 9;1: conemu sleep cannot exceed 10000ms" { test "OSC 9;1: conemu sleep invalid input" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;1;foo"; for (input) |ch| p.next(ch); @@ -2109,7 +2103,7 @@ test "OSC 9;1: conemu sleep invalid input" { test "OSC 9;1: conemu sleep -> desktop notification 1" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;1"; for (input) |ch| p.next(ch); @@ -2123,7 +2117,7 @@ test "OSC 9;1: conemu sleep -> desktop notification 1" { test "OSC 9;1: conemu sleep -> desktop notification 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;1a"; for (input) |ch| p.next(ch); @@ -2137,7 +2131,7 @@ test "OSC 9;1: conemu sleep -> desktop notification 2" { test "OSC 9;2: ConEmu message box" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;2;hello world"; for (input) |ch| p.next(ch); @@ -2150,7 +2144,7 @@ test "OSC 9;2: ConEmu message box" { test "OSC 9;2: ConEmu message box invalid input" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;2"; for (input) |ch| p.next(ch); @@ -2163,7 +2157,7 @@ test "OSC 9;2: ConEmu message box invalid input" { test "OSC 9;2: ConEmu message box empty message" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;2;"; for (input) |ch| p.next(ch); @@ -2176,7 +2170,7 @@ test "OSC 9;2: ConEmu message box empty message" { test "OSC 9;2: ConEmu message box spaces only message" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;2; "; for (input) |ch| p.next(ch); @@ -2189,7 +2183,7 @@ test "OSC 9;2: ConEmu message box spaces only message" { test "OSC 9;2: message box -> desktop notification 1" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;2"; for (input) |ch| p.next(ch); @@ -2203,7 +2197,7 @@ test "OSC 9;2: message box -> desktop notification 1" { test "OSC 9;2: message box -> desktop notification 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;2a"; for (input) |ch| p.next(ch); @@ -2217,7 +2211,7 @@ test "OSC 9;2: message box -> desktop notification 2" { test "OSC 9;3: ConEmu change tab title" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;3;foo bar"; for (input) |ch| p.next(ch); @@ -2230,7 +2224,7 @@ test "OSC 9;3: ConEmu change tab title" { test "OSC 9;3: ConEmu change tab title reset" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;3;"; for (input) |ch| p.next(ch); @@ -2244,7 +2238,7 @@ test "OSC 9;3: ConEmu change tab title reset" { test "OSC 9;3: ConEmu change tab title spaces only" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;3; "; for (input) |ch| p.next(ch); @@ -2258,7 +2252,7 @@ test "OSC 9;3: ConEmu change tab title spaces only" { test "OSC 9;3: change tab title -> desktop notification 1" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;3"; for (input) |ch| p.next(ch); @@ -2272,7 +2266,7 @@ test "OSC 9;3: change tab title -> desktop notification 1" { test "OSC 9;3: message box -> desktop notification 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;3a"; for (input) |ch| p.next(ch); @@ -2286,7 +2280,7 @@ test "OSC 9;3: message box -> desktop notification 2" { test "OSC 9;4: ConEmu progress set" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;1;100"; for (input) |ch| p.next(ch); @@ -2300,7 +2294,7 @@ test "OSC 9;4: ConEmu progress set" { test "OSC 9;4: ConEmu progress set overflow" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;1;900"; for (input) |ch| p.next(ch); @@ -2314,7 +2308,7 @@ test "OSC 9;4: ConEmu progress set overflow" { test "OSC 9;4: ConEmu progress set single digit" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;1;9"; for (input) |ch| p.next(ch); @@ -2328,7 +2322,7 @@ test "OSC 9;4: ConEmu progress set single digit" { test "OSC 9;4: ConEmu progress set double digit" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;1;94"; for (input) |ch| p.next(ch); @@ -2342,7 +2336,7 @@ test "OSC 9;4: ConEmu progress set double digit" { test "OSC 9;4: ConEmu progress set extra semicolon ignored" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;1;100"; for (input) |ch| p.next(ch); @@ -2356,7 +2350,7 @@ test "OSC 9;4: ConEmu progress set extra semicolon ignored" { test "OSC 9;4: ConEmu progress remove with no progress" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;0;"; for (input) |ch| p.next(ch); @@ -2370,7 +2364,7 @@ test "OSC 9;4: ConEmu progress remove with no progress" { test "OSC 9;4: ConEmu progress remove with double semicolon" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;0;;"; for (input) |ch| p.next(ch); @@ -2384,7 +2378,7 @@ test "OSC 9;4: ConEmu progress remove with double semicolon" { test "OSC 9;4: ConEmu progress remove ignores progress" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;0;100"; for (input) |ch| p.next(ch); @@ -2398,7 +2392,7 @@ test "OSC 9;4: ConEmu progress remove ignores progress" { test "OSC 9;4: ConEmu progress remove extra semicolon" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;0;100;"; for (input) |ch| p.next(ch); @@ -2411,7 +2405,7 @@ test "OSC 9;4: ConEmu progress remove extra semicolon" { test "OSC 9;4: ConEmu progress error" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;2"; for (input) |ch| p.next(ch); @@ -2425,7 +2419,7 @@ test "OSC 9;4: ConEmu progress error" { test "OSC 9;4: ConEmu progress error with progress" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;2;100"; for (input) |ch| p.next(ch); @@ -2439,7 +2433,7 @@ test "OSC 9;4: ConEmu progress error with progress" { test "OSC 9;4: progress pause" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;4"; for (input) |ch| p.next(ch); @@ -2453,7 +2447,7 @@ test "OSC 9;4: progress pause" { test "OSC 9;4: ConEmu progress pause with progress" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;4;100"; for (input) |ch| p.next(ch); @@ -2467,7 +2461,7 @@ test "OSC 9;4: ConEmu progress pause with progress" { test "OSC 9;4: progress -> desktop notification 1" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4"; for (input) |ch| p.next(ch); @@ -2481,7 +2475,7 @@ test "OSC 9;4: progress -> desktop notification 1" { test "OSC 9;4: progress -> desktop notification 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;"; for (input) |ch| p.next(ch); @@ -2495,7 +2489,7 @@ test "OSC 9;4: progress -> desktop notification 2" { test "OSC 9;4: progress -> desktop notification 3" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;5"; for (input) |ch| p.next(ch); @@ -2509,7 +2503,7 @@ test "OSC 9;4: progress -> desktop notification 3" { test "OSC 9;4: progress -> desktop notification 4" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;4;5a"; for (input) |ch| p.next(ch); @@ -2523,7 +2517,7 @@ test "OSC 9;4: progress -> desktop notification 4" { test "OSC 9;5: ConEmu wait input" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;5"; for (input) |ch| p.next(ch); @@ -2535,7 +2529,7 @@ test "OSC 9;5: ConEmu wait input" { test "OSC 9;5: ConEmu wait ignores trailing characters" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "9;5;foo"; for (input) |ch| p.next(ch); @@ -2547,7 +2541,7 @@ test "OSC 9;5: ConEmu wait ignores trailing characters" { test "OSC 9;6: ConEmu guimacro 1" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "9;6;a"; @@ -2561,7 +2555,7 @@ test "OSC 9;6: ConEmu guimacro 1" { test "OSC: 9;6: ConEmu guimacro 2" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "9;6;ab"; @@ -2575,7 +2569,7 @@ test "OSC: 9;6: ConEmu guimacro 2" { test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "9;6"; @@ -2610,7 +2604,7 @@ test "OSC 21: kitty color protocol" { const testing = std.testing; const Kind = kitty_color.Kind; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; @@ -2681,7 +2675,7 @@ test "OSC 21: kitty color protocol" { test "OSC 21: kitty color protocol without allocator" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); defer p.deinit(); const input = "21;foreground=?"; @@ -2692,7 +2686,7 @@ test "OSC 21: kitty color protocol without allocator" { test "OSC 21: kitty color protocol double reset" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; @@ -2708,7 +2702,7 @@ test "OSC 21: kitty color protocol double reset" { test "OSC 21: kitty color protocol reset after invalid" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; @@ -2729,7 +2723,7 @@ test "OSC 21: kitty color protocol reset after invalid" { test "OSC 21: kitty color protocol no key" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "21;"; @@ -2743,7 +2737,7 @@ test "OSC 21: kitty color protocol no key" { test "OSC 22: pointer cursor" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "22;pointer"; for (input) |ch| p.next(ch); @@ -2756,7 +2750,7 @@ test "OSC 22: pointer cursor" { test "OSC 52: get/set clipboard" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "52;s;?"; for (input) |ch| p.next(ch); @@ -2770,7 +2764,7 @@ test "OSC 52: get/set clipboard" { test "OSC 52: get/set clipboard (optional parameter)" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "52;;?"; for (input) |ch| p.next(ch); @@ -2784,7 +2778,7 @@ test "OSC 52: get/set clipboard (optional parameter)" { test "OSC 52: get/set clipboard with allocator" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "52;s;?"; @@ -2799,7 +2793,7 @@ test "OSC 52: get/set clipboard with allocator" { test "OSC 52: clear clipboard" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); defer p.deinit(); const input = "52;;"; @@ -2838,7 +2832,7 @@ test "OSC 52: clear clipboard" { test "OSC 133: prompt_start" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A"; for (input) |ch| p.next(ch); @@ -2852,7 +2846,7 @@ test "OSC 133: prompt_start" { test "OSC 133: prompt_start with single option" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;aid=14"; for (input) |ch| p.next(ch); @@ -2865,7 +2859,7 @@ test "OSC 133: prompt_start with single option" { test "OSC 133: prompt_start with redraw disabled" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;redraw=0"; for (input) |ch| p.next(ch); @@ -2878,7 +2872,7 @@ test "OSC 133: prompt_start with redraw disabled" { test "OSC 133: prompt_start with redraw invalid value" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;redraw=42"; for (input) |ch| p.next(ch); @@ -2892,7 +2886,7 @@ test "OSC 133: prompt_start with redraw invalid value" { test "OSC 133: prompt_start with continuation" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;k=c"; for (input) |ch| p.next(ch); @@ -2905,7 +2899,7 @@ test "OSC 133: prompt_start with continuation" { test "OSC 133: prompt_start with secondary" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;k=s"; for (input) |ch| p.next(ch); @@ -2918,7 +2912,7 @@ test "OSC 133: prompt_start with secondary" { test "OSC 133: prompt_start with special_key" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;special_key=1"; for (input) |ch| p.next(ch); @@ -2931,7 +2925,7 @@ test "OSC 133: prompt_start with special_key" { test "OSC 133: prompt_start with special_key invalid" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;special_key=bobr"; for (input) |ch| p.next(ch); @@ -2944,7 +2938,7 @@ test "OSC 133: prompt_start with special_key invalid" { test "OSC 133: prompt_start with special_key 0" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;special_key=0"; for (input) |ch| p.next(ch); @@ -2957,7 +2951,7 @@ test "OSC 133: prompt_start with special_key 0" { test "OSC 133: prompt_start with special_key empty" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;special_key="; for (input) |ch| p.next(ch); @@ -2970,7 +2964,7 @@ test "OSC 133: prompt_start with special_key empty" { test "OSC 133: prompt_start with click_events true" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;click_events=1"; for (input) |ch| p.next(ch); @@ -2983,7 +2977,7 @@ test "OSC 133: prompt_start with click_events true" { test "OSC 133: prompt_start with click_events false" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;click_events=0"; for (input) |ch| p.next(ch); @@ -2996,7 +2990,7 @@ test "OSC 133: prompt_start with click_events false" { test "OSC 133: prompt_start with click_events empty" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;A;click_events="; for (input) |ch| p.next(ch); @@ -3009,7 +3003,7 @@ test "OSC 133: prompt_start with click_events empty" { test "OSC 133: end_of_command no exit code" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;D"; for (input) |ch| p.next(ch); @@ -3021,7 +3015,7 @@ test "OSC 133: end_of_command no exit code" { test "OSC 133: end_of_command with exit code" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;D;25"; for (input) |ch| p.next(ch); @@ -3034,7 +3028,7 @@ test "OSC 133: end_of_command with exit code" { test "OSC 133: prompt_end" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;B"; for (input) |ch| p.next(ch); @@ -3046,7 +3040,7 @@ test "OSC 133: prompt_end" { test "OSC 133: end_of_input" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C"; for (input) |ch| p.next(ch); @@ -3058,7 +3052,7 @@ test "OSC 133: end_of_input" { test "OSC 133: end_of_input with cmdline 1" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=echo bobr kurwa"; for (input) |ch| p.next(ch); @@ -3072,7 +3066,7 @@ test "OSC 133: end_of_input with cmdline 1" { test "OSC 133: end_of_input with cmdline 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=echo bobr\\ kurwa"; for (input) |ch| p.next(ch); @@ -3086,7 +3080,7 @@ test "OSC 133: end_of_input with cmdline 2" { test "OSC 133: end_of_input with cmdline 3" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=echo bobr\\nkurwa"; for (input) |ch| p.next(ch); @@ -3100,7 +3094,7 @@ test "OSC 133: end_of_input with cmdline 3" { test "OSC 133: end_of_input with cmdline 4" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=$'echo bobr kurwa'"; for (input) |ch| p.next(ch); @@ -3114,7 +3108,7 @@ test "OSC 133: end_of_input with cmdline 4" { test "OSC 133: end_of_input with cmdline 5" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline='echo bobr kurwa'"; for (input) |ch| p.next(ch); @@ -3128,7 +3122,7 @@ test "OSC 133: end_of_input with cmdline 5" { test "OSC 133: end_of_input with cmdline 6" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline='echo bobr kurwa"; for (input) |ch| p.next(ch); @@ -3141,7 +3135,7 @@ test "OSC 133: end_of_input with cmdline 6" { test "OSC 133: end_of_input with cmdline 7" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=$'echo bobr kurwa"; for (input) |ch| p.next(ch); @@ -3154,7 +3148,7 @@ test "OSC 133: end_of_input with cmdline 7" { test "OSC 133: end_of_input with cmdline 8" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=$'"; for (input) |ch| p.next(ch); @@ -3167,7 +3161,7 @@ test "OSC 133: end_of_input with cmdline 8" { test "OSC 133: end_of_input with cmdline 9" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline=$'"; for (input) |ch| p.next(ch); @@ -3180,7 +3174,7 @@ test "OSC 133: end_of_input with cmdline 9" { test "OSC 133: end_of_input with cmdline 10" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline="; for (input) |ch| p.next(ch); @@ -3194,7 +3188,7 @@ test "OSC 133: end_of_input with cmdline 10" { test "OSC 133: end_of_input with cmdline_url 1" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr kurwa"; for (input) |ch| p.next(ch); @@ -3208,7 +3202,7 @@ test "OSC 133: end_of_input with cmdline_url 1" { test "OSC 133: end_of_input with cmdline_url 2" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr%20kurwa"; for (input) |ch| p.next(ch); @@ -3222,7 +3216,7 @@ test "OSC 133: end_of_input with cmdline_url 2" { test "OSC 133: end_of_input with cmdline_url 3" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr%3bkurwa"; for (input) |ch| p.next(ch); @@ -3236,7 +3230,7 @@ test "OSC 133: end_of_input with cmdline_url 3" { test "OSC 133: end_of_input with cmdline_url 4" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr%3kurwa"; for (input) |ch| p.next(ch); @@ -3249,7 +3243,7 @@ test "OSC 133: end_of_input with cmdline_url 4" { test "OSC 133: end_of_input with cmdline_url 5" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr%kurwa"; for (input) |ch| p.next(ch); @@ -3262,7 +3256,7 @@ test "OSC 133: end_of_input with cmdline_url 5" { test "OSC 133: end_of_input with cmdline_url 6" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr%kurwa"; for (input) |ch| p.next(ch); @@ -3275,7 +3269,7 @@ test "OSC 133: end_of_input with cmdline_url 6" { test "OSC 133: end_of_input with cmdline_url 7" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr kurwa%20"; for (input) |ch| p.next(ch); @@ -3289,7 +3283,7 @@ test "OSC 133: end_of_input with cmdline_url 7" { test "OSC 133: end_of_input with cmdline_url 8" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr kurwa%2"; for (input) |ch| p.next(ch); @@ -3302,7 +3296,7 @@ test "OSC 133: end_of_input with cmdline_url 8" { test "OSC 133: end_of_input with cmdline_url 9" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "133;C;cmdline_url=echo bobr kurwa%2"; for (input) |ch| p.next(ch); @@ -3315,7 +3309,7 @@ test "OSC 133: end_of_input with cmdline_url 9" { test "OSC: OSC 777 show desktop notification with title" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "777;notify;Title;Body"; for (input) |ch| p.next(ch); From 14b441be1e2c23b9b10030f1a3f3216d91f42faf Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 13 Oct 2025 14:10:29 -0700 Subject: [PATCH 268/319] renderer: Include arrows block in constrained symbols (#9189) Fixes #8693 **Before** Screenshot 2025-10-13 at 14 00 28 **After** Screenshot 2025-10-13 at 14 01 14 The effect is somewhat subtle with my combination of fonts. See #8693 for the more egregious examples that this fixes. --- src/build/uucode_config.zig | 1 + src/renderer/cell.zig | 1 + 2 files changed, 2 insertions(+) diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 085ca2561..9a3b4bec7 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -41,6 +41,7 @@ fn computeIsSymbol( _ = tracking; const block = data.block; data.is_symbol = data.general_category == .other_private_use or + block == .arrows or block == .dingbats or block == .emoticons or block == .miscellaneous_symbols or diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 8c0215673..d8427689b 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -238,6 +238,7 @@ pub fn isCovering(cp: u21) bool { /// Returns true of the codepoint is a "symbol-like" character, which /// for now we define as anything in a private use area, and anything /// in several unicode blocks: +/// - Arrows /// - Dingbats /// - Emoticons /// - Miscellaneous Symbols From 54625537417459fd18a95a8120d82988c62f6b02 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Oct 2025 14:27:38 -0700 Subject: [PATCH 269/319] config: only create template file is prior was not found --- src/config/Config.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index aabefcd8f..da7fced08 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3509,13 +3509,13 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { // If both files are not found, then we create a template file. // For macOS, we only create the template file in the app support - if (app_support_loaded and xdg_loaded) { + if (!app_support_loaded and !xdg_loaded) { writeConfigTemplate(app_support_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; } } else { - if (xdg_loaded) { + if (!xdg_loaded) { writeConfigTemplate(xdg_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; From 17a20e5b1c0165f24b5606aa909cdebdfb2e56b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Oct 2025 20:44:32 -0700 Subject: [PATCH 270/319] termio: don't start scroll timer if its already active (#9195) This might fix #9191, but since I don't have a reproduction I can't be sure. In any case, this is a bad bug that should be fixed. The issue is that we weren't checking our scroll timer completion state. This meant that if `start_scroll_timer` was called multiple times within a single loop tick, we'd enqueue our completion multiple times, leading to various undefined behaviors. If we don't enqueue anything else in the interim, this is safe by chance. But if we enqueue something else, then we'd hit a queue assertion failure and honestly I'm not totally sure what would happen. I wasn't able to trigger the "bad" case, but I was able to trigger the benign case very easily. Our other timers such as the renderer cursor timer already have this protection. Let's fix this and continue looking... --- src/termio/Thread.zig | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index edf966df7..bb616e623 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -471,15 +471,23 @@ fn stopCallback( fn startScrollTimer(self: *Thread, cb: *CallbackData) void { self.scroll_active = true; - // Start the timer which loops - self.scroll.run( - &self.loop, - &self.scroll_c, - selection_scroll_ms, - CallbackData, - cb, - selectionScrollCallback, - ); + switch (self.scroll_c.state()) { + // If it is already active, e.g. startScrollTimer is called multiple + // times, then we just return. We can't simply check `scroll_active` + // because its possible that `stopScrollTimer` was called but there + // was no loop tick between then and now to halt out completion. + .active => return, + + // If the completion is not active then we need to start it. + .dead => self.scroll.run( + &self.loop, + &self.scroll_c, + selection_scroll_ms, + CallbackData, + cb, + selectionScrollCallback, + ), + } } fn stopScrollTimer(self: *Thread) void { From 75734a4d070b92e9b73dda1aab93f5ae1c7a3766 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Oct 2025 21:00:55 -0700 Subject: [PATCH 271/319] macos: clarify the "ready to install update" state - The copy is updated to better explain what the user should do next. - The symbol is updated to make it clear the update isn't yet installed. --- macos/Sources/Features/Update/UpdateViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index f0b39ed2d..7a92337cc 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -31,7 +31,7 @@ class UpdateViewModel: ObservableObject { case .extracting(let extracting): return String(format: "Preparing: %.0f%%", extracting.progress * 100) case .readyToInstall: - return "Install Update" + return "Ready to Install Update" case .installing: return "Restart to Complete Update" case .notFound: @@ -70,7 +70,7 @@ class UpdateViewModel: ObservableObject { case .extracting: return "shippingbox" case .readyToInstall: - return "checkmark.circle.fill" + return "restart.circle.fill" case .installing: return "power.circle" case .notFound: From 41bb8d7af056e13cc1d20ff038b3edf7f6a1b0dc Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Tue, 14 Oct 2025 06:46:24 -0700 Subject: [PATCH 272/319] fix make clean: change dir name from zig-cache to .zig-cache (#9192) `make clean` was not removing the `.zig-cache` directory. Instead it was removing the `zig-cache` directory. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ad8379f7e..c5511a62e 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ vendor/glad/include/glad/glad.h: vendor/glad/include/glad/gl.h clean: rm -rf \ - zig-out zig-cache \ + zig-out .zig-cache \ macos/build \ macos/GhosttyKit.xcframework .PHONY: clean From 6eb26da3b75e4cff3881502f2d304bcf2755b7b5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Oct 2025 06:55:09 -0700 Subject: [PATCH 273/319] macos: fix failing xcode tests --- macos/Tests/Update/UpdateViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift index 56748ff83..e41804e08 100644 --- a/macos/Tests/Update/UpdateViewModelTests.swift +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -53,7 +53,7 @@ struct UpdateViewModelTests { @Test func testReadyToInstallText() { let viewModel = UpdateViewModel() viewModel.state = .readyToInstall(.init(reply: { _ in })) - #expect(viewModel.text == "Install Update") + #expect(viewModel.text == "Ready to Install Update") } @Test func testInstallingText() { From 06ad3b77b7df11070277432244738cde9b75ceb6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Oct 2025 07:11:10 -0700 Subject: [PATCH 274/319] Zig 0.15.2 (#9200) --- flake.lock | 6 +++--- flake.nix | 2 +- src/benchmark/IsSymbol.zig | 16 ++++++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 349248668..a9005eebe 100644 --- a/flake.lock +++ b/flake.lock @@ -97,11 +97,11 @@ ] }, "locked": { - "lastModified": 1759192380, - "narHash": "sha256-0BWJgt4OSzxCESij5oo8WLWrPZ+1qLp8KUQe32QeV4Q=", + "lastModified": 1760401936, + "narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "0bcd1401ed43d10f10cbded49624206553e92f57", + "rev": "365085b6652259753b598d43b723858184980bbe", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 18241a447..85550b989 100644 --- a/flake.nix +++ b/flake.nix @@ -49,7 +49,7 @@ pkgs = nixpkgs.legacyPackages.${system}; in { devShell.${system} = pkgs.callPackage ./nix/devShell.nix { - zig = zig.packages.${system}."0.15.1"; + zig = zig.packages.${system}."0.15.2"; wraptest = pkgs.callPackage ./nix/wraptest.nix {}; zon2nix = zon2nix; }; diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index dffa5071a..c4667b333 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -91,12 +91,14 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var read_buf: [4096]u8 = undefined; - var r = f.reader(&read_buf); + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -116,12 +118,14 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const f = self.data_f orelse return; var read_buf: [4096]u8 = undefined; - var r = f.reader(&read_buf); + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached From 9f726492ac91d47355e37f04808dd5d3317270d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Oct 2025 07:13:05 -0700 Subject: [PATCH 275/319] macOS: release builds from source using `zig build` uses ReleaseLocal (#9201) This fixes codesign issues that are common. The official release process does not use this, but it is useful for local builds. --- src/build/GhosttyXcodebuild.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 0afb64007..27691d744 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -31,7 +31,7 @@ pub fn init( .ReleaseSafe, .ReleaseSmall, .ReleaseFast, - => "Release", + => "ReleaseLocal", }; const xc_arch: ?[]const u8 = switch (deps.xcframework.target) { From 3d837cbbce254f0169602119e7b3fc33179c954d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Oct 2025 07:31:31 -0700 Subject: [PATCH 276/319] macos: "Check for updates" cancels whatever the current update state is (#9203) This mainly allows users who have a pending update but didn't install it for some time to re-check to see if there is something newer in the mean time. --- .../Features/Update/UpdateController.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 2dfb0a420..8a2a939bd 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -93,7 +93,22 @@ class UpdateController { /// /// This is typically connected to a menu item action. @objc func checkForUpdates() { - updater.checkForUpdates() + // If we're already idle, then just check for updates immediately. + if viewModel.state == .idle { + updater.checkForUpdates() + return + } + + // If we're not idle then we need to cancel any prior state. + installCancellable?.cancel() + viewModel.state.cancel() + + // The above will take time to settle, so we delay the check for some time. + // The 100ms is arbitrary and I'd rather not, but we have to wait more than + // one loop tick it seems. + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in + self?.updater.checkForUpdates() + } } /// Validate the check for updates menu item. From 54b021f6d67f3c95008abe67fbbab6681ca6fb6d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 14 Oct 2025 14:04:42 -0500 Subject: [PATCH 277/319] core: update zf to remove zg transitive dep (#9208) --- build.zig.zon | 4 ++-- build.zig.zon.json | 16 +++------------- build.zig.zon.nix | 22 +++------------------- build.zig.zon.txt | 4 +--- flatpak/zig-packages.json | 18 +++--------------- 5 files changed, 12 insertions(+), 52 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 8c05cb50a..dd2f9cfab 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -50,8 +50,8 @@ }, .zf = .{ // natecraddock/zf - .url = "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz", - .hash = "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg", + .url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + .hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", .lazy = true, }, .gobject = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index a94c3b13a..d0193ed8b 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -119,11 +119,6 @@ "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", "hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" }, - "vaxis-0.5.1-BWNV_BMgCQDXdZzABeY4F_xwgE7nHFtYEP07KgEwJWo8": { - "name": "vaxis", - "url": "git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a", - "hash": "sha256-5c+TjmiH4071lKI+U8SIJ0M+4ezzcAtLI2ZSfZYdXSA=" - }, "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": { "name": "vaxis", "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", @@ -149,15 +144,10 @@ "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", "hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=" }, - "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg": { + "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": { "name": "zf", - "url": "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz", - "hash": "sha256-Kaduui4Llb9Fq1lCKLOeG8cWTknjhos8cSTeJ50KP/I=" - }, - "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9": { - "name": "zg", - "url": "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz", - "hash": "sha256-BZhz1nPqxK6hdsJQ66n7Jk4zMgFSGLXm8eU0CX/7mDI=" + "url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + "hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=" }, "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": { "name": "zig_js", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index ec69854fe..a6d768180 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -274,14 +274,6 @@ in hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; }; } - { - name = "vaxis-0.5.1-BWNV_BMgCQDXdZzABeY4F_xwgE7nHFtYEP07KgEwJWo8"; - path = fetchZigArtifact { - name = "vaxis"; - url = "git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a"; - hash = "sha256-5c+TjmiH4071lKI+U8SIJ0M+4ezzcAtLI2ZSfZYdXSA="; - }; - } { name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS"; path = fetchZigArtifact { @@ -323,19 +315,11 @@ in }; } { - name = "zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg"; + name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz"; - hash = "sha256-Kaduui4Llb9Fq1lCKLOeG8cWTknjhos8cSTeJ50KP/I="; - }; - } - { - name = "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9"; - path = fetchZigArtifact { - name = "zg"; - url = "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz"; - hash = "sha256-BZhz1nPqxK6hdsJQ66n7Jk4zMgFSGLXm8eU0CX/7mDI="; + url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz"; + hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index ef854348a..e789fa4eb 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,6 +1,4 @@ git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732 -git+https://github.com/rockorager/libvaxis#6eb16bb4190dc074dafaf4f0ce7dadd50e81192a -https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz @@ -32,6 +30,6 @@ https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz -https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz +https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index ef9cb7624..93d73d2ca 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -143,12 +143,6 @@ "dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", "sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" }, - { - "type": "git", - "url": "https://github.com/rockorager/libvaxis", - "commit": "6eb16bb4190dc074dafaf4f0ce7dadd50e81192a", - "dest": "vendor/p/vaxis-0.5.1-BWNV_BMgCQDXdZzABeY4F_xwgE7nHFtYEP07KgEwJWo8" - }, { "type": "archive", "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", @@ -181,15 +175,9 @@ }, { "type": "archive", - "url": "https://github.com/natecraddock/zf/archive/d2c89a7d6b579981da6c9c8912134cf5a22c7194.tar.gz", - "dest": "vendor/p/zf-0.10.3-OIRy8euIAACn1wj5gnZbqxbAdAKI5JGMeex8zrdPqORg", - "sha256": "29a76eba2e0b95bf45ab594228b39e1bc7164e49e3868b3c7124de279d0a3ff2" - }, - { - "type": "archive", - "url": "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz", - "dest": "vendor/p/zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9", - "sha256": "059873d673eac4aea176c250eba9fb264e3332015218b5e6f1e534097ffb9832" + "url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + "dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", + "sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568" }, { "type": "archive", From e5247f6d10ae02cc892c77d7435319549769ba1c Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 14 Oct 2025 15:12:45 -0400 Subject: [PATCH 278/319] termio: reimplement OSC 7 URI handling (#9193) This reimplements the MAC address-aware URI parsing logic used by the OSC 7 handler and adds an additional .raw_path option that returns the full, unencoded path string (including query and fragment values), which is needed for compliant kitty-shell-cwd:// handling. Notably, this implementation takes an options-based approach that allows these additional behaviors to be enabled at runtime. It also leverages two std.Uri.parse guarantees: 1. Return slices point into the original text string. 2. .raw components don't require unescaping (.percent_encoded does). The implementation is in a new 'os.uri' module because its now generic enough to not be hostname-oriented. We use os.uri.parseUri and its parsing options to reimplement our OSC 7 file-style URI handling. This has two advantages: First, it fixes kitty-shell-cwd scheme handling. This scheme expects the full, unencoded path string, whereas the file scheme expects normal URI percent encoding. This was preventing paths containing "special" URI characters (like "path?") from working correctly in our bash, zsh, and elvish shell integrations, which report working directories using the kitty-shell-cwd scheme. (fish uses file URIs, which work as expected.) Second, we can greatly simplify our hostname and path string handling because we can now rely on the "raw" std.Uri component form to always provide the correct representation. Lastly, this lets us remove the previous URI-related code from the os.hostname module, restoring its focus to hostname-related functions. See: #5289 --- src/os/hostname.zig | 317 +--------------------------------- src/os/main.zig | 2 + src/os/uri.zig | 204 ++++++++++++++++++++++ src/termio/stream_handler.zig | 73 +++----- 4 files changed, 234 insertions(+), 362 deletions(-) create mode 100644 src/os/uri.zig diff --git a/src/os/hostname.zig b/src/os/hostname.zig index a75ca1cbb..283ece8d9 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,148 +1,11 @@ const std = @import("std"); -const builtin = @import("builtin"); const posix = std.posix; -pub const HostnameParsingError = error{ - NoHostnameInUri, - NoSpaceLeft, -}; - pub const UrlParsingError = std.Uri.ParseError || error{ HostnameIsNotMacAddress, NoSchemeProvided, }; -const mac_address_length = 17; - -fn isUriPathSeparator(c: u8) bool { - return switch (c) { - '?', '#' => true, - else => false, - }; -} - -fn isValidMacAddress(mac_address: []const u8) bool { - // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. - if (mac_address.len != 17) { - return false; - } - - for (mac_address, 0..) |c, i| { - if ((i + 1) % 3 == 0) { - if (c != ':') { - return false; - } - } else if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { - return false; - } - } - - return true; -} - -/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and -/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS -/// the url passed to this function might have a mac address as its hostname and parses it -/// correctly. -pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri { - return std.Uri.parse(url) catch |e| { - // The mac-address-as-hostname issue is specific to macOS so we just return an error if we - // hit it on other platforms. - if (comptime builtin.os.tag != .macos) return e; - - // It's possible this is a mac address on macOS where the last 2 characters in the - // address are non-digits, e.g. 'ff', and thus an invalid port. - // - // Example: file://12:34:56:78:90:12/path/to/file - if (e != error.InvalidPort) return e; - - const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse { - return error.NoSchemeProvided; - }; - const scheme = url[0..url_without_scheme_start]; - const url_without_scheme = url[url_without_scheme_start + 3 ..]; - - // The first '/' after the scheme marks the end of the hostname. If the first '/' - // following the end of the scheme is not at the right position this is not a - // valid mac address. - if (url_without_scheme.len != mac_address_length and - std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length) - { - return error.HostnameIsNotMacAddress; - } - - // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..mac_address_length]; - - if (!isValidMacAddress(mac_address)) { - return error.HostnameIsNotMacAddress; - } - - var uri_path_end_idx: usize = mac_address_length; - while (uri_path_end_idx < url_without_scheme.len and - !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) - { - uri_path_end_idx += 1; - } - - // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI - // spec. - return .{ - .scheme = scheme, - .host = .{ .percent_encoded = mac_address }, - .path = .{ - .percent_encoded = url_without_scheme[mac_address_length..uri_path_end_idx], - }, - }; - }; -} - -/// 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 error.NoHostnameInUri; - const host: []const u8 = 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 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 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; - - // 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 > 99) return host; - - var fbs = std.io.fixedBufferStream(buf); - try std.fmt.format( - fbs.writer(), - // Make sure "port" is always 2 digits, prefixed with a 0 when "port" is a 1-digit number. - "{s}:{d:0>2}", - .{ host, port }, - ); - - return fbs.getWritten(); -} - pub const LocalHostnameValidationError = error{ PermissionDenied, Unexpected, @@ -151,7 +14,7 @@ pub const LocalHostnameValidationError = error{ /// 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 { +pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. if (std.mem.eql(u8, "localhost", hostname)) return true; @@ -161,185 +24,19 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { return std.mem.eql(u8, hostname, ourHostname); } -test parseUrl { - // 1. Typical hostnames. - - var uri = try parseUrl("file://personal.computer/home/test/"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - // 2. Hostnames that are mac addresses. - - // Numerical mac addresses. - - uri = try parseUrl("file://12:34:56:78:90:12/home/test/"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - // Alphabetical mac addresses. - - uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - // 3. Hostnames that are mac addresses with no path. - - // Numerical mac addresses. - - uri = try parseUrl("file://12:34:56:78:90:12"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - // Alphabetical mac addresses. - - uri = try parseUrl("file://ab:cd:ef:ab:cd:ef"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); +test "isLocal returns true when provided hostname is localhost" { + try std.testing.expect(try isLocal("localhost")); } -test "parseUrl succeeds even if path component is missing" { - const uri = try parseUrl("file://12:34:56:78:90:ab"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded); - try std.testing.expect(uri.path.isEmpty()); - try std.testing.expect(uri.port == null); -} - -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 succeeds with hostname as mac address with the last component as ascii" { - const uri = try parseUrl("file://12:34:56:78:90:ab"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:ab", 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"); - - 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 "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")); -} - -test "isLocalHostname returns true when hostname is local" { +test "isLocal 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)); + try std.testing.expect(try isLocal(localHostname)); } -test "isLocalHostname returns false when hostname is not local" { +test "isLocal returns false when hostname is not local" { try std.testing.expectEqual( false, - try isLocalHostname("not-the-local-hostname"), + try isLocal("not-the-local-hostname"), ); } diff --git a/src/os/main.zig b/src/os/main.zig index af851f673..2d269e412 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -29,6 +29,7 @@ pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); pub const macos = @import("macos.zig"); pub const shell = @import("shell.zig"); +pub const uri = @import("uri.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); @@ -67,6 +68,7 @@ pub const getKernelInfo = kernel_info.getKernelInfo; test { _ = i18n; _ = path; + _ = uri; if (comptime builtin.os.tag == .linux) { _ = kernel_info; diff --git a/src/os/uri.zig b/src/os/uri.zig new file mode 100644 index 000000000..3d674870c --- /dev/null +++ b/src/os/uri.zig @@ -0,0 +1,204 @@ +const std = @import("std"); + +pub const ParseOptions = struct { + /// Parse MAC addresses in the host component. + /// + /// This is useful when the "Private Wi-Fi address" is enabled on macOS, + /// which sets the hostname to a rotating MAC address (12:34:56:ab:cd:ef). + mac_address: bool = false, + + /// Return the full, raw, unencoded path string. Any query and fragment + /// values will be return as part of the path instead of as distinct + /// fields. + raw_path: bool = false, +}; + +pub const ParseError = std.Uri.ParseError || error{InvalidMacAddress}; + +/// Parses a URI from the given string. +/// +/// This extends std.Uri.parse with some additional ParseOptions. +pub fn parse(text: []const u8, options: ParseOptions) ParseError!std.Uri { + var uri = std.Uri.parse(text) catch |err| uri: { + // We can attempt to re-parse the text as a URI that has a MAC address + // in its host field (which tripped up std.Uri.parse's port parsing): + // + // file://12:34:56:78:90:aa/path/to/file + // ^^ InvalidPort + // + if (err != error.InvalidPort or !options.mac_address) return err; + + // We can assume that the initial Uri.parse already validated the + // scheme, so we only need to find its bounds within the string. + const scheme_end = std.mem.indexOf(u8, text, "://") orelse { + return error.InvalidFormat; + }; + const scheme = text[0..scheme_end]; + + // We similarly find the bounds of the host component by looking + // for the first slash (/) after the scheme. This is all we need + // for this case because the resulting slice can be unambiguously + // determined to be a MAC address (or not). + const host_start = scheme_end + "://".len; + const host_end = std.mem.indexOfScalarPos(u8, text, host_start, '/') orelse text.len; + const mac_address = text[host_start..host_end]; + if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress; + + // Parse the rest of the text (starting with the path component) as a + // partial URI and then add our MAC address as its host component. + var uri = try std.Uri.parseAfterScheme(scheme, text[host_end..]); + uri.host = .{ .percent_encoded = mac_address }; + break :uri uri; + }; + + // When MAC address parsing is enabled, we need to handle the case where + // std.Uri.parse parsed the address's last octet as a numeric port number. + // We use a few heuristics to identify this case (14 characters, 4 colons) + // and then "repair" the result by reassign the .host component to the full + // MAC address and clearing the .port component. + // + // 12:34:56:78:90:99 -> [12:34:56:78:90, 99] -> 12:34:56:78:90:99 + // (original host) (parsed host + port) (restored host) + // + if (options.mac_address and uri.host != null) mac: { + const host = uri.host.?.percent_encoded; + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) break :mac; + + const port = uri.port orelse break :mac; + if (port > 99) break :mac; + + // std.Uri.parse returns slices pointing into the original text string. + const host_start = @intFromPtr(host.ptr) - @intFromPtr(text.ptr); + const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr); + const mac_address = text[host_start..path_start]; + if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress; + + uri.host = .{ .percent_encoded = mac_address }; + uri.port = null; + } + + // When the raw_path option is active, return everything after the authority + // (host) in the .path component, including any query and fragment values. + if (options.raw_path) { + // std.Uri.parse returns slices pointing into the original text string. + const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr); + uri.path = .{ .raw = text[path_start..] }; + uri.query = null; + uri.fragment = null; + } + + return uri; +} + +test "parse: mac_address" { + const testing = @import("std").testing; + + // Numeric MAC address without a port + const uri1 = try parse("file://00:12:34:56:78:90/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri1.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri1.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri1.path.percent_encoded); + try testing.expectEqual(null, uri1.port); + + // Numeric MAC address with a port + const uri2 = try parse("file://00:12:34:56:78:90:999/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri2.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri2.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri2.path.percent_encoded); + try testing.expectEqual(999, uri2.port); + + // Alphabetic MAC address without a port + const uri3 = try parse("file://ab:cd:ef:ab:cd:ef/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri3.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri3.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri3.path.percent_encoded); + try testing.expectEqual(null, uri3.port); + + // Alphabetic MAC address with a port + const uri4 = try parse("file://ab:cd:ef:ab:cd:ef:999/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri4.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri4.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri4.path.percent_encoded); + try testing.expectEqual(999, uri4.port); + + // Numeric MAC address without a path component + const uri5 = try parse("file://00:12:34:56:78:90", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri5.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri5.host.?.percent_encoded); + try testing.expect(uri5.path.isEmpty()); + + // Alphabetic MAC address without a path component + const uri6 = try parse("file://ab:cd:ef:ab:cd:ef", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri6.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri6.host.?.percent_encoded); + try testing.expect(uri6.path.isEmpty()); + + // Invalid MAC addresses + try testing.expectError(error.InvalidMacAddress, parse( + "file://zz:zz:zz:zz:zz:00/path", + .{ .mac_address = true }, + )); + try testing.expectError(error.InvalidMacAddress, parse( + "file://zz:zz:zz:zz:zz:zz/path", + .{ .mac_address = true }, + )); +} + +test "parse: raw_path" { + const testing = @import("std").testing; + + const text = "file://localhost/path??#fragment"; + var buf: [256]u8 = undefined; + + const uri1 = try parse(text, .{ .raw_path = false }); + try testing.expectEqualStrings("file", uri1.scheme); + try testing.expectEqualStrings("localhost", uri1.host.?.percent_encoded); + try testing.expectEqualStrings("/path", try uri1.path.toRaw(&buf)); + try testing.expectEqualStrings("?", uri1.query.?.percent_encoded); + try testing.expectEqualStrings("fragment", uri1.fragment.?.percent_encoded); + + const uri2 = try parse(text, .{ .raw_path = true }); + try testing.expectEqualStrings("file", uri2.scheme); + try testing.expectEqualStrings("localhost", uri2.host.?.percent_encoded); + try testing.expectEqualStrings("/path??#fragment", try uri2.path.toRaw(&buf)); + try testing.expectEqual(null, uri2.query); + try testing.expectEqual(null, uri2.fragment); + + const uri3 = try parse("file://localhost", .{ .raw_path = true }); + try testing.expectEqualStrings("file", uri3.scheme); + try testing.expectEqualStrings("localhost", uri3.host.?.percent_encoded); + try testing.expect(uri3.path.isEmpty()); + try testing.expectEqual(null, uri3.query); + try testing.expectEqual(null, uri3.fragment); +} + +/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef. +fn isValidMacAddress(s: []const u8) bool { + if (s.len != 17) return false; + + for (s, 0..) |c, i| { + if (i % 3 == 2) { + if (c != ':') return false; + } else { + switch (c) { + '0'...'9', 'A'...'F', 'a'...'f' => {}, + else => return false, + } + } + } + + return true; +} + +test isValidMacAddress { + const testing = @import("std").testing; + + try testing.expect(isValidMacAddress("01:23:45:67:89:Aa")); + try testing.expect(isValidMacAddress("Aa:Bb:Cc:Dd:Ee:Ff")); + + try testing.expect(!isValidMacAddress("")); + try testing.expect(!isValidMacAddress("00:23:45")); + try testing.expect(!isValidMacAddress("00:23:45:Xx:Yy:Zz")); + try testing.expect(!isValidMacAddress("01-23-45-67-89-Aa")); + try testing.expect(!isValidMacAddress("01:23:45:67:89:Aa:Bb")); +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 06ff29809..db2cf11a6 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1089,7 +1089,13 @@ pub const StreamHandler = struct { return; } - const uri: std.Uri = internal_os.hostname.parseUrl(url) catch |e| { + // Attempt to parse this file-style URI using options appropriate + // for this OSC 7 context (e.g. kitty-shell-cwd expects the full, + // unencoded path). + const uri: std.Uri = internal_os.uri.parse(url, .{ + .mac_address = comptime builtin.os.tag != .macos, + .raw_path = std.mem.startsWith(u8, url, "kitty-shell-cwd://"), + }) catch |e| { log.warn("invalid url in OSC 7: {}", .{e}); return; }; @@ -1097,26 +1103,18 @@ pub const StreamHandler = struct { if (!std.mem.eql(u8, "file", uri.scheme) and !std.mem.eql(u8, "kitty-shell-cwd", uri.scheme)) { - log.warn("OSC 7 scheme must be file, got: {s}", .{uri.scheme}); + log.warn("OSC 7 scheme must be file or kitty-shell-cwd, got: {s}", .{uri.scheme}); 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_from_uri = internal_os.hostname.bufPrintHostnameFromFileUri( - &host_and_port_buf, - uri, - ) catch |err| switch (err) { - error.NoHostnameInUri => { + var host_buffer: [std.Uri.host_name_max]u8 = undefined; + const host = uri.getHost(&host_buffer) catch |err| switch (err) { + error.UriMissingHost => { 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}); + error.UriHostTooLong => { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); return; }, }; @@ -1124,9 +1122,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 = internal_os.hostname.isLocalHostname( - hostname_from_uri, - ) catch |err| switch (err) { + const host_valid = internal_os.hostname.isLocal(host) catch |err| switch (err) { error.PermissionDenied, error.Unexpected, => { @@ -1135,43 +1131,16 @@ pub const StreamHandler = struct { }, }; if (!host_valid) { - log.warn("OSC 7 host must be local", .{}); + log.warn("OSC 7 host ({s}) must be local", .{host}); return; } - // We need to unescape the path. We first try to unescape onto - // the stack and fall back to heap allocation if we have to. - var path_buf: [1024]u8 = undefined; - const path, const heap = path: { - // 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 path = switch (uri.path) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // If the path doesn't have any escapes, we can use it directly. - if (std.mem.indexOfScalar(u8, path, '%') == null) - break :path .{ path, false }; - - // First try to stack-allocate - var stack_writer: std.Io.Writer = .fixed(&path_buf); - if (uri.path.formatRaw(&stack_writer)) |_| { - break :path .{ stack_writer.buffered(), false }; - } else |_| {} - - // Fall back to heap - var alloc_writer: std.Io.Writer.Allocating = .init(self.alloc); - if (uri.path.formatRaw(&alloc_writer.writer)) |_| { - break :path .{ alloc_writer.written(), true }; - } else |_| {} - - // Fall back to using it directly... - log.warn("failed to unescape OSC 7 path, using it directly path={s}", .{path}); - break :path .{ path, false }; - }; - defer if (heap) self.alloc.free(path); + // We need the raw path, which might require unescaping. We try to + // avoid making any heap allocations by using the stack first. + var arena_alloc: std.heap.ArenaAllocator = .init(self.alloc); + var stack_alloc = std.heap.stackFallback(1024, arena_alloc.allocator()); + defer arena_alloc.deinit(); + const path = try uri.path.toRawMaybeAlloc(stack_alloc.get()); log.debug("terminal pwd: {s}", .{path}); try self.terminal.setPwd(path); From bdd2e4d7347cbf16db479ecea8108b44e7e4203d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 15 Oct 2025 13:55:11 -0500 Subject: [PATCH 279/319] build: more Zig 0.15.2 updates (#9217) - update nixpkgs now that Zig 0.15.2 is available in nixpkgs - drop hack that worked around compile failures on systems with more than 32 cores - enforce patch version of Zig --- build.zig | 14 -------------- build.zig.zon | 2 +- flake.lock | 6 +++--- flatpak/dependencies.yml | 8 ++++---- src/build/zig.zig | 5 +++-- src/config/Config.zig | 2 +- src/terminal/PageList.zig | 2 +- typos.toml | 4 ++++ 8 files changed, 17 insertions(+), 26 deletions(-) diff --git a/build.zig b/build.zig index 205896390..7836b5c0d 100644 --- a/build.zig +++ b/build.zig @@ -10,10 +10,6 @@ comptime { } pub fn build(b: *std.Build) !void { - // Works around a Zig but still present in 0.15.1. Remove when fixed. - // https://github.com/ghostty-org/ghostty/issues/8924 - try limitCoresForZigBug(); - // This defines all the available build options (e.g. `-D`). If you // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. @@ -312,13 +308,3 @@ pub fn build(b: *std.Build) !void { try translations_step.addError("cannot update translations when i18n is disabled", .{}); } } - -// WARNING: Remove this when https://github.com/ghostty-org/ghostty/issues/8924 is resolved! -// Limit ourselves to 32 cpus on Linux because of an upstream Zig bug. -fn limitCoresForZigBug() !void { - if (comptime builtin.os.tag != .linux) return; - const pid = std.os.linux.getpid(); - var set: std.bit_set.ArrayBitSet(usize, std.os.linux.CPU_SETSIZE * 8) = .initEmpty(); - for (0..32) |cpu| set.set(cpu); - try std.os.linux.sched_setaffinity(pid, &set.masks); -} diff --git a/build.zig.zon b/build.zig.zon index dd2f9cfab..050162936 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,7 +3,7 @@ .version = "1.2.1", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.15.2", .dependencies = .{ // Zig libs diff --git a/flake.lock b/flake.lock index a9005eebe..90b97ed4a 100644 --- a/flake.lock +++ b/flake.lock @@ -37,10 +37,10 @@ "nixpkgs": { "locked": { "lastModified": 315532800, - "narHash": "sha256-YwoXN6fthkakCFD7nXPcUK+rkNr6ZTNTuF8zdGaxZo0=", - "rev": "dc704e6102e76aad573f63b74c742cd96f8f1e6c", + "narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=", + "rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre870318.dc704e6102e7/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre877938.82c2e0d6dde5/nixexprs.tar.xz" }, "original": { "type": "tarball", diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml index 667e4662c..87512a547 100644 --- a/flatpak/dependencies.yml +++ b/flatpak/dependencies.yml @@ -13,12 +13,12 @@ modules: - chmod a+x /app/zig/zig sources: - type: archive - sha256: c61c5da6edeea14ca51ecd5e4520c6f4189ef5250383db33d01848293bfafe05 - url: https://ziglang.org/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz + sha256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239 + url: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz only-arches: [x86_64] - type: archive - sha256: bb4a8d2ad735e7fba764c497ddf4243cb129fece4148da3222a7046d3f1f19fe - url: https://ziglang.org/download/0.15.1/zig-aarch64-linux-0.15.1.tar.xz + sha256: 958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f + url: https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz only-arches: [aarch64] - name: bzip2-redirect diff --git a/src/build/zig.zig b/src/build/zig.zig index 7e327127d..3ee8ffe74 100644 --- a/src/build/zig.zig +++ b/src/build/zig.zig @@ -7,10 +7,11 @@ pub fn requireZig(comptime required_zig: []const u8) void { const current_vsn = builtin.zig_version; const required_vsn = std.SemanticVersion.parse(required_zig) catch unreachable; if (current_vsn.major != required_vsn.major or - current_vsn.minor != required_vsn.minor) + current_vsn.minor != required_vsn.minor or + current_vsn.patch < required_vsn.patch) { @compileError(std.fmt.comptimePrint( - "Your Zig version v{} does not meet the required build version of v{}", + "Your Zig version v{f} does not meet the required build version of v{f}", .{ current_vsn, required_vsn }, )); } diff --git a/src/config/Config.zig b/src/config/Config.zig index da7fced08..bb10ff439 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4109,7 +4109,7 @@ pub fn finalize(self: *Config) !void { // Clamp our contrast self.@"minimum-contrast" = @min(21, @max(1, self.@"minimum-contrast")); - // Minimmum window size + // Minimum window size if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width"); if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height"); diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 9bf116598..e98c6f50d 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1976,7 +1976,7 @@ pub const AdjustCapacity = struct { pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError; -/// Adjust the capcaity of the given page in the list. This should +/// Adjust the capacity of the given page in the list. This should /// be used in cases where OutOfMemory is returned by some operation /// i.e to increase style counts, grapheme counts, etc. /// diff --git a/typos.toml b/typos.toml index 5a23527d9..e97951755 100644 --- a/typos.toml +++ b/typos.toml @@ -55,6 +55,10 @@ typ = "typ" kend = "kend" # GTK GIR = "GIR" +# terminfo +rin = "rin" +# sprites +ower = "ower" [type.po] extend-glob = ["*.po"] From c91bfb9dd515326bc318ded92d3c7ae0a5b2ab68 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 15 Oct 2025 13:55:30 -0500 Subject: [PATCH 280/319] bump version for development (#9218) --- build.zig.zon | 2 +- nix/package.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 050162936..245a67e7b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ghostty, - .version = "1.2.1", + .version = "1.3.0-dev", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, .minimum_zig_version = "0.15.2", diff --git a/nix/package.nix b/nix/package.nix index 73d31c3b9..3d00648ec 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -40,7 +40,7 @@ in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; - version = "1.2.1"; + version = "1.3.0-dev"; # We limit source like this to try and reduce the amount of rebuilds as possible # thus we only provide the source that is needed for the build From 1caab0c208a48a03d9b73fd60727d2b38e66562b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 15 Oct 2025 13:56:07 -0500 Subject: [PATCH 281/319] nix: make sure zon2nix uses Zig 0.15 in generated files (#9220) --- build.zig.zon.nix | 4 ++-- nix/build-support/check-zig-cache.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon.nix b/build.zig.zon.nix index a6d768180..3c4f4d855 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -5,7 +5,7 @@ fetchurl, fetchgit, runCommandLocal, - zig_0_14, + zig_0_15, name ? "zig-packages", }: let unpackZigArtifact = { @@ -14,7 +14,7 @@ }: runCommandLocal name { - nativeBuildInputs = [zig_0_14]; + nativeBuildInputs = [zig_0_15]; } '' hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" diff --git a/nix/build-support/check-zig-cache.sh b/nix/build-support/check-zig-cache.sh index e92a27b6f..9a3927846 100755 --- a/nix/build-support/check-zig-cache.sh +++ b/nix/build-support/check-zig-cache.sh @@ -79,7 +79,7 @@ elif [ "$1" != "--update" ]; then exit 1 fi -zon2nix "$BUILD_ZIG_ZON" --14 --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json" +zon2nix "$BUILD_ZIG_ZON" --15 --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json" alejandra --quiet "$WORK_DIR/build.zig.zon.nix" prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json" prettier --log-level warn --write "$WORK_DIR/zig-packages.json" From d460800a1710b6fa700424b12421cea0ab5b03ea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 15:44:51 -0700 Subject: [PATCH 282/319] chore: typos should ignore build artifacts (#9222) --- typos.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typos.toml b/typos.toml index e97951755..26876aef9 100644 --- a/typos.toml +++ b/typos.toml @@ -5,6 +5,9 @@ extend-exclude = [ "build.zig.zon.nix", "build.zig.zon.txt", "build.zig.zon.json", + # Build artifacts + "macos/build/*", + "zig-out/*", # vendored code "vendor/*", "pkg/*", From 3665040b59a41540bd9946107735f5a0aa0d7af3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 15:47:08 -0700 Subject: [PATCH 283/319] Selection dragging should not process when terminal screen changes (#9223) This hasn't caused any known bugs but leads to selection memory corruption and assertion failures in runtime safe modes. When the terminal screen changes (primary to secondary) and we have an active dragging mode going either by moving the mouse or our selection tick timer, we should halt. We still keep the mouse state active which lets selection continue once the screen switches back. --- src/Surface.zig | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 82d6615b6..456acad2c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1057,6 +1057,16 @@ fn selectionScrollTick(self: *Surface) !void { defer self.renderer_state.mutex.unlock(); const t: *terminal.Terminal = self.renderer_state.terminal; + // If our screen changed while this is happening, we stop our + // selection scroll. + if (self.mouse.left_click_screen != t.active_screen) { + self.io.queueMessage( + .{ .selection_scroll = false }, + .locked, + ); + return; + } + // Scroll the viewport as required try t.scrollViewport(.{ .delta = delta }); @@ -4151,6 +4161,12 @@ pub fn cursorPosCallback( // count because we don't want to handle selection. if (self.mouse.left_click_count == 0) break :select; + // If our terminal screen changed then we don't process this. We don't + // invalidate our pin or mouse state because if the screen switches + // back then we can continue our selection. + const t: *terminal.Terminal = self.renderer_state.terminal; + if (self.mouse.left_click_screen != t.active_screen) break :select; + // All roads lead to requiring a re-render at this point. try self.queueRender(); @@ -4174,7 +4190,7 @@ pub fn cursorPosCallback( } // Convert to points - const screen = &self.renderer_state.terminal.screen; + const screen = &t.screen; const pin = screen.pages.pin(.{ .viewport = .{ .x = pos_vp.x, From 014a2e004274c6725c3f0225dd2a230c1cbeae30 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 16:01:26 -0700 Subject: [PATCH 284/319] termio: color change operations must gracefully handle renderer mailbox full (#9224) Fixes #9191 This changes our color change operations from writing to the renderer mailbox directly to using our `rendererMailboxWriter` function which handles the scenario where the mailbox is full by yielding the lock, waking up the renderer, and retrying later. This is a known deadlock scenario we've worked around since the private beta days, but unfortunately this slipped through and I didn't catch it in review. What happens here is it's possible with certain escape sequences for the IO thread to saturate other mailboxes with messages while holding the terminal state lock. This can happen to any thread. This ultimately leads to starvation and all threads deadlock. Our IO thread is the only thread that produces this kind of massive stream of events while holding the lock, so we have helpers in it to attempt to queue (cheap, fast) and if that fails then to yield the lock, wakeup the target thread, requeue, and grab the lock again (expensive, slow). --- src/termio/stream_handler.zig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index db2cf11a6..dd8669d90 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1192,21 +1192,21 @@ pub const StreamHandler = struct { .dynamic => |dynamic| switch (dynamic) { .foreground => { self.foreground_color = set.color; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .foreground_color = set.color, - }, .{ .forever = {} }); + }); }, .background => { self.background_color = set.color; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .background_color = set.color, - }, .{ .forever = {} }); + }); }, .cursor => { self.cursor_color = set.color; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .cursor_color = set.color, - }, .{ .forever = {} }); + }); }, .pointer_foreground, .pointer_background, @@ -1246,9 +1246,9 @@ pub const StreamHandler = struct { .dynamic => |dynamic| switch (dynamic) { .foreground => { self.foreground_color = null; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .foreground_color = self.foreground_color, - }, .{ .forever = {} }); + }); self.surfaceMessageWriter(.{ .color_change = .{ .target = target, @@ -1257,9 +1257,9 @@ pub const StreamHandler = struct { }, .background => { self.background_color = null; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .background_color = self.background_color, - }, .{ .forever = {} }); + }); self.surfaceMessageWriter(.{ .color_change = .{ .target = target, @@ -1269,9 +1269,9 @@ pub const StreamHandler = struct { .cursor => { self.cursor_color = null; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .cursor_color = self.cursor_color, - }, .{ .forever = {} }); + }); if (self.default_cursor_color) |color| { self.surfaceMessageWriter(.{ .color_change = .{ From e1b527fb9ac276f21d64780818e4bfd70ead802e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 19:42:49 -0700 Subject: [PATCH 285/319] core: PageList tracks minimum metadata for rendering a scrollbar (#9225) Related to #111 This adds the necessary logic and data for the `PageList` data structure to keep track of **total length** of the screen, **offset** into the viewport, and **length** of the viewport. These three values are necessary to _render_ a scrollbar. This PR updates the renderer to grab this information but stops short of actually drawing a scrollbar (which we'll do with native UI), in the interest of having a PR that doesn't contain too many changes. **This doesn't yet draw a scrollbar, these are just the internal changes necessary to support it.** ## Background The `PageList` structure is very core to how we represent terminal state. It maintains a doubly linked list of "pages" (not literally virtual memory pages, but close). Each page stores cell information, styles, hyperlinks, etc fully self-contained in a contiguous sets of VM pages using offset addresses rather than full pointers. **Pages are not guaranteed to be equal sizes.** (This is where scrollbars get difficult) Because it is a linked list structure of non-equal sized nodes, it isn't amenable to typical scrollbar behavior. A scrollbar needs to know: full size, offset, and length in order to draw the scrollbar properly. Getting these values naively is `O(N)` within the data structure that is on the hottest IO performance path in all of Ghostty. ## Implementation ### PageList We now maintain two cached values for **total length** and **viewport offset**. The total length is relatively straightforward, we just have to be careful to update it in every operation that could add or remove rows. I've done this and ensured that every place we update it is covered with unit test coverage. The viewport offset is nasty, but I came up with what I believe is a good solution. The viewport when arbitrarily scrolled is defined as a direct pointer to the linked list node plus a row offset into that node. The only way to calculate offset from the top is `O(N)`. But we have a couple shortcuts: 1. If the viewport is at the bottom (most common) or top, calculating the offset is `O(1)`: bottom is `total_rows - active_rows`, both readily available. And top is `0` by definition. 2. Operations on the PageList typically add or remove rows. We don't do arbitrary linked list surgery. If we instrument those areas with delta updates to our cache, we can avoid the `O(N)` cost for most operations, including scrolling a scrollbar. The only expensive operation is a full, arbitrary jump (new node pointer). Point 1 was quick to implement, so I focused all the complexity on point 2. Whenever we have an operation that adds or removes rows (for example pruning the scroll back, adding more, erase rows within the active area, etc.) then I do the math to calculate the delta change required for the offset if we've already calculated it, and apply that directly. ### Renderer The other issue was how to notify the apprts of scrollbar state. Sending messages on any terminal change within the IO thread is a non-option because (1) sending messages is slow (2) the terminal changes a lot and (3) any slowness in the IO thread slows down overall terminal throughput. The solution was to **trigger scrollbar notifications with the renderer vsync**. We read the scrollbar information when we render a frame, compare it to renderer previous state, and if the scrollbar changed, send a message to the apprt _after the frame is GPU-renderer_. The renderer spends _most_ of its time sleeping compared to the IO thread, and has more opportunities for optimizing its awake time. Additionally, there's no reason to update the scrollbar information if the renderer hasn't rendered the new frames because the user can't even see the stuff the scrollbar wants to scroll to. We're talking about millisecond scale stuff here at worst but it adds up. ## Performance No noticeable performance impact for the additional metrics: image ## AI Usage I used Amp to help audit the codebase and write tests. I wrote all the main implementation code manually. I came up with the main design myself. Relevant threads: - https://ampcode.com/threads/T-95fff686-75bb-4553-a2fb-e41fe4cd4b77#message-0-block-0 - https://ampcode.com/threads/T-48e9a288-b280-4eec-83b7-ca73d029b4ef#message-91-block-0 ## Future This is just the internal changes necessary to _draw_ a scrollbar. There will be other changes we'll need to add to handle grabbing and actually jumping the scrollbar. I have a good idea of how to implement those performantly as well. --- src/renderer/generic.zig | 25 + src/terminal/PageList.zig | 1200 ++++++++++++++++++++++++++++++++++--- src/terminal/main.zig | 1 + 3 files changed, 1155 insertions(+), 71 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d66a32286..876e3f945 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -114,6 +114,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// True if the window is focused focused: bool, + /// The most recent scrollbar state. We use this as a cache to + /// determine if we need to notify the apprt that there was a + /// scrollbar change. + scrollbar: terminal.Scrollbar, + scrollbar_dirty: bool, + /// The foreground color set by an OSC 10 sequence. If unset then /// default_foreground_color is used. foreground_color: ?terminal.color.RGB, @@ -683,6 +689,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .grid_metrics = font_critical.metrics, .size = options.size, .focused = true, + .scrollbar = .zero, + .scrollbar_dirty = false, .foreground_color = null, .default_foreground_color = options.config.foreground, .background_color = null, @@ -1087,6 +1095,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { preedit: ?renderer.State.Preedit, cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, + scrollbar: terminal.Scrollbar, /// If true, rebuild the full screen. full_rebuild: bool, @@ -1111,6 +1120,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } + // Get our scrollbar out of the terminal. We synchronize + // the scrollbar read with frame data updates because this + // naturally limits the number of calls to this method (it + // can be expensive) and also makes it so we don't need another + // cross-thread mailbox message within the IO path. + const scrollbar = state.terminal.screen.pages.scrollbar(); + // Swap bg/fg if the terminal is reversed const bg = self.background_color orelse self.default_background_color; const fg = self.foreground_color orelse self.default_foreground_color; @@ -1238,6 +1254,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .preedit = preedit, .cursor_style = cursor_style, .color_palette = state.terminal.color_palette.colors, + .scrollbar = scrollbar, .full_rebuild = full_rebuild, }; }; @@ -1266,6 +1283,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + // The scrollbar is only emitted during draws so we also + // check the scrollbar cache here and update if needed. + // This is pretty fast. + if (!self.scrollbar.eql(critical.scrollbar)) { + self.scrollbar = critical.scrollbar; + self.scrollbar_dirty = true; + } + // Update our background color self.uniforms.bg_color = .{ critical.bg.r, diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index e98c6f50d..b71c87faa 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -128,6 +128,10 @@ explicit_max_size: usize, /// and at least two pages for our algorithms. min_max_size: usize, +/// The total number of rows represented by this PageList. This is used +/// specifically for scrollbar information so we can have the total size. +total_rows: usize, + /// The list of tracked pins. These are kept up to date automatically. tracked_pins: PinSet, @@ -145,12 +149,35 @@ viewport: Viewport, /// never be access directly; use `viewport`. viewport_pin: *Pin, +/// The row offset from the top that the viewport pin is at. We +/// store the offset from the top because it doesn't change while more +/// data is printed to the terminal. +/// +/// This is null when it isn't calculated. It is calculated on demand +/// when the viewportRowOffset function is called, because it is only +/// required for certain operations such as rendering the scrollbar. +/// +/// In order to make this more efficient, in many places where the value +/// would be invalidated, we update it in-place instead. This is key to +/// keeping our performance decent in normal cases since recalculating +/// this from scratch, depending on the size of the scrollback and position +/// of the pin, can be very expensive. +/// +/// This is only valid if viewport is `pin`. Every other offset is +/// self-evident or quick to calculate. +viewport_pin_row_offset: ?usize, + /// The current desired screen dimensions. I say "desired" because individual /// pages may still be a different size and not yet reflowed since we lazily /// reflow text. cols: size.CellCountInt, rows: size.CellCountInt, +/// If this is true then verifyIntegrity will do nothing. This is +/// only present with runtime safety enabled. +pause_integrity_checks: if (build_options.slow_runtime_safety) usize else void = + if (build_options.slow_runtime_safety) 0 else {}, + /// The viewport location. pub const Viewport = union(enum) { /// The viewport is pinned to the active area. By using a specific marker @@ -249,7 +276,7 @@ pub fn init( errdefer tracked_pins.deinit(pool.alloc); try tracked_pins.putNoClobber(pool.alloc, viewport_pin, {}); - return .{ + const result: PageList = .{ .cols = cols, .rows = rows, .pool = pool, @@ -258,10 +285,14 @@ pub fn init( .page_size = page_size, .explicit_max_size = max_size orelse std.math.maxInt(usize), .min_max_size = min_max_size, + .total_rows = rows, .tracked_pins = tracked_pins, .viewport = .{ .active = {} }, .viewport_pin = viewport_pin, + .viewport_pin_row_offset = null, }; + result.assertIntegrity(); + return result; } fn initPages( @@ -306,9 +337,87 @@ fn initPages( return .{ page_list, page_size }; } +/// Assert that the PageList is in a valid state. This is a no-op in +/// release builds. +pub inline fn assertIntegrity(self: *const PageList) void { + if (comptime !build_options.slow_runtime_safety) return; + + self.verifyIntegrity() catch |err| { + log.err("PageList integrity check failed: {}", .{err}); + @panic("PageList integrity check failed"); + }; +} + +/// Pause or resume integrity checks. This is useful when you're doing +/// a multi-step operation that temporarily leaves the PageList in an +/// inconsistent state. +pub inline fn pauseIntegrityChecks(self: *PageList, pause: bool) void { + if (comptime !build_options.slow_runtime_safety) return; + if (pause) { + self.pause_integrity_checks += 1; + } else { + self.pause_integrity_checks -= 1; + } +} + +const IntegrityError = error{ + TotalRowsMismatch, + ViewportPinOffsetMismatch, +}; + +/// Verify the integrity of the PageList. This is expensive and should +/// only be called in debug/test builds. +fn verifyIntegrity(self: *const PageList) IntegrityError!void { + if (comptime !build_options.slow_runtime_safety) return; + if (self.pause_integrity_checks > 0) return; + + // Verify that our cached total_rows matches the actual row count + const actual_total = self.totalRows(); + if (actual_total != self.total_rows) { + log.warn( + "PageList integrity violation: total_rows mismatch cached={} actual={}", + .{ self.total_rows, actual_total }, + ); + return IntegrityError.TotalRowsMismatch; + } + + // Verify that our viewport pin row offset is correct. + if (self.viewport == .pin) pin: { + const cached_offset = self.viewport_pin_row_offset orelse break :pin; + const actual_offset: usize = offset: { + var offset: usize = 0; + var node = self.pages.last; + while (node) |n| : (node = n.prev) { + offset += n.data.size.rows; + if (n == self.viewport_pin.node) { + offset -= self.viewport_pin.y; + break :offset self.total_rows - offset; + } + } + + log.warn( + "PageList integrity violation: viewport pin not in list", + .{}, + ); + return error.ViewportPinOffsetMismatch; + }; + + if (cached_offset != actual_offset) { + log.warn( + "PageList integrity violation: viewport pin offset mismatch cached={} actual={}", + .{ cached_offset, actual_offset }, + ); + return error.ViewportPinOffsetMismatch; + } + } +} + /// Deinit the pagelist. If you own the memory pool (used clonePool) then /// this will reset the pool and retain capacity. pub fn deinit(self: *PageList) void { + // Verify integrity before cleanup + self.assertIntegrity(); + // Always deallocate our hashmap. self.tracked_pins.deinit(self.pool.alloc); @@ -339,6 +448,8 @@ pub fn deinit(self: *PageList) void { /// This can't fail because we always retain at least enough allocated /// memory to fit the active area. pub fn reset(self: *PageList) void { + defer self.assertIntegrity(); + // We need enough pages/nodes to keep our active area. This should // never fail since we by definition have allocated a page already // that fits our size but I'm not confident to make that assertion. @@ -413,6 +524,9 @@ pub fn reset(self: *PageList) void { self.rows, ) catch @panic("initPages failed"); + // Our total rows always goes back to the default + self.total_rows = self.rows; + // Update all our tracked pins to point to our first page top-left { var it = self.tracked_pins.iterator(); @@ -570,9 +684,11 @@ pub fn clone( .min_max_size = self.min_max_size, .cols = self.cols, .rows = self.rows, + .total_rows = total_rows, .tracked_pins = tracked_pins, .viewport = .{ .active = {} }, .viewport_pin = viewport_pin, + .viewport_pin_row_offset = null, }; // We always need to have enough rows for our viewport because this is @@ -589,8 +705,12 @@ pub fn clone( const row = &last.data.rows.ptr(last.data.memory)[last.data.size.rows - 1]; last.data.clearCells(row, 0, result.cols); } + + // Update our total rows to be our row size. + result.total_rows = result.rows; } + result.assertIntegrity(); return result; } @@ -617,6 +737,8 @@ pub const Resize = struct { /// Resize /// TODO: docs pub fn resize(self: *PageList, opts: Resize) !void { + defer self.assertIntegrity(); + if (comptime std.debug.runtime_safety) { // Resize does not work with 0 values, this should be protected // upstream @@ -624,6 +746,12 @@ pub fn resize(self: *PageList, opts: Resize) !void { if (opts.rows) |v| assert(v > 0); } + // Resizing (especially with reflow) can cause our row offset to + // become invalid. Rather than do something fancy like we do other + // places and try to update it in place, we just invalidate it because + // its too easy to get the logic wrong in here. + self.viewport_pin_row_offset = null; + if (!opts.reflow) return try self.resizeWithoutReflow(opts); // Recalculate our minimum max size. This allows grow to work properly @@ -658,7 +786,6 @@ pub fn resize(self: *PageList, opts: Resize) !void { copy.cols = self.cols; break :opts copy; }); - try self.resizeCols(cols, opts.cursor); }, } @@ -728,16 +855,21 @@ fn resizeCols( self.pages.first = dst_node; self.pages.last = dst_node; - var dst_cursor = ReflowCursor.init(dst_node); - // Reflow all our rows. - while (it.next()) |row| { - try dst_cursor.reflowRow(self, row); + { + var dst_cursor = ReflowCursor.init(dst_node); + while (it.next()) |row| { + try dst_cursor.reflowRow(self, row); - // Once we're done reflowing a page, destroy it. - if (row.y == row.node.data.size.rows - 1) { - self.destroyNode(row.node); + // Once we're done reflowing a page, destroy it. + if (row.y == row.node.data.size.rows - 1) { + self.destroyNode(row.node); + } } + + // At the end of the reflow, setup our total row cache + // log.warn("total old={} new={}", .{ self.total_rows, dst_cursor.total_rows }); + self.total_rows = dst_cursor.total_rows; } // If our total rows is less than our active rows, we need to grow. @@ -804,6 +936,9 @@ const ReflowCursor = struct { page_cell: *pagepkg.Cell, new_rows: usize, + /// This is the final row count of the reflowed pages. + total_rows: usize, + fn init(node: *List.Node) ReflowCursor { const page = &node.data; const rows = page.rows.ptr(page.memory); @@ -816,6 +951,9 @@ const ReflowCursor = struct { .page_row = &rows[0], .page_cell = &rows[0].cells.ptr(page.memory)[0], .new_rows = 0, + + // Initially whatever size our input node is. + .total_rows = node.data.size.rows, }; } @@ -1229,12 +1367,21 @@ const ReflowCursor = struct { ) !void { const old_x = self.x; const old_y = self.y; + const old_total_rows = self.total_rows; + + self.* = .init(node: { + // Pause integrity checks because the total row count won't + // be correct during a reflow. + list.pauseIntegrityChecks(true); + defer list.pauseIntegrityChecks(false); + break :node try list.adjustCapacity( + self.node, + adjustment, + ); + }); - self.* = .init(try list.adjustCapacity( - self.node, - adjustment, - )); self.cursorAbsolute(old_x, old_y); + self.total_rows = old_total_rows; } /// True if this cursor is at the bottom of the page by capacity, @@ -1253,11 +1400,6 @@ const ReflowCursor = struct { } } - fn cursorDown(self: *ReflowCursor) void { - assert(self.y + 1 < self.page.size.rows); - self.cursorAbsolute(self.x, self.y + 1); - } - /// Create a new row and move the cursor down. /// /// Asserts that the cursor is on the bottom row of the @@ -1309,6 +1451,12 @@ const ReflowCursor = struct { list: *PageList, cap: Capacity, ) !void { + // The functions below may overwrite self so we need to cache + // our total rows. We add one because no matter what when this + // returns we'll have one more row added. + const new_total_rows: usize = self.total_rows + 1; + defer self.total_rows = new_total_rows; + if (self.bottom()) { try self.cursorNewPage(list, cap); } else { @@ -1374,6 +1522,11 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // destroy pages if we're increasing cols which will free up page_size // so that when we call grow() in the row mods, we won't prune. if (opts.cols) |cols| { + // Any column change without reflow should not result in row counts + // changing. + const old_total_rows = self.total_rows; + defer assert(self.total_rows == old_total_rows); + switch (std.math.order(cols, self.cols)) { .eq => {}, @@ -1442,7 +1595,10 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // behavior because it seemed fine in an ocean of differing behavior // between terminal apps. I'm completely open to changing it as long // as resize behavior isn't regressed in a user-hostile way. - _ = self.trimTrailingBlankRows(self.rows - rows); + const trimmed = self.trimTrailingBlankRows(self.rows - rows); + + // Account for our trimmed rows in the total row cache + self.total_rows -= trimmed; // If we didn't trim enough, just modify our row count and this // will create additional history. @@ -1502,6 +1658,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { } if (build_options.slow_runtime_safety) { + // We never have less rows than our active screen has. assert(self.totalRows() >= self.rows); } } @@ -1673,6 +1830,10 @@ fn trailingBlankLines( /// Trims up to max trailing blank rows from the pagelist and returns the /// number of rows trimmed. A blank row is any row with no text (but may /// have styling). +/// +/// IMPORTANT: This function does NOT update `total_rows`. It returns the +/// number of rows trimmed, and the caller is responsible for decrementing +/// `total_rows` by this amount. fn trimTrailingBlankRows( self: *PageList, max: size.CellCountInt, @@ -1744,21 +1905,81 @@ pub const Scroll = union(enum) { /// previously allocated pages. pub fn scroll(self: *PageList, behavior: Scroll) void { switch (behavior) { - .active => self.viewport = .{ .active = {} }, - .top => self.viewport = .{ .top = {} }, + .active => self.viewport = .active, + .top => self.viewport = .top, .pin => |p| { if (self.pinIsActive(p)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; + return; + } else if (self.pinIsTop(p)) { + self.viewport = .top; return; } self.viewport_pin.* = p; - self.viewport = .{ .pin = {} }; + self.viewport = .pin; + self.viewport_pin_row_offset = null; // invalidate cache }, .delta_prompt => |n| self.scrollPrompt(n), - .delta_row => |n| { - if (n == 0) return; + .delta_row => |n| delta_row: { + switch (self.viewport) { + // If we're at the top and we're scrolling backwards, + // we don't have to do anything, because there's nowhere to go. + .top => if (n <= 0) break :delta_row, + // If we're at active and we're scrolling forwards, we don't + // have to do anything because it'll result in staying in + // the active. + .active => if (n >= 0) break :delta_row, + + // If we're already a pin type, then we can fast-path our + // delta by simply moving the pin. This has the added benefit + // that we can update our row offset cache efficiently, too. + .pin => switch (std.math.order(n, 0)) { + .eq => break :delta_row, + + .lt => switch (self.viewport_pin.upOverflow(@intCast(-n))) { + .offset => |new_pin| { + self.viewport_pin.* = new_pin; + if (self.viewport_pin_row_offset) |*v| { + v.* -= @as(usize, @intCast(-n)); + } + break :delta_row; + }, + + // If we overflow up we're at the top. + .overflow => { + self.viewport = .top; + break :delta_row; + }, + }, + + .gt => switch (self.viewport_pin.downOverflow(@intCast(n))) { + // If we offset its a valid pin but we still have to + // check if we're in the active area. + .offset => |new_pin| { + if (self.pinIsActive(new_pin)) { + self.viewport = .active; + } else { + self.viewport_pin.* = new_pin; + if (self.viewport_pin_row_offset) |*v| { + v.* += @intCast(n); + } + } + break :delta_row; + }, + + // If we overflow down we're at active. + .overflow => { + self.viewport = .active; + break :delta_row; + }, + }, + }, + } + + // Slow path: we have to calculate the new pin by moving + // from our viewport. const top = self.getTopLeft(.viewport); const p: Pin = if (n < 0) switch (top.upOverflow(@intCast(-n))) { .offset => |v| v, @@ -1776,13 +1997,22 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { // active area, you usually expect that the viewport will now // follow the active area. if (self.pinIsActive(p)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; + return; + } + + // If we're at the top, then just set the top. This is a lot + // more efficient everywhere. We must check this after the + // active check above because we prefer active if they overlap. + if (self.pinIsTop(p)) { + self.viewport = .top; return; } // Pin is not active so we need to track it. self.viewport_pin.* = p; - self.viewport = .{ .pin = {} }; + self.viewport = .pin; + self.viewport_pin_row_offset = null; // invalidate cache }, } } @@ -1818,10 +2048,11 @@ fn scrollPrompt(self: *PageList, delta: isize) void { // into the active area. Otherwise, we scroll up to the pin. if (prompt_pin) |p| { if (self.pinIsActive(p)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; } else { self.viewport_pin.* = p; - self.viewport = .{ .pin = {} }; + self.viewport = .pin; + self.viewport_pin_row_offset = null; // invalidate cache } } } @@ -1829,6 +2060,8 @@ fn scrollPrompt(self: *PageList, delta: isize) void { /// Clear the screen by scrolling written contents up into the scrollback. /// This will not update the viewport. pub fn scrollClear(self: *PageList) !void { + defer self.assertIntegrity(); + // Go through the active area backwards to find the first non-empty // row. We use this to determine how many rows to scroll up. const non_empty: usize = non_empty: { @@ -1856,6 +2089,131 @@ pub fn scrollClear(self: *PageList) !void { for (0..non_empty) |_| _ = try self.grow(); } +/// This represents the state necessary to render a scrollbar for this +/// PageList. It has the total size, the offset, and the size of the viewport. +pub const Scrollbar = struct { + /// Total size of the scrollable area. + total: usize, + + /// The offset into the total area that the viewport is at. This is + /// guaranteed to be less than or equal to total. This includes the + /// visible row. + offset: usize, + + /// The length of the visible area. This is including the offset row. + len: usize, + + /// A zero-sized scrollable region. + pub const zero: Scrollbar = .{ + .total = 0, + .offset = 0, + .len = 0, + }; + + /// Comparison for scrollbars. + pub fn eql(self: Scrollbar, other: Scrollbar) bool { + return self.total == other.total and + self.offset == other.offset and + self.len == other.len; + } +}; + +/// Return the scrollbar state for this PageList. +/// +/// This may be expensive to calculate depending on where the viewport +/// is (arbitrary pins are expensive). The caller should take care to only +/// call this as needed and not too frequently. +pub fn scrollbar(self: *PageList) Scrollbar { + return .{ + .total = self.total_rows, + .offset = self.viewportRowOffset(), + .len = self.rows, // Length is always rows + }; +} + +/// Returns the offset of the current viewport from the top of the +/// screen. +/// +/// This is potentially expensive to calculate because if the viewport +/// is a pin and the pin is near the beginning of the scrollback, we +/// will traverse a lot of linked list nodes. +/// +/// The result is cached so repeated calls are cheap. +fn viewportRowOffset(self: *PageList) usize { + return switch (self.viewport) { + .top => 0, + .active => self.total_rows - self.rows, + .pin => pin: { + // We assert integrity on this code path because it verifies + // that the cached value is correct. + defer self.assertIntegrity(); + + // Return cached value if available + if (self.viewport_pin_row_offset) |cached| break :pin cached; + + // Traverse from the end and count rows until we reach the + // viewport pin. We count backwards because most of the time + // a user is scrolling near the active area. + const top_offset: usize = offset: { + var offset: usize = 0; + var node = self.pages.last; + while (node) |n| : (node = n.prev) { + offset += n.data.size.rows; + if (n == self.viewport_pin.node) { + assert(n.data.size.rows > self.viewport_pin.y); + offset -= self.viewport_pin.y; + break :offset self.total_rows - offset; + } + } + + // Invalid pins are not possible. + unreachable; + }; + + // The offset is from the bottom and our cached value and this + // function returns from the top, so we need to invert it. + self.viewport_pin_row_offset = top_offset; + break :pin top_offset; + }, + }; +} + +/// This fixes up the viewport data when rows are removed from the +/// PageList. This will update a viewport to `active` if row removal +/// puts the viewport into the active area, to `top` if the viewport +/// is now at row 0, and updates any row offset caches as necessary. +/// +/// This is unit tested transitively through other tests such as +/// eraseRows. +fn fixupViewport( + self: *PageList, + removed: usize, +) void { + switch (self.viewport) { + .active => {}, + + // For pin, we check if our pin is now in the active area and if so + // we move our viewport back to the active area. + .pin => if (self.pinIsActive(self.viewport_pin.*)) { + self.viewport = .active; + } else if (self.viewport_pin_row_offset) |*v| { + // If we have a cached row offset, we need to update it + // to account for the erased rows. + if (v.* < removed) { + self.viewport = .top; + } else { + v.* -= removed; + } + }, + + // For top, we move back to active if our erasing moved our + // top page into the active area. + .top => if (self.pinIsActive(.{ .node = self.pages.first.? })) { + self.viewport = .active; + }, + } +} + /// Returns the actual max size. This may be greater than the explicit /// value if the explicit value is less than the min_max_size. /// @@ -1887,11 +2245,17 @@ inline fn growRequiredForActive(self: *const PageList) bool { /// /// This returns the newly allocated page node if there is one. pub fn grow(self: *PageList) !?*List.Node { + defer self.assertIntegrity(); + const last = self.pages.last.?; if (last.data.capacity.rows > last.data.size.rows) { // Fast path: we have capacity in the last page. last.data.size.rows += 1; last.data.assertIntegrity(); + + // Increase our total rows by one + self.total_rows += 1; + return null; } @@ -1921,6 +2285,27 @@ pub fn grow(self: *PageList) !?*List.Node { const buf = first.data.memory; @memset(buf, 0); + // Decrease our total row count from the pruned page and then + // add one for our new row. + self.total_rows -= first.data.size.rows; + self.total_rows += 1; + + // If we have a pin viewport cache then we need to update it. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + // If our offset is less than the number of rows in the + // pruned page, then we are now at the top. + if (v.* < first.data.size.rows) { + self.viewport = .top; + break :viewport; + } + + // Otherwise, our viewport pin is below what we pruned + // so we just decrement our offset. + v.* -= first.data.size.rows; + } + } + // Initialize our new page and reinsert it as the last first.data = .initBuf(.init(buf), layout); first.data.size.rows = 1; @@ -1954,6 +2339,9 @@ pub fn grow(self: *PageList) !?*List.Node { // verified the case above. next_node.data.assertIntegrity(); + // Record the increased row count + self.total_rows += 1; + return next_node; } @@ -1997,6 +2385,7 @@ pub fn adjustCapacity( node: *List.Node, adjustment: AdjustCapacity, ) AdjustCapacityError!*List.Node { + defer self.assertIntegrity(); const page: *Page = &node.data; // We always start with the base capacity of the existing page. This @@ -2110,6 +2499,10 @@ inline fn createPageExt( /// 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. +/// +/// IMPORTANT: This function does NOT update `total_rows`. The caller is +/// responsible for accounting for the removed rows. This function only +/// updates `page_size` (byte accounting), not row accounting. fn destroyNode(self: *PageList, node: *List.Node) void { destroyNodeExt(&self.pool, node, &self.page_size); } @@ -2147,6 +2540,7 @@ pub fn eraseRow( self: *PageList, pt: point.Point, ) !void { + defer self.assertIntegrity(); const pn = self.pin(pt).?; var node = pn.node; @@ -2166,6 +2560,9 @@ pub fn eraseRow( } } + // If we have a pinned viewport, we need to adjust for active area. + self.fixupViewport(1); + { // Set all the rows as dirty in this page var dirty = node.data.dirtyBitSet(); @@ -2236,6 +2633,8 @@ pub fn eraseRowBounded( pt: point.Point, limit: usize, ) !void { + defer self.assertIntegrity(); + // This function has a lot of repeated code in it because it is a hot path. // // To get a better idea of what's happening, read eraseRow first for more @@ -2258,6 +2657,21 @@ pub fn eraseRowBounded( var dirty = node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = pn.y, .end = pn.y + limit }, true); + // If our viewport is a pin and our pin is within the erased + // region we need to maybe shift our cache up. We do this here instead + // of in the pin loop below because its unlikely to be true and we + // don't want to run the conditional N times. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node or + p.y < pn.y or + p.y > pn.y + limit or + p.y == 0) break :viewport; + v.* -= 1; + } + } + // Update pins in the shifted region. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { @@ -2291,6 +2705,18 @@ pub fn eraseRowBounded( // Update tracked pins. { + // See the other places we do something similar in this function + // for a detailed explanation. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node or + p.y < pn.y or + p.y == 0) break :viewport; + v.* -= 1; + } + } + const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { if (p.node == node and p.y >= pn.y) { @@ -2329,6 +2755,17 @@ pub fn eraseRowBounded( var dirty = node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = 0, .end = shifted_limit }, true); + // See the other places we do something similar in this function + // for a detailed explanation. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node or + p.y > shifted_limit) break :viewport; + v.* -= 1; + } + } + // Update pins in the shifted region. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { @@ -2353,6 +2790,16 @@ pub fn eraseRowBounded( // Account for the rows shifted in this node. shifted += node.data.size.rows; + // See the other places we do something similar in this function + // for a detailed explanation. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node) break :viewport; + v.* -= 1; + } + } + // Update tracked pins. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { @@ -2381,6 +2828,8 @@ pub fn eraseRows( tl_pt: point.Point, bl_pt: ?point.Point, ) void { + defer self.assertIntegrity(); + // The count of rows that was erased. var erased: usize = 0; @@ -2459,6 +2908,9 @@ pub fn eraseRows( dirty.setRangeValue(.{ .start = 0, .end = chunk.node.data.size.rows }, true); } + // Update our total row count + self.total_rows -= erased; + // If we deleted active, we need to regrow because one of our invariants // is that we always have full active space. if (tl_pt == .active) { @@ -2473,26 +2925,16 @@ pub fn eraseRows( } // If we have a pinned viewport, we need to adjust for active area. - switch (self.viewport) { - .active => {}, - - // For pin, we check if our pin is now in the active area and if so - // we move our viewport back to the active area. - .pin => if (self.pinIsActive(self.viewport_pin.*)) { - self.viewport = .{ .active = {} }; - }, - - // For top, we move back to active if our erasing moved our - // top page into the active area. - .top => if (self.pinIsActive(.{ .node = self.pages.first.? })) { - self.viewport = .{ .active = {} }; - }, - } + self.fixupViewport(erased); } /// 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). +/// +/// IMPORTANT: This function does NOT update `total_rows`. The caller is +/// responsible for accounting for the removed rows before or after calling +/// this function. fn erasePage(self: *PageList, node: *List.Node) void { assert(node.next != null or node.prev != null); @@ -2601,6 +3043,11 @@ fn pinIsActive(self: *const PageList, p: Pin) bool { return false; } +/// Returns true if the pin is at the top of the scrollback area. +fn pinIsTop(self: *const PageList, p: Pin) bool { + return p.y == 0 and p.node == self.pages.first.?; +} + /// Convert a pin to a point in the given context. If the pin can't fit /// within the given tag (i.e. its in the history but you requested active), /// then this will return null. @@ -3341,23 +3788,9 @@ fn totalPages(self: *const PageList) usize { } /// Grow the number of rows available in the page list by n. -/// This is only used for testing so it isn't optimized. +/// This is only used for testing so it isn't optimized in any way. fn growRows(self: *PageList, n: usize) !void { - var page = self.pages.last.?; - var n_rem: usize = n; - if (page.data.size.rows < page.data.capacity.rows) { - const add = @min(n_rem, page.data.capacity.rows - page.data.size.rows); - page.data.size.rows += add; - if (n_rem == add) return; - n_rem -= add; - } - - while (n_rem > 0) { - page = (try self.grow()).?; - const add = @min(n_rem, page.data.capacity.rows); - page.data.size.rows = add; - n_rem -= add; - } + for (0..n) |_| _ = try self.grow(); } /// Clear all dirty bits on all pages. This is not efficient since it @@ -3896,6 +4329,9 @@ test "PageList" { try testing.expect(s.pages.first != null); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + // Initial total rows should be our row count + try testing.expectEqual(s.rows, s.total_rows); + // Our viewport pin must be defined. It isn't used until the // viewport is a pin but it prevents undefined access on clone. try testing.expect(s.viewport_pin.node == s.pages.first.?); @@ -3906,6 +4342,13 @@ test "PageList" { .y = 0, .x = 0, }, s.getTopLeft(.active)); + + // Scrollbar should be where we expect it + try testing.expectEqual(Scrollbar{ + .total = s.rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); } test "PageList init rows across two pages" { @@ -3929,6 +4372,16 @@ test "PageList init rows across two pages" { try testing.expect(s.viewport == .active); try testing.expect(s.pages.first != null); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // Initial total rows should be our row count + try testing.expectEqual(s.rows, s.total_rows); + + // Scrollbar should be where we expect it + try testing.expectEqual(Scrollbar{ + .total = s.rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); } test "PageList pointFromPin active no history" { @@ -4116,6 +4569,13 @@ test "PageList active after grow" { .y = 10, } }, pt); } + + // Scrollbar should be in the active area + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 10, + .len = s.rows, + }, s.scrollbar()); } test "PageList grow allows exceeding max size for active area" { @@ -4141,6 +4601,9 @@ test "PageList grow allows exceeding max size for active area" { page.data.size.rows = 1; page.data.capacity.rows = 1; } + + // Avoid integrity check failures + s.total_rows = s.totalRows(); } // Grow our row and ensure we don't prune pages because we need @@ -4192,6 +4655,13 @@ test "PageList grow prune required with a single page" { const new = try s.grow(); try testing.expect(new != null); try testing.expect(new != s.pages.first); + + // Scrollbar should be in the active area + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll top" { @@ -4220,6 +4690,12 @@ test "PageList scroll top" { } }, pt); } + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); @@ -4229,6 +4705,12 @@ test "PageList scroll top" { } }, pt); } + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + s.scroll(.{ .active = {} }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); @@ -4237,6 +4719,12 @@ test "PageList scroll top" { .y = 20, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row back" { @@ -4257,6 +4745,12 @@ test "PageList scroll delta row back" { s.scroll(.{ .delta_row = -1 }); + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows - 1, + .len = s.rows, + }, s.scrollbar()); + { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -4273,6 +4767,20 @@ test "PageList scroll delta row back" { .y = 9, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows - 11, + .len = s.rows, + }, s.scrollbar()); + + s.scroll(.{ .delta_row = -1 }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows - 12, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row back overflow" { @@ -4301,6 +4809,12 @@ test "PageList scroll delta row back overflow" { } }, pt); } + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); @@ -4309,6 +4823,12 @@ test "PageList scroll delta row back overflow" { .y = 0, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row forward" { @@ -4330,6 +4850,12 @@ test "PageList scroll delta row forward" { s.scroll(.{ .top = {} }); s.scroll(.{ .delta_row = 2 }); + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 2, + .len = s.rows, + }, s.scrollbar()); + { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -4346,6 +4872,12 @@ test "PageList scroll delta row forward" { .y = 2, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 2, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row forward into active" { @@ -4364,6 +4896,12 @@ test "PageList scroll delta row forward into active" { .y = 0, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row back without space preserves active" { @@ -4383,6 +4921,117 @@ test "PageList scroll delta row back without space preserves active" { } try testing.expect(s.viewport == .active); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to pin" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 4, + .x = 2, + } }).? }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 4, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 4, + } }, pt); + } + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 5, + .x = 2, + } }).? }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 5, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } +} + +test "PageList scroll to pin in active" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 30, + .x = 2, + } }).? }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } +} + +test "PageList scroll to pin at top" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 0, + .x = 2, + } }).? }); + + try testing.expect(s.viewport == .top); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } } test "PageList scroll clear" { @@ -4418,7 +5067,7 @@ test "PageList scroll clear" { } } -test "PageList: jump zero" { +test "PageList: jump zero prompts" { const testing = std.testing; const alloc = testing.allocator; @@ -4438,9 +5087,15 @@ test "PageList: jump zero" { s.scroll(.{ .delta_prompt = 0 }); try testing.expect(s.viewport == .active); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } -test "Screen: jump to prompt" { +test "Screen: jump back one prompt" { const testing = std.testing; const alloc = testing.allocator; @@ -4466,6 +5121,12 @@ test "Screen: jump to prompt" { .x = 0, .y = 1, } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 1, + .len = s.rows, + }, s.scrollbar()); } { s.scroll(.{ .delta_prompt = -1 }); @@ -4474,16 +5135,32 @@ test "Screen: jump to prompt" { .x = 0, .y = 1, } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 1, + .len = s.rows, + }, s.scrollbar()); } // Jump forward { s.scroll(.{ .delta_prompt = 1 }); try testing.expect(s.viewport == .active); + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } { s.scroll(.{ .delta_prompt = 1 }); try testing.expect(s.viewport == .active); + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } } @@ -4567,6 +5244,15 @@ test "PageList grow prune scrollback" { defer s.untrackPin(p); try testing.expect(p.node == s.pages.first.?); + // Scroll back to create a pinned viewport (not active) + const pin_y = page1.capacity.rows / 2; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + + // Get the scrollbar state to populate the cache + const scrollbar_before = s.scrollbar(); + try testing.expectEqual(pin_y, scrollbar_before.offset); + // Next should create a new page, but it should reuse our first // page since we're at max size. const new = (try s.grow()).?; @@ -4581,6 +5267,330 @@ test "PageList grow prune scrollback" { try testing.expect(p.node == s.pages.first.?); try testing.expect(p.x == 0); try testing.expect(p.y == 0); + + // Verify the viewport offset cache was invalidated. After pruning, + // the offset should have changed because we removed rows from + // the beginning. + { + const scrollbar_after = s.scrollbar(); + const rows_pruned = page1.capacity.rows; + const expected_offset = if (pin_y >= rows_pruned) + pin_y - rows_pruned + else + 0; + try testing.expectEqual(expected_offset, scrollbar_after.offset); + } +} + +test "PageList grow prune scrollback with viewport pin not in pruned page" { + const testing = std.testing; + const alloc = testing.allocator; + + // Zero here forces minimum max size to effectively two pages. + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + // Grow to capacity of first page + const page1_node = s.pages.last.?; + const page1 = page1_node.data; + for (0..page1.capacity.rows - page1.size.rows) |_| { + try testing.expect(try s.grow() == null); + } + + // Grow and allocate second page, then fill it up + const page2_node = (try s.grow()).?; + const page2 = page2_node.data; + for (0..page2.capacity.rows - page2.size.rows) |_| { + try testing.expect(try s.grow() == null); + } + + // Get our page size + const old_page_size = s.page_size; + + // Scroll back to create a pinned viewport in page2 (NOT page1) + // This is the key difference from the previous test - the viewport + // pin is NOT in the page that will be pruned. + const pin_y = page1.capacity.rows + 5; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expect(s.viewport_pin.node == page2_node); + + // Get the scrollbar state to populate the cache + const scrollbar_before = s.scrollbar(); + try testing.expectEqual(pin_y, scrollbar_before.offset); + + // Next grow will trigger pruning of the first page. + // The viewport_pin.node is page2, not page1, so it won't be moved + // by the pin update loop, but the cached offset still needs to be + // invalidated because rows were removed from the beginning. + const new = (try s.grow()).?; + try testing.expect(s.pages.last.? == new); + try testing.expectEqual(s.page_size, old_page_size); + + // Our first should now be page2 (page1 was pruned) + try testing.expectEqual(page2_node, s.pages.first.?); + + // The viewport pin should still be on page2, unchanged + try testing.expect(s.viewport_pin.node == page2_node); + + // Verify the viewport offset cache was invalidated/updated. + // After pruning, the offset should have decreased by the number + // of rows that were pruned. + const scrollbar_after = s.scrollbar(); + const rows_pruned = page1.capacity.rows; + const expected_offset = pin_y - rows_pruned; + try testing.expectEqual(expected_offset, scrollbar_after.offset); +} + +test "PageList eraseRows invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback + const pin_y = page.capacity.rows; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase some history rows BEFORE the viewport pin. + // This removes rows from before our pin, which changes its absolute + // offset from the top, but the cache is not invalidated. + const rows_to_erase = page.capacity.rows / 2; + s.eraseRows( + .{ .history = .{} }, + .{ .history = .{ .y = rows_to_erase - 1 } }, + ); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - rows_to_erase, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRow invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback + const pin_y = page.capacity.rows; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a single row from the history BEFORE the viewport pin. + // This removes one row from before our pin, which changes its absolute + // offset from the top by 1, but the cache is not invalidated. + try s.eraseRow(.{ .history = .{ .y = 0 } }); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback + const pin_y: u16 = 4; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the history BEFORE the viewport pin with a bounded + // shift. This removes one row from before our pin, which changes its + // absolute offset from the top by 1, but the cache is not invalidated. + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, 10); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded multi-page invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback, after the first page + const pin_y = page.capacity.rows + 1; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the beginning of history with a limit that spans + // across multiple pages. This ensures we hit the code path where + // eraseRowBounded finds the limit boundary in a subsequent page. + const limit = page.capacity.rows + 10; + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, limit); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded full page shift invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 4) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere well beyond + // the first two pages + const pin_y = 5; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the beginning of history with a limit that is + // larger than multiple full pages. This ensures we hit the code path + // where eraseRowBounded continues looping through entire pages, + // rotating all rows in each page until it reaches the limit or + // runs out of pages. + const limit = page.capacity.rows * 2 + 10; + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, limit); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded exhausts pages invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Our total rows should include history + const total_rows_before = s.totalRows(); + try testing.expect(total_rows_before > s.rows); + + // Scroll back to create a pinned viewport somewhere in the history, + // well after the erase will complete + const pin_y = page.capacity.rows * 2 + 10; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the beginning of history with a limit that is + // LARGER than all remaining pages combined. This ensures we exhaust + // all pages in the while loop and reach the cleanup code after the loop. + const limit = total_rows_before * 2; + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, limit); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); } test "PageList adjustCapacity to increase styles" { @@ -5156,8 +6166,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.node == s.pages.first.?); + try testing.expect(s.viewport == .top); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); @@ -5186,13 +6195,11 @@ 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.node == s.pages.first.?); + try testing.expect(s.viewport == .top); // 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.node == s.pages.first.?); + try testing.expect(s.viewport == .top); } test "PageList erase resets viewport to active if top is inside active" { @@ -6096,14 +7103,16 @@ test "PageList resize (no reflow) more rows contains viewport" { // Set viewport above active by scrolling up one. s.scroll(.{ .delta_row = -1 }); // The viewport should be a pin now. - try testing.expectEqual(Viewport.pin, s.viewport); + try testing.expectEqual(Viewport.top, s.viewport); // Resize try s.resize(.{ .rows = 7, .reflow = false }); try testing.expectEqual(@as(usize, 7), s.rows); try testing.expectEqual(@as(usize, 7), s.totalRows()); - // The viewport should now be active, not a pin. - try testing.expectEqual(Viewport.active, s.viewport); + + // Question: maybe the viewport should actually be in the active + // here and not pinned to the top. + try testing.expectEqual(Viewport.top, s.viewport); } test "PageList resize (no reflow) less cols" { @@ -6657,6 +7666,55 @@ test "PageList resize reflow more cols wrapped rows" { } } +test "PageList resize reflow invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 4, null); + defer s.deinit(); + try s.growRows(20); + + const page = &s.pages.last.?.data; + for (0..s.rows) |y| { + if (y % 2 == 0) { + const rac = page.getRowAndCell(0, y); + rac.row.wrap = true; + } else { + const rac = page.getRowAndCell(0, y); + rac.row.wrap_continuation = true; + } + + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + } + } + + // Scroll to a pinned viewport in history + const pin_y = 10; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Resize with reflow - unwrapping rows changes total_rows + try s.resize(.{ .cols = 4, .reflow = true }); + try testing.expectEqual(@as(usize, 4), s.cols); + + // Verify scrollbar cache was invalidated during reflow + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 8, + .len = s.rows, + }, s.scrollbar()); +} + test "PageList resize reflow more cols creates multiple pages" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 4498a5def..59b5d0d53 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -37,6 +37,7 @@ pub const Pin = PageList.Pin; pub const Point = point.Point; pub const Screen = @import("Screen.zig"); pub const ScreenType = Terminal.ScreenType; +pub const Scrollbar = PageList.Scrollbar; pub const Selection = @import("Selection.zig"); pub const SizeReportStyle = csi.SizeReportStyle; pub const StringMap = @import("StringMap.zig"); From 123d23682a4a2fe105c7148fb81ef824bc32af9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:43:22 -0700 Subject: [PATCH 286/319] build(deps): bump namespacelabs/nscloud-setup-buildx-action from 0.0.19 to 0.0.20 (#9227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [namespacelabs/nscloud-setup-buildx-action](https://github.com/namespacelabs/nscloud-setup-buildx-action) from 0.0.19 to 0.0.20.
Release notes

Sourced from namespacelabs/nscloud-setup-buildx-action's releases.

v0.0.20

What's Changed

New Contributors

Full Changelog: https://github.com/namespacelabs/nscloud-setup-buildx-action/compare/v0...v0.0.20

Commits
  • 91c2e65 Merge pull request #11 from namespacelabs/add-wait-for-builder-input-to-eager...
  • 459dd43 Add wait-for-builder input to eagerly start build clusters and wait for them
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=namespacelabs/nscloud-setup-buildx-action&package-manager=github_actions&previous-version=0.0.19&new-version=0.0.20)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6757db2ee..e92f30083 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1057,7 +1057,7 @@ jobs: uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@7020d7d8e659afecbfec162ab4693c7e56278311 # v0.0.19 + uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20 - name: Download Source Tarball Artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 From 3ffbd87a0fe5c58cc673663bcc7b38d2bc12ef30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:43:35 -0700 Subject: [PATCH 287/319] build(deps): bump cachix/install-nix-action from 31.8.0 to 31.8.1 (#9226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.8.0 to 31.8.1.
Release notes

Sourced from cachix/install-nix-action's releases.

v31.8.1

What's Changed

Full Changelog: https://github.com/cachix/install-nix-action/compare/v31...v31.8.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cachix/install-nix-action&package-manager=github_actions&previous-version=31.8.0&new-version=31.8.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 4 +- .github/workflows/test.yml | 48 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index b28dd4299..52190f020 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 5718a85bb..50a8ec7bb 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -89,7 +89,7 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index b4ef9e1f5..ee2fe47b3 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -34,7 +34,7 @@ jobs: with: # Important so that build number generation works fetch-depth: 0 - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -168,7 +168,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e92f30083..4f3196c3b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -113,7 +113,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -146,7 +146,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -180,7 +180,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -222,7 +222,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -258,7 +258,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -287,7 +287,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -320,7 +320,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -366,7 +366,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -585,7 +585,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -627,7 +627,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -675,7 +675,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -710,7 +710,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -774,7 +774,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -801,7 +801,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -829,7 +829,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -856,7 +856,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -883,7 +883,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -910,7 +910,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -937,7 +937,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -971,7 +971,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -998,7 +998,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1035,7 +1035,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1123,7 +1123,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 3d6c2ed1f..b218cdb26 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -29,7 +29,7 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0 + uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 From 5a9bd0e49ef11499eb0ccb63725cc882b93356e0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 20:04:36 -0700 Subject: [PATCH 288/319] snap: update to Zig 0.15.2 --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 271deeeb2..7bdbc9b48 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -52,7 +52,7 @@ parts: rm -rf $CRAFT_PART_SRC/* if [[ -n $arch ]]; then - curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.15.1/zig-$arch-linux-0.15.1.tar.xz + curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.15.2/zig-$arch-linux-0.15.2.tar.xz else echo "Unsupported arch" exit 1 From da7736cd443f60649b54e87adc9c20c0ec89c13e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 21:03:25 -0700 Subject: [PATCH 289/319] renderer: emit scrollbar apprt event when scrollbar changes --- include/ghostty.h | 9 +++++++++ src/Surface.zig | 13 +++++++++++++ src/apprt/action.zig | 4 ++++ src/apprt/surface.zig | 3 +++ src/renderer/generic.zig | 9 +++++++++ src/terminal/PageList.zig | 15 +++++++++++++++ 6 files changed, 53 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index 48836ee96..acb6988d6 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -741,6 +741,13 @@ typedef struct { uint64_t duration; } ghostty_action_command_finished_s; +// terminal.Scrollbar +typedef struct { + uint64_t total; + uint64_t offset; + uint64_t len; +} ghostty_action_scrollbar_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -767,6 +774,7 @@ typedef enum { GHOSTTY_ACTION_RESET_WINDOW_SIZE, GHOSTTY_ACTION_INITIAL_SIZE, GHOSTTY_ACTION_CELL_SIZE, + GHOSTTY_ACTION_SCROLLBAR, GHOSTTY_ACTION_RENDER, GHOSTTY_ACTION_INSPECTOR, GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, @@ -809,6 +817,7 @@ typedef union { ghostty_action_size_limit_s size_limit; ghostty_action_initial_size_s initial_size; ghostty_action_cell_size_s cell_size; + ghostty_action_scrollbar_s scrollbar; ghostty_action_inspector_e inspector; ghostty_action_desktop_notification_s desktop_notification; ghostty_action_set_title_s set_title; diff --git a/src/Surface.zig b/src/Surface.zig index 456acad2c..a9052896f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -983,6 +983,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .renderer_health => |health| self.updateRendererHealth(health), + .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), + .report_color_scheme => |force| self.reportColorScheme(force), .present_surface => try self.presentSurface(), @@ -1459,6 +1461,17 @@ fn updateRendererHealth(self: *Surface, health: rendererpkg.Health) void { }; } +/// Called when the scrollbar state changes. +fn updateScrollbar(self: *Surface, scrollbar: terminal.Scrollbar) void { + _ = self.rt_app.performAction( + .{ .surface = self }, + .scrollbar, + scrollbar, + ) catch |err| { + log.warn("failed to notify app of scrollbar change err={}", .{err}); + }; +} + /// This should be called anytime `config_conditional_state` changes /// so that the apprt can reload the configuration. fn notifyConfigConditionalState(self: *Surface) void { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 14a8165f2..e593d4bce 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -164,6 +164,9 @@ pub const Action = union(Key) { /// The cell size has changed to the given dimensions in pixels. cell_size: CellSize, + /// The scrollbar is updating. + scrollbar: terminal.Scrollbar, + /// The target should be re-rendered. This usually has a specific /// surface target but if the app is targeted then all active /// surfaces should be redrawn. @@ -324,6 +327,7 @@ pub const Action = union(Key) { reset_window_size, initial_size, cell_size, + scrollbar, render, inspector, show_gtk_inspector, diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index a46732c16..b71bf1e6e 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -104,6 +104,9 @@ pub const Message = union(enum) { /// of the command. stop_command: ?u8, + /// The scrollbar state changed for the surface. + scrollbar: terminal.Scrollbar, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 876e3f945..6031bede4 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1314,6 +1314,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + // After the graphics API is complete (so we defer) we want to + // update our scrollbar state. + defer if (self.scrollbar_dirty) { + self.scrollbar_dirty = false; + _ = self.surface_mailbox.push(.{ + .scrollbar = self.scrollbar, + }, .{ .forever = {} }); + }; + // Let our graphics API do any bookkeeping, etc. // that it needs to do before / after `drawFrame`. self.api.drawFrameStart(); diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index b71c87faa..058314166 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2110,6 +2110,21 @@ pub const Scrollbar = struct { .len = 0, }; + // Sync with: ghostty_action_scrollbar_s + pub const C = extern struct { + total: u64, + offset: u64, + len: u64, + }; + + pub fn cval(self: Scrollbar) C { + return .{ + .total = @intCast(self.total), + .offset = @intCast(self.offset), + .len = @intCast(self.len), + }; + } + /// Comparison for scrollbars. pub fn eql(self: Scrollbar, other: Scrollbar) bool { return self.total == other.total and From 135136f733bec586cb08cbf3073f83e98e679868 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Oct 2025 08:38:00 -0700 Subject: [PATCH 290/319] terminal: PageList scroll to absolute row function --- src/terminal/PageList.zig | 543 +++++++++++++++++++++++++++++++++++++- src/terminal/Screen.zig | 2 + 2 files changed, 531 insertions(+), 14 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 058314166..3aba29128 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1886,6 +1886,11 @@ pub const Scroll = union(enum) { /// the scrollback history. top, + /// Scroll to the given absolute row from the top. A value of zero + /// is the top row. This row will be the first visible row in the viewport. + /// Scrolling into or below the active area will clamp to the active area. + row: usize, + /// Scroll up (negative) or down (positive) by the given number of /// rows. This is clamped to the "top" and "active" top left. delta_row: isize, @@ -1904,6 +1909,8 @@ pub const Scroll = union(enum) { /// pages, etc. This can only be used to move the viewport within the /// previously allocated pages. pub fn scroll(self: *PageList, behavior: Scroll) void { + defer self.assertIntegrity(); + switch (behavior) { .active => self.viewport = .active, .top => self.viewport = .top, @@ -1920,6 +1927,93 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { self.viewport = .pin; self.viewport_pin_row_offset = null; // invalidate cache }, + .row => |n| row: { + // If we're at the top, pin the top. + if (n == 0) { + self.viewport = .top; + break :row; + } + + // If we're below the top of the active area, pin the active area. + if (n >= self.total_rows - self.rows) { + self.viewport = .active; + break :row; + } + + // See if there are any other faster paths we can take. + switch (self.viewport) { + .top, .active => {}, + .pin => if (self.viewport_pin_row_offset) |*v| { + // If we have a pin and we already calculated a row offset, + // then we can efficiently calculate the delta and move + // that much from that pin. + const delta: isize = delta: { + const n_isize: isize = @intCast(n); + const v_isize: isize = @intCast(v.*); + break :delta n_isize - v_isize; + }; + self.scroll(.{ .delta_row = delta }); + return; + }, + } + + // We have an accurate row offset so store it to prevent + // calculating this again. + self.viewport_pin_row_offset = n; + self.viewport = .pin; + + // Slow path, we've just got to traverse the linked list and + // get to our row. As a slight speedup, let's pick the traversal + // that's likely faster based on our absolute row and total rows. + const midpoint = self.total_rows / 2; + if (n < midpoint) { + // Iterate forward from the first node. + var node_it = self.pages.first; + var rem: size.CellCountInt = std.math.cast( + size.CellCountInt, + n, + ) orelse { + self.viewport = .active; + break :row; + }; + while (node_it) |node| : (node_it = node.next) { + if (rem < node.data.size.rows) { + self.viewport_pin.* = .{ + .node = node, + .y = rem, + }; + break :row; + } + + rem -= node.data.size.rows; + } + } else { + // Iterate backwards from the last node. + var node_it = self.pages.last; + var rem: size.CellCountInt = std.math.cast( + size.CellCountInt, + self.total_rows - n, + ) orelse { + self.viewport = .active; + break :row; + }; + while (node_it) |node| : (node_it = node.prev) { + if (rem <= node.data.size.rows) { + self.viewport_pin.* = .{ + .node = node, + .y = node.data.size.rows - rem, + }; + break :row; + } + + rem -= node.data.size.rows; + } + } + + // If we reached here, then we couldn't find the offset. + // This feels impossible? Just clamp to active, screw it lol. + self.viewport = .active; + }, .delta_prompt => |n| self.scrollPrompt(n), .delta_row => |n| delta_row: { switch (self.viewport) { @@ -5049,6 +5143,427 @@ test "PageList scroll to pin at top" { } } +test "PageList scroll to row 0" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + s.scroll(.{ .row = 0 }); + try testing.expect(s.viewport == .top); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row in scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(20); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } + + s.scroll(.{ .row = 5 }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 5, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 5, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row in middle" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(50); + + const total = s.total_rows; + const midpoint = total / 2; + s.scroll(.{ .row = midpoint }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = midpoint, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = @as(size.CellCountInt, @intCast(midpoint)), + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = @as(size.CellCountInt, @intCast(midpoint)), + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = midpoint, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row at active boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(20); + + const active_start = s.total_rows - s.rows; + + s.scroll(.{ .row = active_start }); + + try testing.expect(s.viewport == .active); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = @as(size.CellCountInt, @intCast(active_start)), + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); + + try s.growRows(10); + + try testing.expect(s.viewport == .active); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row beyond active" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .row = 1000 }); + + try testing.expect(s.viewport == .active); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row without scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + s.scroll(.{ .row = 5 }); + + try testing.expect(s.viewport == .active); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row then delta" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(30); + + s.scroll(.{ .row = 10 }); + + try testing.expect(s.viewport == .pin); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 10, + .len = s.rows, + }, s.scrollbar()); + + s.scroll(.{ .delta_row = 5 }); + + try testing.expect(s.viewport == .pin); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 15, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 15, + .len = s.rows, + }, s.scrollbar()); + + s.scroll(.{ .delta_row = -3 }); + + try testing.expect(s.viewport == .pin); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 12, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 12, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row with cache fast path down" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(50); + + s.scroll(.{ .row = 10 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 10, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + // Verify cache is populated + try testing.expect(s.viewport_pin_row_offset != null); + try testing.expectEqual(@as(usize, 10), s.viewport_pin_row_offset.?); + + // Now scroll to a different row - this should use the fast path + s.scroll(.{ .row = 20 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 20, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 20, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row with cache fast path up" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(50); + + s.scroll(.{ .row = 30 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 30, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 30, + } }, pt); + } + + // Verify cache is populated + try testing.expect(s.viewport_pin_row_offset != null); + try testing.expectEqual(@as(usize, 30), s.viewport_pin_row_offset.?); + + // Now scroll up to a different row - this should use the fast path + s.scroll(.{ .row = 15 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 15, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 15, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 15, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 15, + .len = s.rows, + }, s.scrollbar()); +} + test "PageList scroll clear" { const testing = std.testing; const alloc = testing.allocator; @@ -5104,7 +5619,7 @@ test "PageList: jump zero prompts" { try testing.expect(s.viewport == .active); try testing.expectEqual(Scrollbar{ - .total = s.totalRows(), + .total = s.total_rows, .offset = s.total_rows - s.rows, .len = s.rows, }, s.scrollbar()); @@ -5138,7 +5653,7 @@ test "Screen: jump back one prompt" { } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); try testing.expectEqual(Scrollbar{ - .total = s.totalRows(), + .total = s.total_rows, .offset = 1, .len = s.rows, }, s.scrollbar()); @@ -5152,7 +5667,7 @@ test "Screen: jump back one prompt" { } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); try testing.expectEqual(Scrollbar{ - .total = s.totalRows(), + .total = s.total_rows, .offset = 1, .len = s.rows, }, s.scrollbar()); @@ -5163,7 +5678,7 @@ test "Screen: jump back one prompt" { s.scroll(.{ .delta_prompt = 1 }); try testing.expect(s.viewport == .active); try testing.expectEqual(Scrollbar{ - .total = s.totalRows(), + .total = s.total_rows, .offset = s.total_rows - s.rows, .len = s.rows, }, s.scrollbar()); @@ -5172,7 +5687,7 @@ test "Screen: jump back one prompt" { s.scroll(.{ .delta_prompt = 1 }); try testing.expect(s.viewport == .active); try testing.expectEqual(Scrollbar{ - .total = s.totalRows(), + .total = s.total_rows, .offset = s.total_rows - s.rows, .len = s.rows, }, s.scrollbar()); @@ -6042,11 +6557,11 @@ test "PageList erase" { try testing.expectEqual(@as(usize, 6), s.totalPages()); // Our total rows should be large - try testing.expect(s.totalRows() > s.rows); + try testing.expect(s.total_rows > s.rows); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // We should be back to just one page try testing.expectEqual(@as(usize, 1), s.totalPages()); @@ -6101,7 +6616,7 @@ test "PageList erase row with tracked pin resets to top-left" { cur_page.data.pauseIntegrityChecks(false); // Our total rows should be large - try testing.expect(s.totalRows() > s.rows); + try testing.expect(s.total_rows > s.rows); // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .history = .{} }).?); @@ -6109,7 +6624,7 @@ test "PageList erase row with tracked pin resets to top-left" { // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p.node); @@ -6130,7 +6645,7 @@ test "PageList erase row with tracked pin shifts" { // Erase only a few rows in our active s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 3 } }); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p.node); @@ -6151,7 +6666,7 @@ test "PageList erase row with tracked pin is erased" { // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 3 } }); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p.node); @@ -6180,7 +6695,7 @@ test "PageList erase resets viewport to active if moves within active" { cur_page.data.pauseIntegrityChecks(false); // Move our viewport to the top - s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); + s.scroll(.{ .delta_row = -@as(isize, @intCast(s.total_rows)) }); try testing.expect(s.viewport == .top); // Erase the entire history, we should be back to just our active set. @@ -6209,7 +6724,7 @@ test "PageList erase resets viewport if inside erased page but not active" { cur_page.data.pauseIntegrityChecks(false); // Move our viewport to the top - s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); + s.scroll(.{ .delta_row = -@as(isize, @intCast(s.total_rows)) }); try testing.expect(s.viewport == .top); // Erase the entire history, we should be back to just our active set. @@ -6275,7 +6790,7 @@ test "PageList erase a one-row active" { } s.eraseRows(.{ .active = .{} }, .{ .active = .{} }); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // The row should be empty { diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 228b87922..81d6d4ab6 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1155,6 +1155,7 @@ pub const Scroll = union(enum) { active, top, pin: Pin, + row: usize, delta_row: isize, delta_prompt: isize, }; @@ -1174,6 +1175,7 @@ pub inline fn scroll(self: *Screen, behavior: Scroll) void { .active => self.pages.scroll(.{ .active = {} }), .top => self.pages.scroll(.{ .top = {} }), .pin => |p| self.pages.scroll(.{ .pin = p }), + .row => |v| self.pages.scroll(.{ .row = v }), .delta_row => |v| self.pages.scroll(.{ .delta_row = v }), .delta_prompt => |v| self.pages.scroll(.{ .delta_prompt = v }), } From c86266cd906d3ac9972012699925e9ee395203aa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Oct 2025 08:22:52 -0700 Subject: [PATCH 291/319] input: scroll_to_row action --- src/Surface.zig | 25 ++++++++++++++++++++----- src/input/Binding.zig | 5 +++++ src/input/command.zig | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index a9052896f..e75c4b409 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4827,12 +4827,27 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, .unlocked); }, + .scroll_to_row => |n| { + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t: *terminal.Terminal = self.renderer_state.terminal; + t.screen.scroll(.{ .row = n }); + } + + try self.queueRender(); + }, + .scroll_to_selection => { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - const sel = self.io.terminal.screen.selection orelse return false; - const tl = sel.topLeft(&self.io.terminal.screen); - self.io.terminal.screen.scroll(.{ .pin = tl }); + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const sel = self.io.terminal.screen.selection orelse return false; + const tl = sel.topLeft(&self.io.terminal.screen); + self.io.terminal.screen.scroll(.{ .pin = tl }); + } + + try self.queueRender(); }, .scroll_page_up => { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index c44fb0b09..ad07dce55 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -347,6 +347,10 @@ pub const Action = union(enum) { /// Scroll to the selected text. scroll_to_selection, + /// Scroll to the given absolute row in the screen with 0 being + /// the first row. + scroll_to_row: usize, + /// Scroll the screen up by one page. scroll_page_up, @@ -1077,6 +1081,7 @@ pub const Action = union(enum) { .scroll_to_top, .scroll_to_bottom, .scroll_to_selection, + .scroll_to_row, .scroll_page_up, .scroll_page_down, .scroll_page_fractional, diff --git a/src/input/command.zig b/src/input/command.zig index ba55820fc..29c10527e 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -487,6 +487,7 @@ fn actionCommands(action: Action.Key) []const Command { .esc, .cursor_key, .set_font_size, + .scroll_to_row, .scroll_page_fractional, .scroll_page_lines, .adjust_selection, From 2937aff513f81f97ca2635735d6346a21329df37 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Oct 2025 09:18:55 -0700 Subject: [PATCH 292/319] gtk: mark scrollbar as unimplemented --- src/apprt/gtk/class/application.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index d75a0ef7f..d0125d1eb 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -728,6 +728,7 @@ pub const Application = extern struct { .command_finished => return Action.commandFinished(target, value), // Unimplemented + .scrollbar, .secure_input, .close_all_windows, .float_window, From 7207ff08d586d96b1d95872fe397986cd84fe977 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 15 Oct 2025 20:45:30 -0700 Subject: [PATCH 293/319] macos: SurfaceScrollView --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + macos/Sources/Ghostty/Ghostty.Action.swift | 12 + macos/Sources/Ghostty/Ghostty.App.swift | 30 +++ macos/Sources/Ghostty/Package.swift | 4 + macos/Sources/Ghostty/SurfaceScrollView.swift | 229 ++++++++++++++++++ macos/Sources/Ghostty/SurfaceView.swift | 22 +- 6 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 macos/Sources/Ghostty/SurfaceScrollView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 388122f62..ae0051c53 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -141,6 +141,7 @@ Ghostty/Ghostty.Surface.swift, Ghostty/InspectorView.swift, "Ghostty/NSEvent+Extension.swift", + Ghostty/SurfaceScrollView.swift, Ghostty/SurfaceView_AppKit.swift, Helpers/AppInfo.swift, Helpers/CodableBridge.swift, diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 37b1a362d..4921ef8df 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -100,6 +100,18 @@ extension Ghostty.Action { let state: State let progress: UInt8? } + + struct Scrollbar { + let total: UInt64 + let offset: UInt64 + let len: UInt64 + + init(c: ghostty_action_scrollbar_s) { + total = c.total + offset = c.offset + len = c.len + } + } } // Putting the initializer in an extension preserves the automatic one. diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index bf34b4a91..91829f95c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -571,6 +571,9 @@ extension Ghostty { case GHOSTTY_ACTION_REDO: return redo(app, target: target) + case GHOSTTY_ACTION_SCROLLBAR: + scrollbar(app, target: target, v: action.action.scrollbar) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -1560,6 +1563,33 @@ extension Ghostty { } } + private static func scrollbar( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_scrollbar_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("scrollbar does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + + let scrollbar = Ghostty.Action.Scrollbar(c: v) + NotificationCenter.default.post( + name: .ghosttyDidUpdateScrollbar, + object: surfaceView, + userInfo: [ + SwiftUI.Notification.Name.ScrollbarKey: scrollbar + ] + ) + + default: + assertionFailure() + } + } + private static func configReload( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 85040d390..e8a3d0976 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -344,6 +344,10 @@ extension Notification.Name { /// Toggle maximize of current window static let ghosttyMaximizeDidToggle = Notification.Name("com.mitchellh.ghostty.maximizeDidToggle") + + /// Notification sent when scrollbar updates + static let ghosttyDidUpdateScrollbar = Notification.Name("com.mitchellh.ghostty.didUpdateScrollbar") + static let ScrollbarKey = ghosttyDidUpdateScrollbar.rawValue + ".scrollbar" } // NOTE: I am moving all of these to Notification.Name extensions over time. This diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift new file mode 100644 index 000000000..642d728d9 --- /dev/null +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -0,0 +1,229 @@ +import SwiftUI + +/// Wraps a Ghostty surface view in an NSScrollView to provide native macOS scrollbar support. +/// +/// ## Coordinate System +/// AppKit uses a +Y-up coordinate system (origin at bottom-left), while terminals conceptually +/// use +Y-down (row 0 at top). This class handles the inversion when converting between row +/// offsets and pixel positions. +/// +/// ## Architecture +/// - `scrollView`: The outermost NSScrollView that manages scrollbar rendering and behavior +/// - `documentView`: A blank NSView whose height represents total scrollback (in pixels) +/// - `surfaceView`: The actual Ghostty renderer, positioned to fill the visible rect +class SurfaceScrollView: NSView { + private let scrollView: NSScrollView + private let documentView: NSView + private let surfaceView: Ghostty.SurfaceView + private var observers: [NSObjectProtocol] = [] + private var isLiveScrolling = false + + /// The last row position sent via scroll_to_row action. Used to avoid + /// sending redundant actions when the user drags the scrollbar but stays + /// on the same row. + private var lastSentRow: Int? + + init(contentSize: CGSize, surfaceView: Ghostty.SurfaceView) { + self.surfaceView = surfaceView + // The scroll view is our outermost view that controls all our scrollbar + // rendering and behavior. + scrollView = NSScrollView() + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = false + scrollView.autohidesScrollers = true + scrollView.usesPredominantAxisScrolling = true + + // The document view is what the scrollview is actually going + // to be directly scrolling. We set it up to a "blank" NSView + // with the desired content size. + documentView = NSView(frame: NSRect(origin: .zero, size: contentSize)) + scrollView.documentView = documentView + + // The document view contains our actual surface as a child. + // We synchronize the scrolling of the document with this surface + // so that our primary Ghostty renderer only needs to render the viewport. + documentView.addSubview(surfaceView) + + super.init(frame: .zero) + + // Our scroll view is our only view + addSubview(scrollView) + + // We listen for scroll events through bounds notifications on our NSClipView. + // This is based on: https://christiantietze.de/posts/2018/07/synchronize-nsscrollview/ + scrollView.contentView.postsBoundsChangedNotifications = true + observers.append(NotificationCenter.default.addObserver( + forName: NSView.boundsDidChangeNotification, + object: scrollView.contentView, + queue: .main + ) { [weak self] notification in + self?.handleScrollChange(notification) + }) + + // Listen for scrollbar updates from Ghostty + observers.append(NotificationCenter.default.addObserver( + forName: .ghosttyDidUpdateScrollbar, + object: surfaceView, + queue: .main + ) { [weak self] notification in + self?.handleScrollbarUpdate(notification) + }) + + // Listen for live scroll events + observers.append(NotificationCenter.default.addObserver( + forName: NSScrollView.willStartLiveScrollNotification, + object: scrollView, + queue: .main + ) { [weak self] _ in + self?.isLiveScrolling = true + }) + + observers.append(NotificationCenter.default.addObserver( + forName: NSScrollView.didEndLiveScrollNotification, + object: scrollView, + queue: .main + ) { [weak self] _ in + self?.isLiveScrolling = false + }) + + observers.append(NotificationCenter.default.addObserver( + forName: NSScrollView.didLiveScrollNotification, + object: scrollView, + queue: .main + ) { [weak self] _ in + self?.handleLiveScroll() + }) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) not implemented") + } + + deinit { + observers.forEach { NotificationCenter.default.removeObserver($0) } + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + + // Force layout to be called to fix up our various subviews. + needsLayout = true + } + + override func layout() { + super.layout() + + // Fill entire bounds with scroll view + scrollView.frame = bounds + + // Use contentSize to account for visible scrollers + // + // Only update sizes if we have a valid (non-zero) content size. The content size + // can be zero when this is added early to a view, or to an invisible hierarchy. + // Practically, this happened in the quick terminal. + let contentSize = scrollView.contentSize + if contentSize.width > 0 && contentSize.height > 0 { + // Keep document width synchronized with content width + documentView.setFrameSize(CGSize( + width: contentSize.width, + height: documentView.frame.height + )) + + // Inform the actual pty of our size change + surfaceView.sizeDidChange(contentSize) + } + + // When our scrollview changes make sure our surface view is synchronized + synchronizeSurfaceView() + } + + // MARK: Scrolling + + private func synchronizeAppearance() { + let scrollbarConfig = surfaceView.derivedConfig.scrollbar + scrollView.hasVerticalScroller = scrollbarConfig != .never + } + + /// Positions the surface view to fill the currently visible rectangle. + /// + /// This is called whenever the scroll position changes. The surface view (which does the + /// actual terminal rendering) always fills exactly the visible portion of the document view, + /// so the renderer only needs to render what's currently on screen. + private func synchronizeSurfaceView() { + let visibleRect = scrollView.contentView.documentVisibleRect + surfaceView.frame = visibleRect + } + + // MARK: Notifications + + /// Handles bounds changes in the scroll view's clip view, keeping the surface view synchronized. + private func handleScrollChange(_ notification: Notification) { + synchronizeSurfaceView() + } + + /// Handles live scroll events (user actively dragging the scrollbar). + /// + /// Converts the current scroll position to a row number and sends a `scroll_to_row` action + /// to the terminal core. Only sends actions when the row changes to avoid IPC spam. + private func handleLiveScroll() { + // If our cell height is currently zero then we avoid a div by zero below + // and just don't scroll (there's no where to scroll anyways). This can + // happen with a tiny terminal. + let cellHeight = surfaceView.cellSize.height + guard cellHeight > 0 else { return } + + // AppKit views are +Y going up, so we calculate from the bottom + let visibleRect = scrollView.contentView.documentVisibleRect + let documentHeight = documentView.frame.height + let scrollOffset = documentHeight - visibleRect.origin.y - visibleRect.height + let row = Int(scrollOffset / cellHeight) + + // Only send action if the row changed to avoid action spam + guard row != lastSentRow else { return } + lastSentRow = row + + // Use the keybinding action to scroll. + _ = surfaceView.surfaceModel?.perform(action: "scroll_to_row:\(row)") + } + + /// Handles scrollbar state updates from the terminal core. + /// + /// Updates the document view size to reflect total scrollback and adjusts scroll position + /// to match the terminal's viewport. During live scrolling, updates document size but skips + /// programmatic position changes to avoid fighting the user's drag. + /// + /// ## Scrollbar State + /// The scrollbar struct contains: + /// - `total`: Total rows in scrollback + active area + /// - `offset`: First visible row (0 = top of history) + /// - `len`: Number of visible rows (viewport height) + private func handleScrollbarUpdate(_ notification: Notification) { + guard let scrollbar = notification.userInfo?[SwiftUI.Notification.Name.ScrollbarKey] as? Ghostty.Action.Scrollbar else { + return + } + + // Convert row units to pixels using cell height, ignore zero height. + let cellHeight = surfaceView.cellSize.height + guard cellHeight > 0 else { return } + + // Our width should be the content width to account for visible scrollers. + // We don't do horizontal scrolling in terminals. + let totalHeight = CGFloat(scrollbar.total) * cellHeight + let newSize = CGSize(width: scrollView.contentSize.width, height: totalHeight) + documentView.setFrameSize(newSize) + + // Only update our actual scroll position if we're not actively scrolling. + if !isLiveScrolling { + // Invert coordinate system: terminal offset is from top, AppKit position from bottom + let offsetY = CGFloat(scrollbar.total - scrollbar.offset - scrollbar.len) * cellHeight + scrollView.contentView.scroll(to: CGPoint(x: 0, y: offsetY)) + + // Track the current row position to avoid redundant movements when we + // move the scrollbar. + lastSentRow = Int(scrollbar.offset) + } + + // Always update our scrolled view with the latest dimensions + scrollView.reflectScrolledClipView(scrollView.contentView) + } +} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index aca17c0fc..c650bdf8f 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -386,10 +386,6 @@ extension Ghostty { /// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn /// and interacted with. The word "surface" is used because a surface may represent a window, a tab, /// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it. - /// - /// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible - /// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to - /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. struct SurfaceRepresentable: OSViewRepresentable { /// The view to render for the terminal surface. let view: SurfaceView @@ -404,16 +400,26 @@ extension Ghostty { /// The best approach is to wrap this view in a GeometryReader and pass in the geo.size. let size: CGSize + #if canImport(AppKit) + func makeOSView(context: Context) -> SurfaceScrollView { + // On macOS, wrap the surface view in a scroll view + return SurfaceScrollView(contentSize: size, surfaceView: view) + } + + func updateOSView(_ scrollView: SurfaceScrollView, context: Context) { + // Our scrollview always takes up the full size. + scrollView.frame.size = size + } + #else func makeOSView(context: Context) -> SurfaceView { - // We need the view as part of the state to be created previously because - // the view is sent to the Ghostty API so that it can manipulate it - // directly since we draw on a render thread. - return view; + // On iOS, return the surface view directly + return view } func updateOSView(_ view: SurfaceView, context: Context) { view.sizeDidChange(size) } + #endif } /// The configuration for a surface. For any configuration not set, defaults will be chosen from From 4b34b2389a38159de8adf3abdc249829a773e944 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Oct 2025 09:49:03 -0700 Subject: [PATCH 294/319] config: add `scrollbar` config to control when scrollbars appear --- macos/Sources/Ghostty/Ghostty.Config.swift | 16 +++++++++++++ macos/Sources/Ghostty/SurfaceScrollView.swift | 16 ++++++++++++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 3 +++ src/config/Config.zig | 24 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 0d75922cb..f380345c7 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -603,6 +603,17 @@ extension Ghostty { let str = String(cString: ptr) return MacShortcuts(rawValue: str) ?? defaultValue } + + var scrollbar: Scrollbar { + let defaultValue = Scrollbar.system + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "scrollbar" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return Scrollbar(rawValue: str) ?? defaultValue + } } } @@ -641,6 +652,11 @@ extension Ghostty.Config { case ask } + enum Scrollbar: String { + case system + case never + } + enum ResizeOverlay : String { case always case never diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift index 642d728d9..44003e85f 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Combine /// Wraps a Ghostty surface view in an NSScrollView to provide native macOS scrollbar support. /// @@ -16,6 +17,7 @@ class SurfaceScrollView: NSView { private let documentView: NSView private let surfaceView: Ghostty.SurfaceView private var observers: [NSObjectProtocol] = [] + private var cancellables: Set = [] private var isLiveScrolling = false /// The last row position sent via scroll_to_row action. Used to avoid @@ -28,7 +30,7 @@ class SurfaceScrollView: NSView { // The scroll view is our outermost view that controls all our scrollbar // rendering and behavior. scrollView = NSScrollView() - scrollView.hasVerticalScroller = true + scrollView.hasVerticalScroller = false scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = true scrollView.usesPredominantAxisScrolling = true @@ -49,6 +51,9 @@ class SurfaceScrollView: NSView { // Our scroll view is our only view addSubview(scrollView) + // Apply initial scrollbar settings + synchronizeAppearance() + // We listen for scroll events through bounds notifications on our NSClipView. // This is based on: https://christiantietze.de/posts/2018/07/synchronize-nsscrollview/ scrollView.contentView.postsBoundsChangedNotifications = true @@ -93,6 +98,15 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.handleLiveScroll() }) + + // Listen for derived config changes to update scrollbar settings live + surfaceView.$derivedConfig + .sink { [weak self] _ in + DispatchQueue.main.async { [weak self] in + self?.synchronizeAppearance() + } + } + .store(in: &cancellables) } required init?(coder: NSCoder) { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 2b3fd261c..410646f6f 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1532,6 +1532,7 @@ extension Ghostty { let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? + let scrollbar: Ghostty.Config.Scrollbar init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) @@ -1539,6 +1540,7 @@ extension Ghostty { self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil + self.scrollbar = .system } init(_ config: Ghostty.Config) { @@ -1547,6 +1549,7 @@ extension Ghostty { self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) + self.scrollbar = config.scrollbar } } diff --git a/src/config/Config.zig b/src/config/Config.zig index bb10ff439..b3085c4c4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1197,6 +1197,24 @@ input: RepeatableReadableIO = .{}, /// This can be changed at runtime but will only affect new terminal surfaces. @"scrollback-limit": usize = 10_000_000, // 10MB +/// Control when the scrollbar is shown to scroll the scrollback buffer. +/// +/// The default value is `system`. +/// +/// Valid values: +/// +/// * `system` - Respect the system settings for when to show scrollbars. +/// For example, on macOS, this will respect the "Scrollbar behavior" +/// system setting which by default usually only shows scrollbars while +/// actively scrolling or hovering the gutter. +/// +/// * `never` - Never show a scrollbar. You can still scroll using the mouse, +/// keybind actions, etc. but you will not have a visual UI widget showing +/// a scrollbar. +/// +/// This only applies to macOS currently. GTK doesn't yet support scrollbars. +scrollbar: Scrollbar = .system, + /// Match a regular expression against the terminal text and associate clicking /// it with an action. This can be used to match URLs, file paths, etc. Actions /// can be opening using the system opener (e.g. `open` or `xdg-open`) or @@ -8379,6 +8397,12 @@ pub const WindowPadding = struct { } }; +/// See scrollbar +pub const Scrollbar = enum { + system, + never, +}; + /// See scroll-to-bottom pub const ScrollToBottom = packed struct { keystroke: bool = true, From cc91e2ad1679b730ed8f174c35fa4d09eac77632 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 16 Oct 2025 23:52:07 +0200 Subject: [PATCH 295/319] macOS: remove background from SurfaceScrollView --- macos/Sources/Ghostty/SurfaceScrollView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift index 44003e85f..7bec9249f 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -34,6 +34,8 @@ class SurfaceScrollView: NSView { scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = true scrollView.usesPredominantAxisScrolling = true + // hide default background to show blur effect properly + scrollView.drawsBackground = false // The document view is what the scrollview is actually going // to be directly scrolling. We set it up to a "blank" NSView From ffead466c7f40e7602963f7ed2c9d7176770276c Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 16 Oct 2025 22:32:57 -0700 Subject: [PATCH 296/319] Remove hidden titlebar safe area for SurfaceScrollView --- macos/Sources/Ghostty/SurfaceScrollView.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift index 7bec9249f..af15a71fb 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -118,7 +118,12 @@ class SurfaceScrollView: NSView { deinit { observers.forEach { NotificationCenter.default.removeObserver($0) } } - + + // The entire bounds is a safe area, so we override any default + // insets. This is necessary for the content view to match the + // surface view if we have the "hidden" titlebar style. + override var safeAreaInsets: NSEdgeInsets { return NSEdgeInsetsZero } + override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) From 51b2374616e94b653242523d6399c259c409e881 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 17 Oct 2025 00:06:00 -0700 Subject: [PATCH 297/319] Add window padding to scrollView document height --- macos/Sources/Ghostty/SurfaceScrollView.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift index af15a71fb..714227cd1 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -226,11 +226,17 @@ class SurfaceScrollView: NSView { // Convert row units to pixels using cell height, ignore zero height. let cellHeight = surfaceView.cellSize.height guard cellHeight > 0 else { return } - + + // The full document height must include the vertical padding around the cell + // grid, otherwise the content view ends up misaligned with the surface. + let documentGridHeight = CGFloat(scrollbar.total) * cellHeight + let gridHeight = CGFloat(scrollbar.len) * cellHeight + let padding = scrollView.contentSize.height - gridHeight + let documentHeight = documentGridHeight + padding + // Our width should be the content width to account for visible scrollers. // We don't do horizontal scrolling in terminals. - let totalHeight = CGFloat(scrollbar.total) * cellHeight - let newSize = CGSize(width: scrollView.contentSize.width, height: totalHeight) + let newSize = CGSize(width: scrollView.contentSize.width, height: documentHeight) documentView.setFrameSize(newSize) // Only update our actual scroll position if we're not actively scrolling. From ed443bc6ed45fb26e50ff750290992e3e2c609d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Oct 2025 15:49:30 -0700 Subject: [PATCH 298/319] gtk: Scrollbars --- src/apprt/gtk/build/gresource.zig | 1 + src/apprt/gtk/class/application.zig | 13 +- src/apprt/gtk/class/split_tree.zig | 5 +- src/apprt/gtk/class/surface.zig | 229 ++++++++++++++++++ .../gtk/class/surface_scrolled_window.zig | 209 ++++++++++++++++ src/apprt/gtk/ui/1.2/surface.blp | 1 + .../gtk/ui/1.5/surface-scrolled-window.blp | 11 + 7 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 src/apprt/gtk/class/surface_scrolled_window.zig create mode 100644 src/apprt/gtk/ui/1.5/surface-scrolled-window.blp diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index fabd5763e..cc701d7c2 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -46,6 +46,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 5, .name = "split-tree" }, .{ .major = 1, .minor = 5, .name = "split-tree-split" }, .{ .major = 1, .minor = 2, .name = "surface" }, + .{ .major = 1, .minor = 5, .name = "surface-scrolled-window" }, .{ .major = 1, .minor = 5, .name = "surface-title-dialog" }, .{ .major = 1, .minor = 3, .name = "surface-child-exited" }, .{ .major = 1, .minor = 5, .name = "tab" }, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index d0125d1eb..ceea6fee5 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -709,6 +709,8 @@ pub const Application = extern struct { .ring_bell => Action.ringBell(target), + .scrollbar => Action.scrollbar(target, value), + .set_title => Action.setTitle(target, value), .show_child_exited => return Action.showChildExited(target, value), @@ -728,7 +730,6 @@ pub const Application = extern struct { .command_finished => return Action.commandFinished(target, value), // Unimplemented - .scrollbar, .secure_input, .close_all_windows, .float_window, @@ -2328,6 +2329,16 @@ const Action = struct { } } + pub fn scrollbar( + target: apprt.Target, + value: apprt.Action.Value(.scrollbar), + ) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.surface.setScrollbar(value), + } + } + pub fn setTitle( target: apprt.Target, value: apprt.action.SetTitle, diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index a498ca5dc..1c901b1bb 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -22,6 +22,7 @@ const Config = @import("config.zig").Config; const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const Surface = @import("surface.zig").Surface; +const SurfaceScrolledWindow = @import("surface_scrolled_window.zig").SurfaceScrolledWindow; const log = std.log.scoped(.gtk_ghostty_split_tree); @@ -874,7 +875,9 @@ pub const SplitTree = extern struct { current: Surface.Tree.Node.Handle, ) *gtk.Widget { return switch (tree.nodes[current.idx()]) { - .leaf => |v| v.as(gtk.Widget), + .leaf => |v| gobject.ext.newInstance(SurfaceScrolledWindow, .{ + .surface = v, + }).as(gtk.Widget), .split => |s| SplitTreeSplit.new( current, &s, diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 51e4ea7b2..646ad5dbd 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -40,12 +40,16 @@ pub const Surface = extern struct { const Self = @This(); parent_instance: Parent, pub const Parent = adw.Bin; + pub const Implements = [_]type{gtk.Scrollable}; pub const getGObjectType = gobject.ext.defineClass(Self, .{ .name = "GhosttySurface", .instanceInit = &init, .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, + .implements = &.{ + gobject.ext.implement(gtk.Scrollable, .{}), + }, }); /// A SplitTree implementation that stores surfaces. @@ -301,6 +305,62 @@ pub const Surface = extern struct { }, ); }; + + pub const hadjustment = struct { + pub const name = "hadjustment"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*gtk.Adjustment, + .{ + .accessor = .{ + .getter = getHAdjustmentValue, + .setter = setHAdjustmentValue, + }, + }, + ); + }; + + pub const vadjustment = struct { + pub const name = "vadjustment"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*gtk.Adjustment, + .{ + .accessor = .{ + .getter = getVAdjustmentValue, + .setter = setVAdjustmentValue, + }, + }, + ); + }; + + pub const @"hscroll-policy" = struct { + pub const name = "hscroll-policy"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.ScrollablePolicy, + .{ + .default = .natural, + .accessor = C.privateShallowFieldAccessor("hscroll_policy"), + }, + ); + }; + + pub const @"vscroll-policy" = struct { + pub const name = "vscroll-policy"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.ScrollablePolicy, + .{ + .default = .natural, + .accessor = C.privateShallowFieldAccessor("vscroll_policy"), + }, + ); + }; }; pub const signals = struct { @@ -548,6 +608,13 @@ pub const Surface = extern struct { action_group: ?*gio.SimpleActionGroup = null, + // Gtk.Scrollable interface adjustments + hadj: ?*gtk.Adjustment = null, + vadj: ?*gtk.Adjustment = null, + hscroll_policy: gtk.ScrollablePolicy = .natural, + vscroll_policy: gtk.ScrollablePolicy = .natural, + vadj_signal_group: ?*gobject.SignalGroup = null, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -714,6 +781,47 @@ pub const Surface = extern struct { return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0; } + /// Set the scrollbar state for this surface. This will setup the + /// properties for our Gtk.Scrollable interface properly. + pub fn setScrollbar(self: *Self, scrollbar: terminal.Scrollbar) void { + // Update existing adjustment in-place. If we don't have an + // adjustment then we do nothing because we're not part of a + // scrolled window. + const vadj = self.getVAdjustment() orelse return; + + // Check if values match existing adjustment and skip update if so + const value: f64 = @floatFromInt(scrollbar.offset); + const upper: f64 = @floatFromInt(scrollbar.total); + const page_size: f64 = @floatFromInt(scrollbar.len); + + if (std.math.approxEqAbs(f64, vadj.getValue(), value, 0.001) and + std.math.approxEqAbs(f64, vadj.getUpper(), upper, 0.001) and + std.math.approxEqAbs(f64, vadj.getPageSize(), page_size, 0.001)) + { + return; + } + + // If we have a vadjustment we MUST have the signal group since + // it is setup in the prop handler. + const priv = self.private(); + const group = priv.vadj_signal_group.?; + + // During manual scrollbar changes from Ghostty core we don't + // want to emit value-changed signals so we block them. This would + // cause a waste of resources at best and infinite loops at worst. + group.block(); + defer group.unblock(); + + vadj.configure( + value, // value: current scroll position + 0, // lower: minimum value + upper, // upper: maximum value (total scrollable area) + 1, // step_increment: amount to scroll on arrow click + page_size, // page_increment: amount to scroll on page up/down + page_size, // page_size: size of visible area + ); + } + /// Set the current progress report state. pub fn setProgressReport( self: *Self, @@ -1519,6 +1627,7 @@ pub const Surface = extern struct { priv.mouse_hidden = false; priv.focused = true; priv.size = .{ .width = 0, .height = 0 }; + priv.vadj_signal_group = null; // If our configuration is null then we get the configuration // from the application. @@ -1583,6 +1692,22 @@ pub const Surface = extern struct { priv.config = null; } + if (priv.vadj_signal_group) |group| { + group.setTarget(null); + group.as(gobject.Object).unref(); + priv.vadj_signal_group = null; + } + + if (priv.hadj) |v| { + v.as(gobject.Object).unref(); + priv.hadj = null; + } + + if (priv.vadj) |v| { + v.as(gobject.Object).unref(); + priv.vadj = null; + } + if (priv.progress_bar_timer) |timer| { if (glib.Source.remove(timer) == 0) { log.warn("unable to remove progress bar timer", .{}); @@ -1996,6 +2121,43 @@ pub const Surface = extern struct { self.as(gtk.Widget).setCursorFromName(name.ptr); } + fn vadjValueChanged(adj: *gtk.Adjustment, self: *Self) callconv(.c) void { + // This will trigger for every single pixel change in the adjustment, + // but our core surface handles the noise from this so that identical + // rows are cheap. + const core_surface = self.core() orelse return; + const row: usize = @intFromFloat(@round(adj.getValue())); + _ = core_surface.performBindingAction(.{ .scroll_to_row = row }) catch |err| { + log.err("error performing scroll_to_row action err={}", .{err}); + }; + } + + fn propVAdjustment( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + + // When vadjustment is first set, we setup the signal group lazily. + // This makes it so that if we don't use scrollbars, we never + // pay the memory cost of this. + const group: *gobject.SignalGroup = priv.vadj_signal_group orelse group: { + const group = gobject.SignalGroup.new(gtk.Adjustment.getGObjectType()); + group.connect( + "value-changed", + @ptrCast(&vadjValueChanged), + self, + ); + + priv.vadj_signal_group = group; + break :group group; + }; + + // Setup our signal group target + group.setTarget(if (priv.vadj) |v| v.as(gobject.Object) else null); + } + /// Handle bell features that need to happen every time a BEL is received /// Currently this is audio and system but this could change in the future. fn ringBell(self: *Self) void { @@ -2060,6 +2222,66 @@ pub const Surface = extern struct { } } + //--------------------------------------------------------------- + // Gtk.Scrollable interface implementation + + pub fn getHAdjustment(self: *Self) ?*gtk.Adjustment { + return self.private().hadj; + } + + pub fn setHAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void { + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.hadjustment.impl.param_spec); + + const priv = self.private(); + if (priv.hadj) |old| { + old.as(gobject.Object).unref(); + priv.hadj = null; + } + + const adj = adj_ orelse return; + _ = adj.as(gobject.Object).ref(); + priv.hadj = adj; + } + + fn getHAdjustmentValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set(value, self.getHAdjustment()); + } + + fn setHAdjustmentValue(self: *Self, value: *const gobject.Value) void { + self.setHAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment)); + } + + pub fn getVAdjustment(self: *Self) ?*gtk.Adjustment { + return self.private().vadj; + } + + pub fn setVAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void { + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.vadjustment.impl.param_spec); + + const priv = self.private(); + + if (priv.vadj) |old| { + old.as(gobject.Object).unref(); + priv.vadj = null; + } + + const adj = adj_ orelse return; + _ = adj.as(gobject.Object).ref(); + priv.vadj = adj; + } + + fn getVAdjustmentValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set(value, self.getVAdjustment()); + } + + fn setVAdjustmentValue(self: *Self, value: *const gobject.Value) void { + self.setVAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment)); + } + //--------------------------------------------------------------- // Signal Handlers @@ -3013,6 +3235,7 @@ pub const Surface = extern struct { class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); + class.bindTemplateCallback("notify_vadjustment", &propVAdjustment); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown); @@ -3034,6 +3257,12 @@ pub const Surface = extern struct { properties.@"title-override".impl, properties.zoom.impl, properties.@"is-split".impl, + + // For Gtk.Scrollable + properties.hadjustment.impl, + properties.vadjustment.impl, + properties.@"hscroll-policy".impl, + properties.@"vscroll-policy".impl, }); // Signals diff --git a/src/apprt/gtk/class/surface_scrolled_window.zig b/src/apprt/gtk/class/surface_scrolled_window.zig new file mode 100644 index 000000000..3095b4c78 --- /dev/null +++ b/src/apprt/gtk/class/surface_scrolled_window.zig @@ -0,0 +1,209 @@ +const std = @import("std"); +const assert = std.debug.assert; +const adw = @import("adw"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const gresource = @import("../build/gresource.zig"); +const Common = @import("../class.zig").Common; +const Surface = @import("surface.zig").Surface; +const Config = @import("config.zig").Config; + +const log = std.log.scoped(.gtk_ghostty_surface_scrolled_window); + +/// A wrapper widget that embeds a Surface inside a GtkScrolledWindow. +/// This provides scrollbar functionality for the terminal surface. +/// The surface property can be set during initialization or changed +/// dynamically via the surface property. +pub const SurfaceScrolledWindow = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhostttySurfaceScrolledWindow", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const config = struct { + pub const name = "config"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Config, + .{ + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + + pub const surface = struct { + pub const name = "surface"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Surface, + .{ + .accessor = .{ + .getter = getSurfaceValue, + .setter = setSurfaceValue, + }, + }, + ); + }; + }; + + const Private = struct { + config: ?*Config = null, + config_binding: ?*gobject.Binding = null, + surface: ?*Surface = null, + pub var offset: c_int = 0; + }; + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + + fn dispose(self: *Self) callconv(.c) void { + const priv = self.private(); + + if (priv.config_binding) |binding| { + binding.as(gobject.Object).unref(); + priv.config_binding = null; + } + + if (priv.config) |v| { + v.unref(); + priv.config = null; + } + + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.c) void { + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + fn getSurfaceValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set( + value, + self.private().surface, + ); + } + + fn setSurfaceValue(self: *Self, value: *const gobject.Value) void { + self.setSurface(gobject.ext.Value.get( + value, + ?*Surface, + )); + } + + pub fn getSurface(self: *Self) ?*Surface { + return self.private().surface; + } + + pub fn setSurface(self: *Self, surface_: ?*Surface) void { + const priv = self.private(); + + if (surface_ == priv.surface) return; + + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec); + + priv.surface = surface_; + } + + fn closureScrollbarPolicy( + _: *Self, + config_: ?*Config, + ) callconv(.c) gtk.PolicyType { + const config = if (config_) |c| c.get() else return .automatic; + return switch (config.scrollbar) { + .never => .never, + .system => .automatic, + }; + } + + fn propSurface( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + const child: *gtk.Widget = self.as(Parent).getChild().?; + const scrolled_window = gobject.ext.cast(gtk.ScrolledWindow, child).?; + scrolled_window.setChild(if (priv.surface) |s| s.as(gtk.Widget) else null); + + // Unbind old config binding if it exists + if (priv.config_binding) |binding| { + binding.as(gobject.Object).unref(); + priv.config_binding = null; + } + + // Bind config from surface to our config property + if (priv.surface) |surface| { + priv.config_binding = surface.as(gobject.Object).bindProperty( + properties.config.name, + self.as(gobject.Object), + properties.config.name, + .{ .sync_create = true }, + ); + } + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 5, + .name = "surface-scrolled-window", + }), + ); + + // Bindings + class.bindTemplateCallback("scrollbar_policy", &closureScrollbarPolicy); + class.bindTemplateCallback("notify_surface", &propSurface); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.config.impl, + properties.surface.impl, + }); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 84e00ac4a..0596bf15d 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -174,6 +174,7 @@ template $GhosttySurface: Adw.Bin { notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hidden => $notify_mouse_hidden(); notify::mouse-shape => $notify_mouse_shape(); + notify::vadjustment => $notify_vadjustment(); // Some history: we used to use a Stack here and swap between the // terminal and error pages as needed. But a Stack doesn't play nice // with our SplitTree and Gtk.Paned usage[^1]. Replacing this with diff --git a/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp b/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp new file mode 100644 index 000000000..722c4427b --- /dev/null +++ b/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; +using Adw 1; + +template $GhostttySurfaceScrolledWindow: Adw.Bin { + notify::surface => $notify_surface(); + + Gtk.ScrolledWindow { + hscrollbar-policy: never; + vscrollbar-policy: bind $scrollbar_policy(template.config) as ; + } +} From 1cc22f93ca2f4828ab45ba2247798104b2bc4f92 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Oct 2025 14:46:07 -0700 Subject: [PATCH 299/319] renderer: force a full rebuild on any font grid change Fixes #2731 (again) This regressed in 1.2 due to the renderer rework missing porting this. I believe this issue is still valid even with the rework since the font grid changes the atlas and if there are still cached cells that reference the old atlas coordinates it will produce garbage. --- src/renderer/generic.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 6031bede4..d18e78afb 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1068,6 +1068,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Update relevant uniforms self.updateFontGridUniforms(); + + // Force a full rebuild, because cached rows may still reference + // an outdated atlas from the old grid and this can cause garbage + // to be rendered. + self.cells_viewport = null; } /// Update uniforms that are based on the font grid. From ad9f9dc11ee28ca5de2834692d6a2357541b42ee Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 17 Oct 2025 14:57:10 -0700 Subject: [PATCH 300/319] font: Default to light hinting in FreeType --- src/config/Config.zig | 11 ++++++++++- src/font/face/freetype.zig | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b3085c4c4..d0e086710 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -476,6 +476,11 @@ pub const compatibility = std.StaticStringMap( /// /// * `autohint` - Enable the freetype auto-hinter. Enabled by default. /// +/// * `light` - Use a light hinting style, better preserving glyph shapes. +/// This is the most common setting in GTK apps and therefore also Ghostty's +/// default. This has no effect if `monochrome` is enabled. Enabled by +/// default. +/// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` @"freetype-load-flags": FreetypeLoadFlags = .{}, @@ -7886,11 +7891,15 @@ pub const BackgroundImageFit = enum { 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. + // to these defaults. (Strictly speaking, `light` isn't FreeType's + // own default, but appears to be the effective default with most + // Fontconfig-aware software using FreeType, so until Ghostty + // implements Fontconfig support we default to `light`.) hinting: bool = true, @"force-autohint": bool = false, monochrome: bool = false, autohint: bool = true, + light: bool = true, }; /// See linux-cgroup diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 4958c48c8..95f05881b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -378,6 +378,10 @@ pub const Face = struct { // else it won't look very good at all. .target_mono = self.load_flags.monochrome, + // Otherwise we select hinter based on the `light` flag. + .target_normal = !self.load_flags.light and !self.load_flags.monochrome, + .target_light = self.load_flags.light and !self.load_flags.monochrome, + // NO_SVG set to true because we don't currently support rendering // SVG glyphs under FreeType, since that requires bundling another // dependency to handle rendering the SVG. @@ -1143,7 +1147,7 @@ test { ft_font.glyphIndex('A').?, .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, ); - try testing.expectEqual(@as(u32, 20), g2.height); + try testing.expectEqual(@as(u32, 21), g2.height); } } From 5b7f1456407824b624afd2b53438db126fbee90b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Oct 2025 19:48:59 -0700 Subject: [PATCH 301/319] macos: make terminal smaller to account for legacy scrollbar When the preferred scrollbar style is "legacy", the scrollbar takes up space that offsets the actual terminal. To prevent reflow, we detect this before the scrollbar becomes visible and shrink our terminal width to prepare for it. This doesn't account for the style changing at runtime, yet. --- macos/Sources/Ghostty/SurfaceScrollView.swift | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift index 714227cd1..b1e1b9baf 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -142,18 +142,35 @@ class SurfaceScrollView: NSView { // Only update sizes if we have a valid (non-zero) content size. The content size // can be zero when this is added early to a view, or to an invisible hierarchy. // Practically, this happened in the quick terminal. - let contentSize = scrollView.contentSize - if contentSize.width > 0 && contentSize.height > 0 { - // Keep document width synchronized with content width - documentView.setFrameSize(CGSize( - width: contentSize.width, - height: documentView.frame.height - )) - - // Inform the actual pty of our size change - surfaceView.sizeDidChange(contentSize) + var contentSize = scrollView.contentSize + guard contentSize.width > 0 && contentSize.height > 0 else { + synchronizeSurfaceView() + return } + // If we have a legacy scrollbar and its not visible, then we account for that + // in advance, because legacy scrollbars change our contentSize and force reflow + // of our terminal which is not desirable. + // See: https://github.com/ghostty-org/ghostty/discussions/9254 + let style = scrollView.verticalScroller?.scrollerStyle ?? NSScroller.preferredScrollerStyle + if style == .legacy { + if (scrollView.verticalScroller?.isHidden ?? true) { + let scrollerWidth = NSScroller.scrollerWidth(for: .regular, scrollerStyle: .legacy) + contentSize.width -= scrollerWidth + } + } + + // Keep document width synchronized with content width + documentView.setFrameSize(CGSize( + width: contentSize.width, + height: documentView.frame.height + )) + + // Inform the actual pty of our size change. This doesn't change the actual view + // frame because we do want to render the whole thing, but it will prevent our + // rows/cols from going into the non-content area. + surfaceView.sizeDidChange(contentSize) + // When our scrollview changes make sure our surface view is synchronized synchronizeSurfaceView() } From 3e6bda1fffe1e09037423dc45062f6ecb1064f7b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Oct 2025 21:00:31 -0700 Subject: [PATCH 302/319] ci: run release-tip even if prior step failed --- .github/workflows/release-tip.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index ee2fe47b3..8f7bd2665 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -19,7 +19,6 @@ jobs: if: | github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -151,7 +150,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -206,7 +204,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -373,7 +370,6 @@ jobs: # Create our appcast for Sparkle - name: Generate Appcast if: | - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' env: @@ -408,7 +404,6 @@ jobs: # gets out of sync with the binaries. - name: Prep R2 Storage for Appcast if: | - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' run: | @@ -418,7 +413,6 @@ jobs: - name: Upload Appcast to R2 if: | - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 @@ -444,7 +438,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -629,7 +622,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) From ea505ec51db01eb15ac035ab20dea80c9d793582 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 17 Oct 2025 20:33:32 -0700 Subject: [PATCH 303/319] macos: use stable display UUID for quick terminal screen tracking NSScreen instances can be garbage collected at any time, even for screens that remain connected, making NSMapTable with weak keys unreliable for tracking per-screen state. This changes the quick terminal to use CGDisplay UUIDs as stable identifiers, keyed in a strong dictionary. Each entry stores the window frame along with screen dimensions, scale factor, and last-seen timestamp. Rules for pruning: - Entries are invalidated when screens shrink or change scale - Entries persist and update when screens grow (allowing cached state to work with larger resolutions) - Stale entries for disconnected screens expire after 14 days. - Maximum of 10 screen entries to prevent unbounded growth --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../QuickTerminalController.swift | 34 ++---- .../QuickTerminalScreenStateCache.swift | 113 ++++++++++++++++++ .../Extensions/NSScreen+Extension.swift | 7 ++ .../Helpers/Extensions/UUID+Extension.swift | 9 ++ 5 files changed, 137 insertions(+), 27 deletions(-) create mode 100644 macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift create mode 100644 macos/Sources/Helpers/Extensions/UUID+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index ae0051c53..86292fbe2 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ Features/QuickTerminal/QuickTerminalController.swift, Features/QuickTerminal/QuickTerminalPosition.swift, Features/QuickTerminal/QuickTerminalScreen.swift, + Features/QuickTerminal/QuickTerminalScreenStateCache.swift, Features/QuickTerminal/QuickTerminalSize.swift, Features/QuickTerminal/QuickTerminalSpaceBehavior.swift, Features/QuickTerminal/QuickTerminalWindow.swift, diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 37c9985c9..4669e108a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -21,13 +21,8 @@ class QuickTerminalController: BaseTerminalController { // The active space when the quick terminal was last shown. private var previousActiveSpace: CGSSpace? = nil - /// The saved state when the quick terminal's surface tree becomes empty. - /// - /// This preserves the user's window size and position when all terminal surfaces - /// are closed (e.g., via the `exit` command). When a new surface is created, - /// the window will be restored to this frame, preventing SwiftUI from resetting - /// the window to its default minimum size. - private var lastClosedFrames: NSMapTable + /// Cache for per-screen window state. + private let screenStateCache = QuickTerminalScreenStateCache() /// Non-nil if we have hidden dock state. private var hiddenDock: HiddenDock? = nil @@ -45,10 +40,6 @@ class QuickTerminalController: BaseTerminalController { ) { self.position = position self.derivedConfig = DerivedConfig(ghostty.config) - - // This is a weak to strong mapping, so that our keys being NSScreens - // can remove themselves when they disappear. - self.lastClosedFrames = .weakToStrongObjects() // Important detail here: we initialize with an empty surface tree so // that we don't start a terminal process. This gets started when the @@ -379,17 +370,15 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } - // Grab our last closed frame to use, and clear our state since we're animating in. - // We only use the last closed frame if we're opening on the same screen. - let lastClosedFrame: NSRect? = lastClosedFrames.object(forKey: screen)?.frame - lastClosedFrames.removeObject(forKey: screen) + // Grab our last closed frame to use from the cache. + let closedFrame = screenStateCache.frame(for: screen) // Move our window off screen to the initial animation position. position.setInitial( in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize, - closedFrame: lastClosedFrame) + closedFrame: closedFrame) // We need to set our window level to a high value. In testing, only // popUpMenu and above do what we want. This gets it above the menu bar @@ -424,7 +413,7 @@ class QuickTerminalController: BaseTerminalController { in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, - closedFrame: lastClosedFrame) + closedFrame: closedFrame) }, completionHandler: { // There is a very minor delay here so waiting at least an event loop tick // keeps us safe from the view not being on the window. @@ -513,7 +502,7 @@ class QuickTerminalController: BaseTerminalController { // terminal is reactivated with a new surface. Without this, SwiftUI // would reset the window to its minimum content size. if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen { - lastClosedFrames.setObject(.init(frame: window.frame), forKey: screen) + screenStateCache.save(frame: window.frame, for: screen) } // If we hid the dock then we unhide it. @@ -598,7 +587,6 @@ class QuickTerminalController: BaseTerminalController { alert.alertStyle = .warning alert.beginSheetModal(for: window) } - // MARK: First Responder @IBAction override func closeWindow(_ sender: Any) { @@ -736,14 +724,6 @@ class QuickTerminalController: BaseTerminalController { hidden = false } } - - private class LastClosedState { - let frame: NSRect - - init(frame: NSRect) { - self.frame = frame - } - } } extension Notification.Name { diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift new file mode 100644 index 000000000..7dc53816c --- /dev/null +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift @@ -0,0 +1,113 @@ +import Foundation +import Cocoa + +/// Manages cached window state per screen for the quick terminal. +/// +/// This cache tracks the last closed window frame for each screen, allowing the quick terminal +/// to restore to its previous size and position when reopened. It uses stable display UUIDs +/// to survive NSScreen garbage collection and automatically prunes stale entries. +class QuickTerminalScreenStateCache { + /// The maximum number of saved screen states we retain. This is to avoid some kind of + /// pathological memory growth in case we get our screen state serializing wrong. I don't + /// know anyone with more than 10 screens, so let's just arbitrarily go with that. + private static let maxSavedScreens = 10 + + /// Time-to-live for screen entries that are no longer present (14 days). + private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60 + + /// Keyed by display UUID to survive NSScreen garbage collection. + private var stateByDisplay: [UUID: DisplayEntry] = [:] + + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(onScreensChanged(_:)), + name: NSApplication.didChangeScreenParametersNotification, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /// Save the window frame for a screen. + func save(frame: NSRect, for screen: NSScreen) { + guard let key = screen.displayUUID else { return } + let entry = DisplayEntry( + frame: frame, + screenSize: screen.frame.size, + scale: screen.backingScaleFactor, + lastSeen: Date() + ) + stateByDisplay[key] = entry + pruneCapacity() + } + + /// Retrieve the last closed frame for a screen, if valid. + func frame(for screen: NSScreen) -> NSRect? { + guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil } + + // Drop on dimension/scale change that makes the entry invalid + if !entry.isValid(for: screen) { + stateByDisplay.removeValue(forKey: key) + return nil + } + + entry.lastSeen = Date() + stateByDisplay[key] = entry + return entry.frame + } + + @objc private func onScreensChanged(_ note: Notification) { + let screens = NSScreen.screens + let now = Date() + let currentIDs = Set(screens.compactMap { $0.displayUUID }) + + for screen in screens { + guard let key = screen.displayUUID else { continue } + if var entry = stateByDisplay[key] { + // Drop on dimension/scale change that makes the entry invalid + if !entry.isValid(for: screen) { + stateByDisplay.removeValue(forKey: key) + } else { + // Update the screen size if it grew (keep entry valid for larger screens) + entry.screenSize = screen.frame.size + entry.lastSeen = now + stateByDisplay[key] = entry + } + } + } + + // TTL prune for non-present screens + stateByDisplay = stateByDisplay.filter { key, entry in + currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL + } + + pruneCapacity() + } + + private func pruneCapacity() { + guard stateByDisplay.count > Self.maxSavedScreens else { return } + let toRemove = stateByDisplay + .sorted { $0.value.lastSeen < $1.value.lastSeen } + .prefix(stateByDisplay.count - Self.maxSavedScreens) + for (key, _) in toRemove { + stateByDisplay.removeValue(forKey: key) + } + } + + private struct DisplayEntry { + var frame: NSRect + var screenSize: CGSize + var scale: CGFloat + var lastSeen: Date + + /// Returns true if this entry is still valid for the given screen. + /// Valid if the scale matches and the cached size is not larger than the current screen size. + /// This allows entries to persist when screens grow, but invalidates them when screens shrink. + func isValid(for screen: NSScreen) -> Bool { + guard scale == screen.backingScaleFactor else { return false } + return screenSize.width <= screen.frame.size.width && screenSize.height <= screen.frame.size.height + } + } +} diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index f46106004..a8eb7b876 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -5,6 +5,13 @@ extension NSScreen { var displayID: UInt32? { deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? UInt32 } + + /// The stable UUID for this display, suitable for tracking across reconnects and NSScreen garbage collection. + var displayUUID: UUID? { + guard let displayID = displayID else { return nil } + guard let cfuuid = CGDisplayCreateUUIDFromDisplayID(displayID)?.takeRetainedValue() else { return nil } + return UUID(cfuuid) + } // Returns true if the given screen has a visible dock. This isn't // point-in-time visible, this is true if the dock is always visible diff --git a/macos/Sources/Helpers/Extensions/UUID+Extension.swift b/macos/Sources/Helpers/Extensions/UUID+Extension.swift new file mode 100644 index 000000000..e536353c5 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/UUID+Extension.swift @@ -0,0 +1,9 @@ +import Foundation + +extension UUID { + /// Initialize a UUID from a CFUUID. + init?(_ cfuuid: CFUUID) { + guard let uuidString = CFUUIDCreateString(nil, cfuuid) as String? else { return nil } + self.init(uuidString: uuidString) + } +} From be0da4845cb629bcf1f5f1890a28850bb7adfe16 Mon Sep 17 00:00:00 2001 From: tdslot Date: Sat, 18 Oct 2025 19:27:39 +0200 Subject: [PATCH 304/319] =?UTF-8?q?=F0=9F=8C=90=20i18n(locale):=20add=20li?= =?UTF-8?q?thuanian=20language=20support=20(#8711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrius Budvytis <154380884+abudvytis@users.noreply.github.com> --- CODEOWNERS | 1 + po/lt_LT.UTF-8.po | 318 ++++++++++++++++++++++++++++++++++++++++ src/os/i18n_locales.zig | 1 + 3 files changed, 320 insertions(+) create mode 100644 po/lt_LT.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 9e854b06c..8a4f797d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -184,6 +184,7 @@ /po/ko_KR.UTF-8.po @ghostty-org/ko_KR /po/he_IL.UTF-8.po @ghostty-org/he_IL /po/it_IT.UTF-8.po @ghostty-org/it_IT +/po/lt_LT.UTF-8.po @ghostty-org/lt_LT /po/zh_TW.UTF-8.po @ghostty-org/zh_TW /po/hr_HR.UTF-8.po @ghostty-org/hr_HR diff --git a/po/lt_LT.UTF-8.po b/po/lt_LT.UTF-8.po new file mode 100644 index 000000000..0c466d3a4 --- /dev/null +++ b/po/lt_LT.UTF-8.po @@ -0,0 +1,318 @@ +# Language LT translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Tadas Lotuzas , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-07-22 17:18+0000\n" +"PO-Revision-Date: 2025-09-17 13:27+0200\n" +"Last-Translator: Tadas Lotuzas \n" +"Language-Team: Language LT\n" +"Language: LT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Keisti terminalo pavadinimą" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Palikite tuščią, kad atkurtumėte numatytąjį pavadinimą." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Atšaukti" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Gerai" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Konfigūracijos klaidos" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Rasta viena ar daugiau konfigūracijos klaidų. Peržiūrėkite žemiau esančias klaidas " +"ir arba iš naujo įkelkite konfigūraciją, arba ignoruokite šias klaidas." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignoruoti" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "Iš naujo įkelti konfigūraciją" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Padalinti aukštyn" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Padalinti žemyn" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Padalinti kairėn" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Padalinti dešinėn" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Vykdyti komandą…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Kopijuoti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "Įklijuoti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Išvalyti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Atstatyti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Padalinti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Keisti pavadinimą…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Kortelė" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:265 +msgid "New Tab" +msgstr "Nauja kortelė" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Uždaryti kortelę" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Langas" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Naujas langas" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Uždaryti langą" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Konfigūracija" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Atidaryti konfigūraciją" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Komandų paletė" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Terminalo inspektorius" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 +msgid "About Ghostty" +msgstr "Apie Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Išeiti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Leisti prieigą prie iškarpinės" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Programa bando skaityti iš iškarpinės. Žemiau rodomas dabartinis " +"iškarpinės turinys." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Drausti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Leisti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "Prisiminti pasirinkimą šiam padalijimui" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "Iš naujo įkelkite konfigūraciją, kad vėl būtų rodoma ši užuomina" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Programa bando rašyti į iškarpinę. Žemiau rodomas dabartinis " +"iškarpinės turinys." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Įspėjimas: galimai nesaugus įklijavimas" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Šio teksto įklijavimas į terminalą gali būti pavojingas, nes panašu, kad " +"gali būti vykdomos tam tikros komandos." + +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 +msgid "Close" +msgstr "Uždaryti" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Išeiti iš Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Uždaryti langą?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Uždaryti kortelę?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Uždaryti padalijimą?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Visos terminalo sesijos bus nutrauktos." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Visos terminalo sesijos šiame lange bus nutrauktos." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Visos terminalo sesijos šioje kortelėje bus nutrauktos." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Šiuo metu vykdomas procesas šiame padalijime bus nutrauktas." + +#: src/apprt/gtk/Surface.zig:1266 +msgid "Copied to clipboard" +msgstr "Nukopijuota į iškarpinę" + +#: src/apprt/gtk/Surface.zig:1268 +msgid "Cleared clipboard" +msgstr "Iškarpinė išvalyta" + +#: src/apprt/gtk/Surface.zig:2525 +msgid "Command succeeded" +msgstr "Komanda sėkminga" + +#: src/apprt/gtk/Surface.zig:2527 +msgid "Command failed" +msgstr "Komanda nepavyko" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Pagrindinis meniu" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Peržiūrėti atidarytas korteles" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Naujas padalijimas" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Naudojate Ghostty derinimo versiją! Našumas bus sumažintas." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Konfigūracija įkelta iš naujo" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty kūrėjai" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: terminalo inspektorius" diff --git a/src/os/i18n_locales.zig b/src/os/i18n_locales.zig index 0fca223b9..ac1673a94 100644 --- a/src/os/i18n_locales.zig +++ b/src/os/i18n_locales.zig @@ -52,4 +52,5 @@ pub const locales = [_][:0]const u8{ "he_IL.UTF-8", "zh_TW.UTF-8", "hr_HR.UTF-8", + "lt_LT.UTF-8", }; From 92c9ba67d5c2b2e40b6b26d7ea6ef8aa1c239e43 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 19 Oct 2025 00:15:26 +0000 Subject: [PATCH 305/319] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 245a67e7b..1bd4d6a5b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -116,8 +116,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz", - .hash = "N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz", + .hash = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index d0193ed8b..cf2857147 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl": { + "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz", - "hash": "sha256-yTN88FFxGVeK07fSQK+jWy9XLAln7f/W+xcDH+tLOEY=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz", + "hash": "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 3c4f4d855..1ac748b69 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl"; + name = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz"; - hash = "sha256-yTN88FFxGVeK07fSQK+jWy9XLAln7f/W+xcDH+tLOEY="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz"; + hash = "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index e789fa4eb..398231198 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -29,7 +29,7 @@ https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 93d73d2ca..d762d82c1 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251006-150522-c07f0e8/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AALIsAwAgV4B4dOJYBqTmOftgMMm4D4BxFPzns5Bl", - "sha256": "c9337cf0517119578ad3b7d240afa35b2f572c0967edffd6fb17031feb4b3846" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq", + "sha256": "bc59fa32247cb55906ca3495ee9cf89389acbe24828c61ca336494f38949a7f9" }, { "type": "archive", From 3a9eedcd15f7c108d4f762c4b3924b0068523049 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 18 Oct 2025 21:28:47 -0700 Subject: [PATCH 306/319] renderer: don't allow the scrollbar state to block the renderer This fixes the source of a deadlock that some tip users have hit. If our surface mailbox is full and there is a dirty scrollbar state, then drawFrame would block forever trying to queue to the surface mailbox. We now fail instantly if the queue is full and keep the scrollbar state dirty. We can try again on the next frame, it's not a critical thing to get updated. --- src/renderer/generic.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d18e78afb..9e13d0b41 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1322,10 +1322,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // After the graphics API is complete (so we defer) we want to // update our scrollbar state. defer if (self.scrollbar_dirty) { - self.scrollbar_dirty = false; - _ = self.surface_mailbox.push(.{ + // Fail instantly if the surface mailbox if full, we'll just + // get it on the next frame. + if (self.surface_mailbox.push(.{ .scrollbar = self.scrollbar, - }, .{ .forever = {} }); + }, .instant) > 0) self.scrollbar_dirty = false; }; // Let our graphics API do any bookkeeping, etc. From c6788dd17839ef916c6096fe913c341a765ffb8d Mon Sep 17 00:00:00 2001 From: Sola Date: Sun, 19 Oct 2025 20:42:22 +0800 Subject: [PATCH 307/319] fix: fish shell integration should not modify universal path variable `fish_add_path` by default updates the `fish_user_paths` universal variable which makes the modification persist across shell sessions. The integration also tries to update the `fish_user_paths` when the desired path already appears in the `PATH` environment variable but not in `fish_user_paths`. Because `fish_user_paths` will always be inserted before the inherited `PATH` env. This makes the added path unintentionally has a higher priority. This patch makes the above issues by adding `--global` and `--path` options to `fish_user_paths` which limits the modification scope and ensures that the path won't be added if it already exists in `PATH`. --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index f745bbb13..47af9be98 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -67,7 +67,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Add Ghostty binary to PATH if the path feature is enabled if contains path $features; and test -n "$GHOSTTY_BIN_DIR" - fish_add_path --append "$GHOSTTY_BIN_DIR" + fish_add_path --global --path --append "$GHOSTTY_BIN_DIR" end # When using sudo shell integration feature, ensure $TERMINFO is set From 73da748390178e4bdd814aeb8cccd4f37e27825a Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 19 Oct 2025 13:12:32 -0400 Subject: [PATCH 308/319] os: remove unused UrlParsingError This is no longer used as of e5247f6d. --- src/os/hostname.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 283ece8d9..1d5ce428b 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,11 +1,6 @@ const std = @import("std"); const posix = std.posix; -pub const UrlParsingError = std.Uri.ParseError || error{ - HostnameIsNotMacAddress, - NoSchemeProvided, -}; - pub const LocalHostnameValidationError = error{ PermissionDenied, Unexpected, From 010cbce2201c8bd6ff57ce160ddf8fa69829e5b8 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 19 Oct 2025 14:27:18 -0400 Subject: [PATCH 309/319] os: add RFC 1123-compliant hostname.isValid std.net.isValidHostname is currently too generous. It considers strings like ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which is incorrect according to RFC 1123 (the currently accepted standard). Until the standard library function is improved, we can use this local implementation that does follow the RFC 1123 standard. I asked Claude to perform an audit of the code based on its understand of the RFC. It suggested some additional test cases and considers the overall implementation to be robust (its words) and standards compliant. Ref: https://www.rfc-editor.org/rfc/rfc1123 --- src/os/hostname.zig | 85 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 283ece8d9..14cd61e7a 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -11,6 +11,91 @@ pub const LocalHostnameValidationError = error{ Unexpected, }; +/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) +/// +/// std.net.isValidHostname is (currently) too generous. It considers strings like +/// ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which +/// is incorrect. +pub fn isValid(hostname: []const u8) bool { + if (hostname.len == 0) return false; + if (hostname[0] == '.') return false; + + // Ignore trailing dot (FQDN). It doesn't count toward our length. + const end = if (hostname[hostname.len - 1] == '.') end: { + if (hostname.len == 1) return false; + break :end hostname.len - 1; + } else hostname.len; + + if (end > 253) return false; + + // Hostnames are divided into dot-separated "labels", which: + // + // - Start with a letter or digit + // - Can contain letters, digits, or hyphens + // - Must end with a letter or digit + // - Have a minimum of 1 character and a maximum of 63 + var label_start: usize = 0; + var label_len: usize = 0; + for (hostname[0..end], 0..) |c, i| { + switch (c) { + '.' => { + if (label_len == 0 or label_len > 63) return false; + if (!std.ascii.isAlphanumeric(hostname[label_start])) return false; + if (!std.ascii.isAlphanumeric(hostname[i - 1])) return false; + + label_start = i + 1; + label_len = 0; + }, + '-' => { + label_len += 1; + }, + else => { + if (!std.ascii.isAlphanumeric(c)) return false; + label_len += 1; + }, + } + } + + // Validate the final label + if (label_len == 0 or label_len > 63) return false; + if (!std.ascii.isAlphanumeric(hostname[label_start])) return false; + if (!std.ascii.isAlphanumeric(hostname[end - 1])) return false; + + return true; +} + +test isValid { + const testing = std.testing; + + // Valid hostnames + try testing.expect(isValid("example")); + try testing.expect(isValid("example.com")); + try testing.expect(isValid("www.example.com")); + try testing.expect(isValid("sub.domain.example.com")); + try testing.expect(isValid("example.com.")); + try testing.expect(isValid("host-name.example.com.")); + try testing.expect(isValid("123.example.com.")); + try testing.expect(isValid("a-b.com")); + try testing.expect(isValid("a.b.c.d.e.f.g")); + try testing.expect(isValid("127.0.0.1")); // Also a valid hostname + try testing.expect(isValid("a" ** 63 ++ ".com")); // Label exactly 63 chars (valid) + try testing.expect(isValid("a." ** 126 ++ "a")); // Total length 253 (valid) + + // Invalid hostnames + try testing.expect(!isValid("")); + try testing.expect(!isValid(".example.com")); + try testing.expect(!isValid("example.com..")); + try testing.expect(!isValid("host..domain")); + try testing.expect(!isValid("-hostname")); + try testing.expect(!isValid("hostname-")); + try testing.expect(!isValid("a.-.b")); + try testing.expect(!isValid("host_name.com")); + try testing.expect(!isValid(".")); + try testing.expect(!isValid("..")); + try testing.expect(!isValid("a" ** 64 ++ ".com")); // Label length 64 (too long) + try testing.expect(!isValid("a." ** 126 ++ "ab")); // Total length 254 (too long) +} + /// 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`). From b2559e8d92da08fbff2f1d4188afa0d0c81d0b81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:09:59 +0000 Subject: [PATCH 310/319] build(deps): bump flatpak/flatpak-github-actions from 6.5 to 6.6 Bumps [flatpak/flatpak-github-actions](https://github.com/flatpak/flatpak-github-actions) from 6.5 to 6.6. - [Release notes](https://github.com/flatpak/flatpak-github-actions/releases) - [Commits](https://github.com/flatpak/flatpak-github-actions/compare/10a3c29f0162516f0f68006be14c92f34bd4fa6c...92ae9851ad316786193b1fd3f40c4b51eb5cb101) --- updated-dependencies: - dependency-name: flatpak/flatpak-github-actions dependency-version: '6.6' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f3196c3b..1356ec3e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1095,7 +1095,7 @@ jobs: needs: test steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5 + - uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6 with: bundle: com.mitchellh.ghostty manifest-path: flatpak/com.mitchellh.ghostty.yml From d01697e7d591de951e587122a42b875ea291d66d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:10:08 +0000 Subject: [PATCH 311/319] build(deps): bump namespacelabs/nscloud-cache-action Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.18 to 1.2.19. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/7baedde84bbf5063413d621f282834bc2654d0c1...caff5c9dc51d8126e7d16141fb015c478374256b) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.2.19 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/snap.yml | 2 +- .github/workflows/test.yml | 48 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 52190f020..da669b073 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 50a8ec7bb..e5af4ac38 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 8f7bd2665..2331dbba9 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -161,7 +161,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 4e9aa168c..cfe5591f4 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -38,7 +38,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f3196c3b..320ac0f91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,7 +72,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -106,7 +106,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -139,7 +139,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -173,7 +173,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -215,7 +215,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -251,7 +251,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -280,7 +280,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -313,7 +313,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -359,7 +359,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -578,7 +578,7 @@ jobs: echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -620,7 +620,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -668,7 +668,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -703,7 +703,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -767,7 +767,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -796,7 +796,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -824,7 +824,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -851,7 +851,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -878,7 +878,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -905,7 +905,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -932,7 +932,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -966,7 +966,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -993,7 +993,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -1028,7 +1028,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix @@ -1116,7 +1116,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index b218cdb26..c14ee56a6 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix From e5224827105f2a27a73cc69cc35f240f809a42c6 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 19 Oct 2025 20:19:34 -0400 Subject: [PATCH 312/319] cli: fix +ssh-cache IPv6 address validation The host validation code previously expected IPv6 addresses to be enclosed in [brackets], but that's not how ssh(1) expects them. This change removes that requirement and reimplements the host validation routine to check for valid hostnames and IP addresses (IPv4 and IPv6) using standard routines rather than custom logic. --- src/cli/ssh-cache/DiskCache.zig | 115 +++++++++++++------------------- 1 file changed, 45 insertions(+), 70 deletions(-) diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index 608155dfd..25d2cd42e 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -7,8 +7,9 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const xdg = @import("../../os/main.zig").xdg; -const TempDir = @import("../../os/main.zig").TempDir; +const internal_os = @import("../../os/main.zig"); +const xdg = internal_os.xdg; +const TempDir = internal_os.TempDir; const Entry = @import("Entry.zig"); // 512KB - sufficient for approximately 10k entries @@ -334,48 +335,28 @@ fn isValidCacheKey(key: []const u8) bool { if (std.mem.indexOf(u8, key, "@")) |at_pos| { const user = key[0..at_pos]; const hostname = key[at_pos + 1 ..]; - return isValidUser(user) and isValidHostname(hostname); + return isValidUser(user) and isValidHost(hostname); } - return isValidHostname(key); + return isValidHost(key); } -// Basic hostname validation - accepts domains and IPs -// (including IPv6 in brackets) -fn isValidHostname(host: []const u8) bool { - if (host.len == 0 or host.len > 253) return false; - - // Handle IPv6 addresses in brackets - if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') { - const ipv6_part = host[1 .. host.len - 1]; - if (ipv6_part.len == 0) return false; - var has_colon = false; - for (ipv6_part) |c| { - switch (c) { - 'a'...'f', 'A'...'F', '0'...'9' => {}, - ':' => has_colon = true, - else => return false, - } - } - return has_colon; +// Checks if a host is a valid hostname or IP address +fn isValidHost(host: []const u8) bool { + // First check for valid hostnames because this is assumed to be the more + // likely ssh host format. + if (internal_os.hostname.isValid(host)) { + return true; } - // Standard hostname/domain validation - for (host) |c| { - switch (c) { - 'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {}, - else => return false, - } - } - - // No leading/trailing dots or hyphens, no consecutive dots - if (host[0] == '.' or host[0] == '-' or - host[host.len - 1] == '.' or host[host.len - 1] == '-') - { + // We also accept valid IP addresses. In practice, IPv4 addresses are also + // considered valid hostnames due to their overlapping syntax, so we can + // simplify this check to be IPv6-specific. + if (std.net.Address.parseIp6(host, 0)) |_| { + return true; + } else |_| { return false; } - - return std.mem.indexOf(u8, host, "..") == null; } fn isValidUser(user: []const u8) bool { @@ -475,42 +456,36 @@ test "disk cache operations" { } // Tests -test "hostname validation - valid cases" { - const testing = std.testing; - try testing.expect(isValidHostname("example.com")); - try testing.expect(isValidHostname("sub.example.com")); - try testing.expect(isValidHostname("host-name.domain.org")); - try testing.expect(isValidHostname("192.168.1.1")); - try testing.expect(isValidHostname("a")); - try testing.expect(isValidHostname("1")); -} -test "hostname validation - IPv6 addresses" { +test isValidHost { const testing = std.testing; - try testing.expect(isValidHostname("[::1]")); - try testing.expect(isValidHostname("[2001:db8::1]")); - try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported - try testing.expect(!isValidHostname("[]")); // Empty IPv6 - try testing.expect(!isValidHostname("[invalid]")); // No colons -} -test "hostname validation - invalid cases" { - const testing = std.testing; - try testing.expect(!isValidHostname("")); - try testing.expect(!isValidHostname("host\nname")); - try testing.expect(!isValidHostname(".example.com")); - try testing.expect(!isValidHostname("example.com.")); - try testing.expect(!isValidHostname("host..domain")); - try testing.expect(!isValidHostname("-hostname")); - try testing.expect(!isValidHostname("hostname-")); - try testing.expect(!isValidHostname("host name")); - try testing.expect(!isValidHostname("host_name")); - try testing.expect(!isValidHostname("host@domain")); - try testing.expect(!isValidHostname("host:port")); + // Valid hostnames + try testing.expect(isValidHost("localhost")); + try testing.expect(isValidHost("example.com")); + try testing.expect(isValidHost("sub.example.com")); - // Too long - const long_host = "a" ** 254; - try testing.expect(!isValidHostname(long_host)); + // IPv4 addresses + try testing.expect(isValidHost("127.0.0.1")); + try testing.expect(isValidHost("192.168.1.1")); + + // IPv6 addresses + try testing.expect(isValidHost("::1")); + try testing.expect(isValidHost("2001:db8::1")); + try testing.expect(isValidHost("2001:db8:0:1:1:1:1:1")); + try testing.expect(!isValidHost("fe80::1%eth0")); // scopes not supported + + // Invalid hosts + try testing.expect(!isValidHost("")); + try testing.expect(!isValidHost("host\nname")); + try testing.expect(!isValidHost(".example.com")); + try testing.expect(!isValidHost("host..domain")); + try testing.expect(!isValidHost("-hostname")); + try testing.expect(!isValidHost("hostname-")); + try testing.expect(!isValidHost("host name")); + try testing.expect(!isValidHost("host_name")); + try testing.expect(!isValidHost("host@domain")); + try testing.expect(!isValidHost("host:port")); } test "user validation - valid cases" { @@ -551,7 +526,7 @@ test "cache key validation - hostname format" { try testing.expect(isValidCacheKey("example.com")); try testing.expect(isValidCacheKey("sub.example.com")); try testing.expect(isValidCacheKey("192.168.1.1")); - try testing.expect(isValidCacheKey("[::1]")); + try testing.expect(isValidCacheKey("::1")); try testing.expect(!isValidCacheKey("")); try testing.expect(!isValidCacheKey(".invalid.com")); } @@ -563,7 +538,7 @@ test "cache key validation - user@hostname format" { try testing.expect(isValidCacheKey("test-user@192.168.1.1")); try testing.expect(isValidCacheKey("user_name@host.domain.org")); try testing.expect(isValidCacheKey("git@github.com")); - try testing.expect(isValidCacheKey("ubuntu@[::1]")); + try testing.expect(isValidCacheKey("ubuntu@::1")); try testing.expect(!isValidCacheKey("@example.com")); try testing.expect(!isValidCacheKey("user@")); try testing.expect(!isValidCacheKey("user@@host")); From 014de2992eb696b50db9a6bc160695921b09c3b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Oct 2025 20:29:36 -0700 Subject: [PATCH 313/319] macos: goto_split direction is performable (#9284) Fixes #9283 There was a comment here noting this deficiency. GTK implements this properly. --- macos/Ghostty.xcodeproj/project.pbxproj | 2 +- .../Terminal/BaseTerminalController.swift | 13 +------- macos/Sources/Ghostty/Ghostty.App.swift | 28 ++++++++++++----- macos/Sources/Ghostty/Package.swift | 31 +++++++++++++++++++ 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 86292fbe2..ca420afaa 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -870,7 +870,7 @@ A5D449A82B53AE7B000F5B83 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "Ghostty-Debug"; + ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b9f9c5a05..552f864ee 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -569,23 +569,12 @@ class BaseTerminalController: NSWindowController, // Get the direction from the notification guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return } guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return } - - // Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection - let focusDirection: SplitTree.FocusDirection - switch direction { - case .previous: focusDirection = .previous - case .next: focusDirection = .next - case .up: focusDirection = .spatial(.up) - case .down: focusDirection = .spatial(.down) - case .left: focusDirection = .spatial(.left) - case .right: focusDirection = .spatial(.right) - } // Find the node for the target surface guard let targetNode = surfaceTree.root?.node(view: target) else { return } // Find the next surface to focus - guard let nextSurface = surfaceTree.focusTarget(for: focusDirection, from: targetNode) else { + guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else { return } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 91829f95c..3db8e7a11 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1029,26 +1029,38 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return false } guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false } - // For now, we return false if the window has no splits and we return - // true if the window has ANY splits. This isn't strictly correct because - // we should only be returning true if we actually performed the action, - // but this handles the most common case of caring about goto_split performability - // which is the no-split case. + // If the window has no splits, the action is not performable guard controller.surfaceTree.isSplit else { return false } + // Convert the C API direction to our Swift type + guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return false } + + // Find the current node in the tree + guard let targetNode = controller.surfaceTree.root?.node(view: surfaceView) else { return false } + + // Check if a split actually exists in the target direction before + // returning true. This ensures performable keybinds only consume + // the key event when we actually perform navigation. + let focusDirection: SplitTree.FocusDirection = splitDirection.toSplitTreeFocusDirection() + guard controller.surfaceTree.focusTarget(for: focusDirection, from: targetNode) != nil else { + return false + } + + // We have a valid target, post the notification to perform the navigation NotificationCenter.default.post( name: Notification.ghosttyFocusSplit, object: surfaceView, userInfo: [ - Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any, + Notification.SplitDirectionKey: splitDirection as Any, ] ) + return true + default: assertionFailure() + return false } - - return true } private static func resizeSplit( diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e8a3d0976..26804be78 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -223,7 +223,38 @@ extension Ghostty { } } } +} +#if canImport(AppKit) +// MARK: SplitFocusDirection Extensions + +extension Ghostty.SplitFocusDirection { + /// Convert to a SplitTree.FocusDirection for the given ViewType. + func toSplitTreeFocusDirection() -> SplitTree.FocusDirection { + switch self { + case .previous: + return .previous + + case .next: + return .next + + case .up: + return .spatial(.up) + + case .down: + return .spatial(.down) + + case .left: + return .spatial(.left) + + case .right: + return .spatial(.right) + } + } +} +#endif + +extension Ghostty { /// The type of a clipboard request enum ClipboardRequest { /// A direct paste of clipboard contents From 1b866918963421b81f44cff8a5c886c8813a5788 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 19 Oct 2025 20:43:28 -0700 Subject: [PATCH 314/319] termio: use a union to represent how a process is started (#9278) This cleans up some of our termio exec code by unifying process launch state into a single union type. This makes it easier to distinguish between the current two mutually exclusive modes of launching a process: fork/exec and flatpak dbus commands. It also ensures everyplace we touch related to process launching is forced to address every case (exhaustive switch handling). I did find one resource cleanup bug based on this cleanup, which is also fixed here. This just improves memory slightly so it's not a big deal. If we add future ways to launch processes, we can add a new union case. For example, I originally had a `posix_spawn` option while I was experimenting with that before abandoning it (see #9274). --- src/termio/Exec.zig | 182 ++++++++++++++++++++++---------------------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 319ae0ee6..5dfda9a14 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -102,24 +102,17 @@ pub fn threadEnter( errdefer self.subprocess.stop(); // Watcher to detect subprocess exit - var process: ?xev.Process = process: { + var process: ?xev.Process = if (self.subprocess.process) |v| switch (v) { + .fork_exec => |cmd| try xev.Process.init( + cmd.pid orelse return error.ProcessNoPid, + ), + // If we're executing via Flatpak then we can't do // traditional process watching (its implemented // as a special case in os/flatpak.zig) since the // command is on the host. - if (comptime build_config.flatpak) { - if (self.subprocess.flatpak_command != null) { - break :process null; - } - } - - // Get the pid from the subprocess - const command = self.subprocess.command orelse - return error.ProcessNotStarted; - const pid = command.pid orelse - return error.ProcessNoPid; - break :process try xev.Process.init(pid); - }; + .flatpak => null, + } else return error.ProcessNotStarted; errdefer if (process) |*p| p.deinit(); // Track our process start time for abnormal exits @@ -167,17 +160,19 @@ pub fn threadEnter( termio.Termio.ThreadData, td, processExit, - ) else if (comptime build_config.flatpak) { - // If we're in flatpak and we have a flatpak command - // then we can run the special flatpak logic for watching. - if (self.subprocess.flatpak_command) |*c| { - c.waitXev( + ) else if (comptime build_config.flatpak) flatpak: { + switch (self.subprocess.process orelse break :flatpak) { + // If we're in flatpak and we have a flatpak command + // then we can run the special flatpak logic for watching. + .flatpak => |*c| c.waitXev( td.loop, &td.backend.exec.flatpak_wait_c, termio.Termio.ThreadData, td, flatpakExit, - ); + ), + + .fork_exec => {}, } } @@ -587,10 +582,18 @@ const Subprocess = struct { grid_size: renderer.GridSize, screen_size: renderer.ScreenSize, pty: ?Pty = null, - command: ?Command = null, - flatpak_command: ?FlatpakHostCommand = null, + process: ?Process = null, linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default, + /// Union that represents the running process type. + const Process = union(enum) { + /// Standard POSIX fork/exec + fork_exec: Command, + + /// Flatpak DBus command + flatpak: FlatpakHostCommand, + }; + const ArgsFormatter = struct { args: []const [:0]const u8, @@ -883,7 +886,7 @@ const Subprocess = struct { read: Pty.Fd, write: Pty.Fd, } { - assert(self.pty == null and self.command == null); + assert(self.pty == null and self.process == null); // This function is funny because on POSIX systems it can // fail in the forked process. This is flipped to true if @@ -908,6 +911,23 @@ const Subprocess = struct { self.pty = null; }; + // Cleanup we only run in our parent when we successfully start + // the process. + defer if (!in_child and self.process != null) { + if (comptime builtin.os.tag != .windows) { + // Once our subcommand is started we can close the slave + // side. This prevents the slave fd from being leaked to + // future children. + _ = posix.close(pty.slave); + } + + // Successful start we can clear out some memory. + if (self.env) |*env| { + env.deinit(); + self.env = null; + } + }; + log.debug("starting command command={f}", .{ArgsFormatter{ .args = self.args }}); // If we can't access the cwd, then don't set any cwd and inherit. @@ -959,15 +979,15 @@ const Subprocess = struct { } // Flatpak command must have a stable pointer. - self.flatpak_command = .{ + self.process = .{ .flatpak = .{ .argv = self.args, .cwd = cwd, .env = if (self.env) |*env| env else null, .stdin = pty.slave, .stdout = pty.slave, .stderr = pty.slave, - }; - var cmd = &self.flatpak_command.?; + } }; + var cmd = &self.process.?.flatpak; const pid = try cmd.spawn(alloc); errdefer killCommandFlatpak(cmd); @@ -976,11 +996,6 @@ const Subprocess = struct { pid, }); - // Once started, we can close the pty child side. We do this after - // wait right now but that is fine too. This lets us read the - // parent and detect EOF. - _ = posix.close(pty.slave); - return .{ .read = pty.master, .write = pty.master, @@ -1033,20 +1048,7 @@ const Subprocess = struct { log.info("subcommand cgroup={s}", .{self.linux_cgroup orelse "-"}); } - if (comptime builtin.os.tag != .windows) { - // Once our subcommand is started we can close the slave - // side. This prevents the slave fd from being leaked to - // future children. - _ = posix.close(pty.slave); - } - - // Successful start we can clear out some memory. - if (self.env) |*env| { - env.deinit(); - self.env = null; - } - - self.command = cmd; + self.process = .{ .fork_exec = cmd }; return switch (builtin.os.tag) { .windows => .{ .read = pty.out_pipe, @@ -1071,7 +1073,7 @@ const Subprocess = struct { /// Called to notify that we exited externally so we can unset our /// running state. pub fn externalExit(self: *Subprocess) void { - self.command = null; + self.process = null; } /// Stop the subprocess. This is safe to call anytime. This will wait @@ -1079,25 +1081,23 @@ const Subprocess = struct { /// for it to terminate, so it will not block. /// This does not close the pty. pub fn stop(self: *Subprocess) void { - // Kill our command - if (self.command) |*cmd| { - // Note: this will also wait for the command to exit, so - // DO NOT call cmd.wait - killCommand(cmd) catch |err| - log.err("error sending SIGHUP to command, may hang: {}", .{err}); - self.command = null; - } + switch (self.process orelse return) { + .fork_exec => |*cmd| { + // Note: this will also wait for the command to exit, so + // DO NOT call cmd.wait + killCommand(cmd) catch |err| + log.err("error sending SIGHUP to command, may hang: {}", .{err}); + }, - // Kill our Flatpak command - if (comptime build_config.flatpak) { - if (self.flatpak_command) |*cmd| { + .flatpak => |*cmd| if (comptime build_config.flatpak) { killCommandFlatpak(cmd) catch |err| log.err("error sending SIGHUP to command, may hang: {}", .{err}); _ = cmd.wait() catch |err| log.err("error waiting for command to exit: {}", .{err}); - self.flatpak_command = null; - } + }, } + + self.process = null; } /// Resize the pty subprocess. This is safe to call anytime. @@ -1137,41 +1137,45 @@ const Subprocess = struct { _ = try command.wait(false); }, - else => if (getpgid(pid)) |pgid| { - // It is possible to send a killpg between the time that - // our child process calls setsid but before or simultaneous - // to calling execve. In this case, the direct child dies - // but grandchildren survive. To work around this, we loop - // and repeatedly kill the process group until all - // descendents are well and truly dead. We will not rest - // until the entire family tree is obliterated. - while (true) { - switch (posix.errno(c.killpg(pgid, c.SIGHUP))) { - .SUCCESS => log.debug("process group killed pgid={}", .{pgid}), - else => |err| killpg: { - if ((comptime builtin.target.os.tag.isDarwin()) and - err == .PERM) - { - log.debug("killpg failed with EPERM, expected on Darwin and ignoring", .{}); - break :killpg; - } + else => try killPid(pid), + } + } + } - log.warn("error killing process group pgid={} err={}", .{ pgid, err }); - return error.KillFailed; - }, - } + fn killPid(pid: c.pid_t) !void { + const pgid = getpgid(pid) orelse return; - // See Command.zig wait for why we specify WNOHANG. - // The gist is that it lets us detect when children - // are still alive without blocking so that we can - // kill them again. - const res = posix.waitpid(pid, std.c.W.NOHANG); - log.debug("waitpid result={}", .{res.pid}); - if (res.pid != 0) break; - std.Thread.sleep(10 * std.time.ns_per_ms); + // It is possible to send a killpg between the time that + // our child process calls setsid but before or simultaneous + // to calling execve. In this case, the direct child dies + // but grandchildren survive. To work around this, we loop + // and repeatedly kill the process group until all + // descendents are well and truly dead. We will not rest + // until the entire family tree is obliterated. + while (true) { + switch (posix.errno(c.killpg(pgid, c.SIGHUP))) { + .SUCCESS => log.debug("process group killed pgid={}", .{pgid}), + else => |err| killpg: { + if ((comptime builtin.target.os.tag.isDarwin()) and + err == .PERM) + { + log.debug("killpg failed with EPERM, expected on Darwin and ignoring", .{}); + break :killpg; } + + log.warn("error killing process group pgid={} err={}", .{ pgid, err }); + return error.KillFailed; }, } + + // See Command.zig wait for why we specify WNOHANG. + // The gist is that it lets us detect when children + // are still alive without blocking so that we can + // kill them again. + const res = posix.waitpid(pid, std.c.W.NOHANG); + log.debug("waitpid result={}", .{res.pid}); + if (res.pid != 0) break; + std.Thread.sleep(10 * std.time.ns_per_ms); } } From 2696d50ca42003d648aa0172315b6f4d3b66443e Mon Sep 17 00:00:00 2001 From: Matthew Hrehirchuk <49077192+matthew-hre@users.noreply.github.com> Date: Sun, 19 Oct 2025 21:45:37 -0600 Subject: [PATCH 315/319] feat: added mouse-reporting / toggle-mouse-reporting (#9282) Closes #8430 A few questions: * Should I set a default keybind for `toggle-mouse-reporting`? The issue mentioned one, it's currently unset. * Am I handling the `toggle-mouse-reporting` action properly in `performAction` (gtk) / `action` (macos)? Copilot was used to understand the codebase, but code was authored manually. --- src/Surface.zig | 28 +++++++++++++++++++++++----- src/config/Config.zig | 12 ++++++++++++ src/input/Binding.zig | 12 ++++++++++++ src/input/command.zig | 6 ++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index e75c4b409..c9c40f466 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -268,6 +268,7 @@ const DerivedConfig = struct { font: font.SharedGridSet.DerivedConfig, mouse_interval: u64, mouse_hide_while_typing: bool, + mouse_reporting: bool, mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, @@ -341,6 +342,7 @@ const DerivedConfig = struct { .font = try font.SharedGridSet.DerivedConfig.init(alloc, config), .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms .mouse_hide_while_typing = config.@"mouse-hide-while-typing", + .mouse_reporting = config.@"mouse-reporting", .mouse_scroll_multiplier = config.@"mouse-scroll-multiplier", .mouse_shift_capture = config.@"mouse-shift-capture", .macos_non_native_fullscreen = config.@"macos-non-native-fullscreen", @@ -2984,7 +2986,7 @@ pub fn scrollCallback( // If we have an active mouse reporting mode, clear the selection. // The selection can occur if the user uses the shift mod key to // override mouse grabbing from the window. - if (self.io.terminal.flags.mouse_event != .none) { + if (self.isMouseReporting()) { try self.setSelection(null); } @@ -3027,7 +3029,7 @@ pub fn scrollCallback( // the normal logic. // If we're scrolling up or down, then send a mouse event. - if (self.io.terminal.flags.mouse_event != .none) { + if (self.isMouseReporting()) { for (0..@abs(y.delta)) |_| { const pos = try self.rt_surface.getCursorPos(); try self.mouseReport(switch (y.direction()) { @@ -3100,6 +3102,13 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) ! /// The type of action to report for a mouse event. const MouseReportAction = enum { press, release, motion }; +/// Returns true if mouse reporting is enabled both in the config and +/// the terminal state. +fn isMouseReporting(self: *const Surface) bool { + return self.config.mouse_reporting and + self.io.terminal.flags.mouse_event != .none; +} + fn mouseReport( self: *Surface, button: ?input.MouseButton, @@ -3107,9 +3116,13 @@ fn mouseReport( mods: input.Mods, pos: apprt.CursorPos, ) !void { + // Mouse reporting must be enabled by both config and terminal state + assert(self.config.mouse_reporting); + assert(self.io.terminal.flags.mouse_event != .none); + // Depending on the event, we may do nothing at all. switch (self.io.terminal.flags.mouse_event) { - .none => return, + .none => unreachable, // checked by assert above // X10 only reports clicks with mouse button 1, 2, 3. We verify // the button later. @@ -3504,7 +3517,7 @@ pub fn mouseButtonCallback( { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - if (self.io.terminal.flags.mouse_event != .none) report: { + if (self.isMouseReporting()) report: { // If we have shift-pressed and we aren't allowed to capture it, // then we do not do a mouse report. if (mods.shift and !shift_capture) break :report; @@ -4142,7 +4155,7 @@ pub fn cursorPosCallback( } // Do a mouse report - if (self.io.terminal.flags.mouse_event != .none) report: { + if (self.isMouseReporting()) report: { // Shift overrides mouse "grabbing" in the window, taken from Kitty. // This only applies if there is a mouse button pressed so that // movement reports are not affected. @@ -5035,6 +5048,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .toggle, ), + .toggle_mouse_reporting => { + self.config.mouse_reporting = !self.config.mouse_reporting; + log.debug("mouse reporting toggled: {}", .{self.config.mouse_reporting}); + }, + .toggle_command_palette => return try self.rt_app.performAction( .{ .surface = self }, .toggle_command_palette, diff --git a/src/config/Config.zig b/src/config/Config.zig index d0e086710..c9ae121e4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -839,6 +839,18 @@ palette: Palette = .{}, /// * `never` @"mouse-shift-capture": MouseShiftCapture = .false, +/// Enable or disable mouse reporting. When set to `false`, mouse events will +/// not be reported to terminal applications even if they request it. This +/// allows you to always use the mouse for selection and other terminal UI +/// interactions without applications capturing mouse input. +/// +/// When set to `true` (the default), terminal applications can request mouse +/// reporting and will receive mouse events according to their requested mode. +/// +/// This can be toggled at runtime using the `toggle_mouse_reporting` keybind +/// action. +@"mouse-reporting": bool = true, + /// Multiplier for scrolling distance with the mouse wheel. /// /// A prefix of `precision:` or `discrete:` can be used to set the multiplier diff --git a/src/input/Binding.zig b/src/input/Binding.zig index ad07dce55..9bdd858c1 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -637,6 +637,17 @@ pub const Action = union(enum) { /// Only implemented on macOS, as this uses a built-in system API. toggle_secure_input, + /// Toggle mouse reporting on or off. + /// + /// When mouse reporting is disabled, mouse events will not be reported to + /// terminal applications even if they request it. This allows you to always + /// use the mouse for selection and other terminal UI interactions without + /// applications capturing mouse input. + /// + /// This can also be controlled via the `mouse-reporting` configuration + /// option. + toggle_mouse_reporting, + /// Toggle the command palette. /// /// The command palette is a popup that lets you see what actions @@ -1099,6 +1110,7 @@ pub const Action = union(enum) { .toggle_window_decorations, .toggle_window_float_on_top, .toggle_secure_input, + .toggle_mouse_reporting, .toggle_command_palette, .show_on_screen_keyboard, .reset_window_size, diff --git a/src/input/command.zig b/src/input/command.zig index 29c10527e..0904ef2bb 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -449,6 +449,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle secure input mode.", }}, + .toggle_mouse_reporting => comptime &.{.{ + .action = .toggle_mouse_reporting, + .title = "Toggle Mouse Reporting", + .description = "Toggle whether mouse events are reported to terminal applications.", + }}, + .check_for_updates => comptime &.{.{ .action = .check_for_updates, .title = "Check for Updates", From 0546606e059b197479cc8f548b0990a341df6249 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 20 Oct 2025 13:14:42 -0700 Subject: [PATCH 316/319] build: add -Demit-themes option to emit/omit theme resources (#9288) We'll backport this to 1.2.x for distros that would prefer this. --- src/build/Config.zig | 7 +++++++ src/build/GhosttyResources.zig | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/build/Config.zig b/src/build/Config.zig index 745fc926f..124cf7299 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -55,6 +55,7 @@ emit_macos_app: bool = false, emit_terminfo: bool = false, emit_termcap: bool = false, emit_test_exe: bool = false, +emit_themes: bool = false, emit_xcframework: bool = false, emit_webdata: bool = false, emit_unicode_table_gen: bool = false, @@ -368,6 +369,12 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false, }; + config.emit_themes = b.option( + bool, + "emit-themes", + "Install bundled iTerm2-Color-Schemes Ghostty themes", + ) orelse true; + config.emit_webdata = b.option( bool, "emit-webdata", diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index b80aef97e..1ac8fe2a9 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -125,14 +125,16 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { } // Themes - if (b.lazyDependency("iterm2_themes", .{})) |upstream| { - const install_step = b.addInstallDirectory(.{ - .source_dir = upstream.path(""), - .install_dir = .{ .custom = "share" }, - .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), - .exclude_extensions = &.{".md"}, - }); - try steps.append(b.allocator, &install_step.step); + if (cfg.emit_themes) { + if (b.lazyDependency("iterm2_themes", .{})) |upstream| { + const install_step = b.addInstallDirectory(.{ + .source_dir = upstream.path(""), + .install_dir = .{ .custom = "share" }, + .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), + .exclude_extensions = &.{".md"}, + }); + try steps.append(b.allocator, &install_step.step); + } } // Fish shell completions From da165fc3cf6f8ef514f8f7b52c5bdb080509aa74 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 20 Oct 2025 14:41:35 -0700 Subject: [PATCH 317/319] input: modify other keys 2 should use all mods, ignore consumed mods (#9289) Fixes #8900 Our xterm modify other keys state 2 encoding was stripped consumed mods from the keyboard event. This doesn't match xterm or other popular terminal emulators (but most importantly: xterm). Use the full set of mods and add a test to verify this. Reproduction: ``` printf '\033[>4;2m' cat ``` Then press `ctrl+shift+h` and compare across terminals. --- src/input/key_encode.zig | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index fa641c1aa..f411deb19 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -77,7 +77,7 @@ pub fn encode( event: key.KeyEvent, opts: Options, ) std.Io.Writer.Error!void { - // log.warn("KEYENCODER self={}", .{self.*}); + //std.log.warn("KEYENCODER event={} opts={}", .{ event, opts }); return if (opts.kitty_flags.int() != 0) try kitty( writer, event, @@ -411,6 +411,10 @@ fn legacy( // ever be a multi-codepoint sequence that triggers this. if (it.nextCodepoint() != null) break :modify_other; + // The mods we encode for this are just the binding mods (shift, ctrl, + // super, alt). + const mods = event.mods.binding(); + // This copies xterm's `ModifyOtherKeys` function that returns // whether modify other keys should be encoded for the given // input. @@ -420,7 +424,7 @@ fn legacy( break :should_modify true; // If we have anything other than shift pressed, encode. - var mods_no_shift = binding_mods; + var mods_no_shift = mods; mods_no_shift.shift = false; if (!mods_no_shift.empty()) break :should_modify true; @@ -435,7 +439,7 @@ fn legacy( if (should_modify) { for (function_keys.modifiers, 2..) |modset, code| { - if (!binding_mods.equal(modset)) continue; + if (!mods.equal(modset)) continue; return try writer.print( "\x1B[27;{};{}~", .{ code, codepoint }, @@ -1970,6 +1974,20 @@ test "legacy: ctrl+shift+char with modify other state 2" { try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered()); } +test "legacy: ctrl+shift+char with modify other state 2 and consumed mods" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_h, + .mods = .{ .ctrl = true, .shift = true }, + .consumed_mods = .{ .shift = true }, + .utf8 = "H", + }, .{ + .modify_other_keys_state_2 = true, + }); + try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered()); +} + test "legacy: fixterm awkward letters" { var buf: [128]u8 = undefined; { From 3548acfac63e7674b5e25896f6b393474fe8ea65 Mon Sep 17 00:00:00 2001 From: Jared Gizersky <77700596+jaredgizersky@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:27:04 -0400 Subject: [PATCH 318/319] os: handle nil languageCode/countryCode in setLangFromCocoa (#9290) Fixes a crash when NSLocale returns nil for languageCode or countryCode properties. This can happen when the app launches without locale environment variables set. The crash occurs at `src/os/locale.zig:87-88` when trying to call `getProperty()` on a nil object. The fix adds a null check and falls back to `en_US.UTF-8` instead of dereferencing null. ## Testing Tested by running with locale variables unset: ```bash unset LC_ALL && ./zig-out/Ghostty.app/Contents/MacOS/ghostty ``` Before: segmentation fault After: launches successfully with fallback locale --- src/os/locale.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/os/locale.zig b/src/os/locale.zig index b391d690f..92a63741f 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -83,6 +83,11 @@ fn setLangFromCocoa() void { const lang = locale.getProperty(objc.Object, "languageCode"); const country = locale.getProperty(objc.Object, "countryCode"); + if (lang.value == null or country.value == null) { + log.warn("languageCode or countryCode not found. Locale may be incorrect.", .{}); + return; + } + // Get our UTF8 string values const c_lang = lang.getProperty([*:0]const u8, "UTF8String"); const c_country = country.getProperty([*:0]const u8, "UTF8String"); From 9dc2e5978f6ee69f4d784b61865589b502c4012d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 21 Oct 2025 20:55:54 -0700 Subject: [PATCH 319/319] lib-vt: enable freestanding wasm builds (#9301) This makes `libghostty-vt` build for freestanding wasm targets (aka a browser) and produce a `ghostty-vt.wasm` file. This exports the same C API that libghostty-vt does. This commit specifically makes the changes necessary for the build to build properly and for us to run the build in CI. We don't yet actually try using it... --- .github/workflows/test.yml | 1 + build.zig | 17 +++++-- src/build/Config.zig | 8 +++- src/build/GhosttyLibVt.zig | 43 ++++++++++++++--- src/lib/allocator.zig | 6 +++ src/lib_vt.zig | 23 +++++++++ src/os/wasm.zig | 90 ----------------------------------- src/os/wasm/log.zig | 9 ++-- src/os/wasm/target.zig | 2 +- src/terminal/c/key_encode.zig | 4 +- 10 files changed, 94 insertions(+), 109 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6520a9ed5..ef03c5f32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -204,6 +204,7 @@ jobs: aarch64-linux, x86_64-linux, x86_64-windows, + wasm32-freestanding, ] runs-on: namespace-profile-ghostty-sm needs: test diff --git a/build.zig b/build.zig index 7836b5c0d..68dc0028b 100644 --- a/build.zig +++ b/build.zig @@ -101,10 +101,19 @@ pub fn build(b: *std.Build) !void { ); // libghostty-vt - const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( - b, - &mod, - ); + const libghostty_vt_shared = shared: { + if (config.target.result.cpu.arch.isWasm()) { + break :shared try buildpkg.GhosttyLibVt.initWasm( + b, + &mod, + ); + } + + break :shared try buildpkg.GhosttyLibVt.initShared( + b, + &mod, + ); + }; libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(b.getInstallStep()); diff --git a/src/build/Config.zig b/src/build/Config.zig index 124cf7299..e88213d71 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -173,7 +173,13 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { bool, "simd", "Build with SIMD-accelerated code paths. Results in significant performance improvements.", - ) orelse true; + ) orelse simd: { + // We can't build our SIMD dependencies for Wasm. Note that we may + // still use SIMD features in the Wasm-builds. + if (target.result.cpu.arch.isWasm()) break :simd false; + + break :simd true; + }; config.wayland = b.option( bool, diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 590792ef3..d1ab5d1ba 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -1,6 +1,7 @@ const GhosttyLibVt = @This(); const std = @import("std"); +const assert = std.debug.assert; const RunStep = std.Build.Step.Run; const Config = @import("Config.zig"); const GhosttyZig = @import("GhosttyZig.zig"); @@ -17,7 +18,35 @@ artifact: *std.Build.Step.InstallArtifact, /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, -pkg_config: std.Build.LazyPath, +pkg_config: ?std.Build.LazyPath, + +pub fn initWasm( + b: *std.Build, + zig: *const GhosttyZig, +) !GhosttyLibVt { + const target = zig.vt.resolved_target.?; + assert(target.result.cpu.arch.isWasm()); + + const exe = b.addExecutable(.{ + .name = "ghostty-vt", + .root_module = zig.vt_c, + .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, + }); + + // Allow exported symbols to actually be exported. + exe.rdynamic = true; + + // There is no entrypoint for this wasm module. + exe.entry = .disabled; + + return .{ + .step = &exe.step, + .artifact = b.addInstallArtifact(exe, .{}), + .output = exe.getEmittedBin(), + .dsym = null, + .pkg_config = null, + }; +} pub fn initShared( b: *std.Build, @@ -82,9 +111,11 @@ pub fn install( ) void { const b = step.owner; step.dependOn(&self.artifact.step); - step.dependOn(&b.addInstallFileWithDir( - self.pkg_config, - .prefix, - "share/pkgconfig/libghostty-vt.pc", - ).step); + if (self.pkg_config) |pkg_config| { + step.dependOn(&b.addInstallFileWithDir( + pkg_config, + .prefix, + "share/pkgconfig/libghostty-vt.pc", + ).step); + } } diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index bcd7f9dcc..ccea7ae29 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -20,11 +20,17 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { // If we're given an allocator, use it. if (c_alloc_) |c_alloc| return c_alloc.zig(); + // Tests always use the test allocator so we can detect leaks. + if (comptime builtin.is_test) return testing.allocator; + // If we have libc, use that. We prefer libc if we have it because // its generally fast but also lets the embedder easily override // malloc/free with custom allocators like mimalloc or something. if (comptime builtin.link_libc) return std.heap.c_allocator; + // Wasm + if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator; + // No libc, use the preferred allocator for releases which is the // Zig SMP allocator. return std.heap.smp_allocator; diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 1df8330ea..322f391ab 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -9,6 +9,9 @@ //! this in the future. const lib = @This(); +const std = @import("std"); +const builtin = @import("builtin"); + // The public API below reproduces a lot of terminal/main.zig but // is separate because (1) we need our root file to be in `src/` // so we can access other directories and (2) we may want to withhold @@ -126,6 +129,26 @@ comptime { } } +pub const std_options: std.Options = options: { + if (builtin.target.cpu.arch.isWasm()) break :options .{ + // Wasm builds we specifically want to optimize for space with small + // releases so we bump up to warn. Everything else acts pretty normal. + .log_level = switch (builtin.mode) { + .Debug => .debug, + .ReleaseSmall => .warn, + else => .info, + }, + + // Wasm doesn't have access to stdio so we have a custom log function. + .logFn = @import("os/wasm/log.zig").log, + }; + + // For everything else we currently use defaults. Longer term I'm + // SURE this isn't right (e.g. we definitely want to customize the log + // function for the C lib at least). + break :options .{}; +}; + test { _ = terminal; _ = @import("lib/main.zig"); diff --git a/src/os/wasm.zig b/src/os/wasm.zig index 73a5922cf..3d0b90e9a 100644 --- a/src/os/wasm.zig +++ b/src/os/wasm.zig @@ -23,93 +23,3 @@ pub const alloc = if (builtin.is_test) std.testing.allocator else std.heap.wasm_allocator; - -/// For host-owned allocations: -/// We need to keep track of our own pointer lengths because Zig -/// allocators usually don't do this and we need to be able to send -/// a direct pointer back to the host system. A more appropriate thing -/// to do would be to probably make a custom allocator that keeps track -/// of size. -var allocs: std.AutoHashMapUnmanaged([*]u8, usize) = .{}; - -/// Allocate len bytes and return a pointer to the memory in the host. -/// The data is not zeroed. -pub export fn malloc(len: usize) ?[*]u8 { - return alloc_(len) catch return null; -} - -fn alloc_(len: usize) ![*]u8 { - // Create the allocation - const slice = try alloc.alloc(u8, len); - errdefer alloc.free(slice); - - // Store the size so we can deallocate later - try allocs.putNoClobber(alloc, slice.ptr, slice.len); - errdefer _ = allocs.remove(slice.ptr); - - return slice.ptr; -} - -/// Free an allocation from malloc. -pub export fn free(ptr: ?[*]u8) void { - if (ptr) |v| { - if (allocs.get(v)) |len| { - const slice = v[0..len]; - alloc.free(slice); - _ = allocs.remove(v); - } - } -} - -/// Convert an allocated pointer of any type to a host-owned pointer. -/// This pushes the responsibility to free it to the host. The returned -/// pointer will match the pointer but is typed correctly for returning -/// to the host. -pub fn toHostOwned(ptr: anytype) ![*]u8 { - // Convert our pointer to a byte array - const info = @typeInfo(@TypeOf(ptr)).pointer; - const T = info.child; - const size = @sizeOf(T); - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - - // Store the information about it - try allocs.putNoClobber(alloc, casted, size); - errdefer _ = allocs.remove(casted); - - return casted; -} - -/// Returns true if the value is host owned. -pub fn isHostOwned(ptr: anytype) bool { - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - return allocs.contains(casted); -} - -/// Convert a pointer back to a module-owned value. The caller is expected -/// to cast or have the valid pointer for alloc calls. -pub fn toModuleOwned(ptr: anytype) void { - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - _ = allocs.remove(casted); -} - -test "basics" { - const testing = std.testing; - const buf = malloc(32).?; - try testing.expect(allocs.size == 1); - free(buf); - try testing.expect(allocs.size == 0); -} - -test "toHostOwned" { - const testing = std.testing; - - const Point = struct { x: u32 = 0, y: u32 = 0 }; - const p = try alloc.create(Point); - errdefer alloc.destroy(p); - const ptr = try toHostOwned(p); - try testing.expect(allocs.size == 1); - try testing.expect(isHostOwned(p)); - try testing.expect(isHostOwned(ptr)); - free(ptr); - try testing.expect(allocs.size == 0); -} diff --git a/src/os/wasm/log.zig b/src/os/wasm/log.zig index d81571229..1aac8c4e7 100644 --- a/src/os/wasm/log.zig +++ b/src/os/wasm/log.zig @@ -1,15 +1,12 @@ const std = @import("std"); const builtin = @import("builtin"); const wasm = @import("../wasm.zig"); -const wasm_target = @import("target.zig"); // Use the correct implementation -pub const log = if (wasm_target.target) |target| switch (target) { - .browser => Browser.log, -} else @compileError("wasm target required"); +pub const log = Freestanding.log; -/// Browser implementation calls an extern "log" function. -pub const Browser = struct { +/// Freestanding implementation calls an extern "log" function. +pub const Freestanding = struct { // The function std.log will call. pub fn log( comptime level: std.log.Level, diff --git a/src/os/wasm/target.zig b/src/os/wasm/target.zig index cd8b2dd33..a6a29e208 100644 --- a/src/os/wasm/target.zig +++ b/src/os/wasm/target.zig @@ -10,7 +10,7 @@ pub const Target = enum { }; /// Our specific target platform. -pub const target: ?Target = if (!builtin.target.isWasm()) null else target: { +pub const target: ?Target = if (!builtin.target.cpu.arch.isWasm()) null else target: { const result = @as(Target, @enumFromInt(@intFromEnum(options.wasm_target))); // This maybe isn't necessary but I don't know if enums without a specific // tag type and value are guaranteed to be the same between build.zig diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index 96754d884..f5f6ff054 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -123,7 +123,9 @@ pub fn encode( encoder_.?.opts, ) catch unreachable; - out_written.* = discarding.count; + // Discarding always uses a u64. If we're on 32-bit systems + // we cast down. We should make this safer in the future. + out_written.* = @intCast(discarding.count); return .out_of_memory; }, };